LCOV - code coverage report
Current view: top level - contrib/vacuumlo - vacuumlo.c (source / functions) Coverage Total Hit
Test: Code coverage Lines: 0.0 % 298 0
Test Date: 2026-01-26 10:56:24 Functions: 0.0 % 3 0
Legend: Lines:     hit not hit

            Line data    Source code
       1              : /*-------------------------------------------------------------------------
       2              :  *
       3              :  * vacuumlo.c
       4              :  *        This removes orphaned large objects from a database.
       5              :  *
       6              :  * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
       7              :  * Portions Copyright (c) 1994, Regents of the University of California
       8              :  *
       9              :  *
      10              :  * IDENTIFICATION
      11              :  *        contrib/vacuumlo/vacuumlo.c
      12              :  *
      13              :  *-------------------------------------------------------------------------
      14              :  */
      15              : #include "postgres_fe.h"
      16              : 
      17              : #include <sys/stat.h>
      18              : #include <fcntl.h>
      19              : #include <unistd.h>
      20              : #ifdef HAVE_TERMIOS_H
      21              : #include <termios.h>
      22              : #endif
      23              : 
      24              : #include "catalog/pg_class_d.h"
      25              : #include "common/connect.h"
      26              : #include "common/logging.h"
      27              : #include "common/string.h"
      28              : #include "getopt_long.h"
      29              : #include "libpq-fe.h"
      30              : #include "pg_getopt.h"
      31              : 
      32              : #define BUFSIZE                 1024
      33              : 
      34              : enum trivalue
      35              : {
      36              :         TRI_DEFAULT,
      37              :         TRI_NO,
      38              :         TRI_YES,
      39              : };
      40              : 
      41              : struct _param
      42              : {
      43              :         char       *pg_user;
      44              :         enum trivalue pg_prompt;
      45              :         char       *pg_port;
      46              :         char       *pg_host;
      47              :         const char *progname;
      48              :         int                     verbose;
      49              :         int                     dry_run;
      50              :         long            transaction_limit;
      51              : };
      52              : 
      53              : static int      vacuumlo(const char *database, const struct _param *param);
      54              : static void usage(const char *progname);
      55              : 
      56              : 
      57              : 
      58              : /*
      59              :  * This vacuums LOs of one database. It returns 0 on success, -1 on failure.
      60              :  */
      61              : static int
      62            0 : vacuumlo(const char *database, const struct _param *param)
      63              : {
      64            0 :         PGconn     *conn;
      65            0 :         PGresult   *res,
      66              :                            *res2;
      67            0 :         char            buf[BUFSIZE];
      68            0 :         long            matched;
      69            0 :         long            deleted;
      70            0 :         int                     i;
      71            0 :         bool            new_pass;
      72            0 :         bool            success = true;
      73              :         static char *password = NULL;
      74              : 
      75              :         /* Note: password can be carried over from a previous call */
      76            0 :         if (param->pg_prompt == TRI_YES && !password)
      77            0 :                 password = simple_prompt("Password: ", false);
      78              : 
      79              :         /*
      80              :          * Start the connection.  Loop until we have a password if requested by
      81              :          * backend.
      82              :          */
      83            0 :         do
      84              :         {
      85              : #define PARAMS_ARRAY_SIZE          7
      86              : 
      87            0 :                 const char *keywords[PARAMS_ARRAY_SIZE];
      88            0 :                 const char *values[PARAMS_ARRAY_SIZE];
      89              : 
      90            0 :                 keywords[0] = "host";
      91            0 :                 values[0] = param->pg_host;
      92            0 :                 keywords[1] = "port";
      93            0 :                 values[1] = param->pg_port;
      94            0 :                 keywords[2] = "user";
      95            0 :                 values[2] = param->pg_user;
      96            0 :                 keywords[3] = "password";
      97            0 :                 values[3] = password;
      98            0 :                 keywords[4] = "dbname";
      99            0 :                 values[4] = database;
     100            0 :                 keywords[5] = "fallback_application_name";
     101            0 :                 values[5] = param->progname;
     102            0 :                 keywords[6] = NULL;
     103            0 :                 values[6] = NULL;
     104              : 
     105            0 :                 new_pass = false;
     106            0 :                 conn = PQconnectdbParams(keywords, values, true);
     107            0 :                 if (!conn)
     108              :                 {
     109            0 :                         pg_log_error("connection to database \"%s\" failed", database);
     110            0 :                         return -1;
     111              :                 }
     112              : 
     113            0 :                 if (PQstatus(conn) == CONNECTION_BAD &&
     114            0 :                         PQconnectionNeedsPassword(conn) &&
     115            0 :                         !password &&
     116            0 :                         param->pg_prompt != TRI_NO)
     117              :                 {
     118            0 :                         PQfinish(conn);
     119            0 :                         password = simple_prompt("Password: ", false);
     120            0 :                         new_pass = true;
     121            0 :                 }
     122            0 :         } while (new_pass);
     123              : 
     124              :         /* check to see that the backend connection was successfully made */
     125            0 :         if (PQstatus(conn) == CONNECTION_BAD)
     126              :         {
     127            0 :                 pg_log_error("%s", PQerrorMessage(conn));
     128            0 :                 PQfinish(conn);
     129            0 :                 return -1;
     130              :         }
     131              : 
     132            0 :         if (param->verbose)
     133              :         {
     134            0 :                 fprintf(stdout, "Connected to database \"%s\"\n", database);
     135            0 :                 if (param->dry_run)
     136            0 :                         fprintf(stdout, "Test run: no large objects will be removed!\n");
     137            0 :         }
     138              : 
     139            0 :         res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
     140            0 :         if (PQresultStatus(res) != PGRES_TUPLES_OK)
     141              :         {
     142            0 :                 pg_log_error("failed to set \"search_path\": %s", PQerrorMessage(conn));
     143            0 :                 PQclear(res);
     144            0 :                 PQfinish(conn);
     145            0 :                 return -1;
     146              :         }
     147            0 :         PQclear(res);
     148              : 
     149              :         /*
     150              :          * First we create and populate the LO temp table
     151              :          */
     152            0 :         buf[0] = '\0';
     153            0 :         strcat(buf, "CREATE TEMP TABLE vacuum_l AS ");
     154            0 :         if (PQserverVersion(conn) >= 90000)
     155            0 :                 strcat(buf, "SELECT oid AS lo FROM pg_largeobject_metadata");
     156              :         else
     157            0 :                 strcat(buf, "SELECT DISTINCT loid AS lo FROM pg_largeobject");
     158            0 :         res = PQexec(conn, buf);
     159            0 :         if (PQresultStatus(res) != PGRES_COMMAND_OK)
     160              :         {
     161            0 :                 pg_log_error("failed to create temp table: %s", PQerrorMessage(conn));
     162            0 :                 PQclear(res);
     163            0 :                 PQfinish(conn);
     164            0 :                 return -1;
     165              :         }
     166            0 :         PQclear(res);
     167              : 
     168              :         /*
     169              :          * Analyze the temp table so that planner will generate decent plans for
     170              :          * the DELETEs below.
     171              :          */
     172            0 :         buf[0] = '\0';
     173            0 :         strcat(buf, "ANALYZE vacuum_l");
     174            0 :         res = PQexec(conn, buf);
     175            0 :         if (PQresultStatus(res) != PGRES_COMMAND_OK)
     176              :         {
     177            0 :                 pg_log_error("failed to vacuum temp table: %s", PQerrorMessage(conn));
     178            0 :                 PQclear(res);
     179            0 :                 PQfinish(conn);
     180            0 :                 return -1;
     181              :         }
     182            0 :         PQclear(res);
     183              : 
     184              :         /*
     185              :          * Now find any candidate tables that have columns of type oid.
     186              :          *
     187              :          * NOTE: we ignore system tables and temp tables by the expedient of
     188              :          * rejecting tables in schemas named 'pg_*'.  In particular, the temp
     189              :          * table formed above is ignored, and pg_largeobject will be too. If
     190              :          * either of these were scanned, obviously we'd end up with nothing to
     191              :          * delete...
     192              :          */
     193            0 :         buf[0] = '\0';
     194            0 :         strcat(buf, "SELECT s.nspname, c.relname, a.attname ");
     195            0 :         strcat(buf, "FROM pg_class c, pg_attribute a, pg_namespace s, pg_type t ");
     196            0 :         strcat(buf, "WHERE a.attnum > 0 AND NOT a.attisdropped ");
     197            0 :         strcat(buf, "      AND a.attrelid = c.oid ");
     198            0 :         strcat(buf, "      AND a.atttypid = t.oid ");
     199            0 :         strcat(buf, "      AND c.relnamespace = s.oid ");
     200            0 :         strcat(buf, "      AND t.typname in ('oid', 'lo') ");
     201            0 :         strcat(buf, "      AND c.relkind in (" CppAsString2(RELKIND_RELATION) ", " CppAsString2(RELKIND_MATVIEW) ")");
     202            0 :         strcat(buf, "      AND s.nspname !~ '^pg_'");
     203            0 :         res = PQexec(conn, buf);
     204            0 :         if (PQresultStatus(res) != PGRES_TUPLES_OK)
     205              :         {
     206            0 :                 pg_log_error("failed to find OID columns: %s", PQerrorMessage(conn));
     207            0 :                 PQclear(res);
     208            0 :                 PQfinish(conn);
     209            0 :                 return -1;
     210              :         }
     211              : 
     212            0 :         for (i = 0; i < PQntuples(res); i++)
     213              :         {
     214            0 :                 char       *schema,
     215              :                                    *table,
     216              :                                    *field;
     217              : 
     218            0 :                 schema = PQgetvalue(res, i, 0);
     219            0 :                 table = PQgetvalue(res, i, 1);
     220            0 :                 field = PQgetvalue(res, i, 2);
     221              : 
     222            0 :                 if (param->verbose)
     223            0 :                         fprintf(stdout, "Checking %s in %s.%s\n", field, schema, table);
     224              : 
     225            0 :                 schema = PQescapeIdentifier(conn, schema, strlen(schema));
     226            0 :                 table = PQescapeIdentifier(conn, table, strlen(table));
     227            0 :                 field = PQescapeIdentifier(conn, field, strlen(field));
     228              : 
     229            0 :                 if (!schema || !table || !field)
     230              :                 {
     231            0 :                         pg_log_error("%s", PQerrorMessage(conn));
     232            0 :                         PQclear(res);
     233            0 :                         PQfinish(conn);
     234            0 :                         PQfreemem(schema);
     235            0 :                         PQfreemem(table);
     236            0 :                         PQfreemem(field);
     237            0 :                         return -1;
     238              :                 }
     239              : 
     240            0 :                 snprintf(buf, BUFSIZE,
     241              :                                  "DELETE FROM vacuum_l "
     242              :                                  "WHERE lo IN (SELECT %s FROM %s.%s)",
     243            0 :                                  field, schema, table);
     244            0 :                 res2 = PQexec(conn, buf);
     245            0 :                 if (PQresultStatus(res2) != PGRES_COMMAND_OK)
     246              :                 {
     247            0 :                         pg_log_error("failed to check %s in table %s.%s: %s",
     248              :                                                  field, schema, table, PQerrorMessage(conn));
     249            0 :                         PQclear(res2);
     250            0 :                         PQclear(res);
     251            0 :                         PQfinish(conn);
     252            0 :                         PQfreemem(schema);
     253            0 :                         PQfreemem(table);
     254            0 :                         PQfreemem(field);
     255            0 :                         return -1;
     256              :                 }
     257            0 :                 PQclear(res2);
     258              : 
     259            0 :                 PQfreemem(schema);
     260            0 :                 PQfreemem(table);
     261            0 :                 PQfreemem(field);
     262            0 :         }
     263            0 :         PQclear(res);
     264              : 
     265              :         /*
     266              :          * Now, those entries remaining in vacuum_l are orphans.  Delete 'em.
     267              :          *
     268              :          * We don't want to run each delete as an individual transaction, because
     269              :          * the commit overhead would be high.  However, since 9.0 the backend will
     270              :          * acquire a lock per deleted LO, so deleting too many LOs per transaction
     271              :          * risks running out of room in the shared-memory lock table. Accordingly,
     272              :          * we delete up to transaction_limit LOs per transaction.
     273              :          */
     274            0 :         res = PQexec(conn, "begin");
     275            0 :         if (PQresultStatus(res) != PGRES_COMMAND_OK)
     276              :         {
     277            0 :                 pg_log_error("failed to start transaction: %s", PQerrorMessage(conn));
     278            0 :                 PQclear(res);
     279            0 :                 PQfinish(conn);
     280            0 :                 return -1;
     281              :         }
     282            0 :         PQclear(res);
     283              : 
     284            0 :         buf[0] = '\0';
     285            0 :         strcat(buf,
     286              :                    "DECLARE myportal CURSOR WITH HOLD FOR SELECT lo FROM vacuum_l");
     287            0 :         res = PQexec(conn, buf);
     288            0 :         if (PQresultStatus(res) != PGRES_COMMAND_OK)
     289              :         {
     290            0 :                 pg_log_error("DECLARE CURSOR failed: %s", PQerrorMessage(conn));
     291            0 :                 PQclear(res);
     292            0 :                 PQfinish(conn);
     293            0 :                 return -1;
     294              :         }
     295            0 :         PQclear(res);
     296              : 
     297            0 :         snprintf(buf, BUFSIZE, "FETCH FORWARD %ld IN myportal",
     298            0 :                          param->transaction_limit > 0 ? param->transaction_limit : 1000L);
     299              : 
     300            0 :         deleted = 0;
     301              : 
     302            0 :         do
     303              :         {
     304            0 :                 res = PQexec(conn, buf);
     305            0 :                 if (PQresultStatus(res) != PGRES_TUPLES_OK)
     306              :                 {
     307            0 :                         pg_log_error("FETCH FORWARD failed: %s", PQerrorMessage(conn));
     308            0 :                         PQclear(res);
     309            0 :                         PQfinish(conn);
     310            0 :                         return -1;
     311              :                 }
     312              : 
     313            0 :                 matched = PQntuples(res);
     314            0 :                 if (matched <= 0)
     315              :                 {
     316              :                         /* at end of resultset */
     317            0 :                         PQclear(res);
     318            0 :                         break;
     319              :                 }
     320              : 
     321            0 :                 for (i = 0; i < matched; i++)
     322              :                 {
     323            0 :                         Oid                     lo = atooid(PQgetvalue(res, i, 0));
     324              : 
     325            0 :                         if (param->verbose)
     326              :                         {
     327            0 :                                 fprintf(stdout, "\rRemoving lo %6u   ", lo);
     328            0 :                                 fflush(stdout);
     329            0 :                         }
     330              : 
     331            0 :                         if (param->dry_run == 0)
     332              :                         {
     333            0 :                                 if (lo_unlink(conn, lo) < 0)
     334              :                                 {
     335            0 :                                         pg_log_error("failed to remove lo %u: %s", lo,
     336              :                                                                  PQerrorMessage(conn));
     337            0 :                                         if (PQtransactionStatus(conn) == PQTRANS_INERROR)
     338              :                                         {
     339            0 :                                                 success = false;
     340            0 :                                                 break;  /* out of inner for-loop */
     341              :                                         }
     342            0 :                                 }
     343              :                                 else
     344            0 :                                         deleted++;
     345            0 :                         }
     346              :                         else
     347            0 :                                 deleted++;
     348              : 
     349            0 :                         if (param->transaction_limit > 0 &&
     350            0 :                                 (deleted % param->transaction_limit) == 0)
     351              :                         {
     352            0 :                                 res2 = PQexec(conn, "commit");
     353            0 :                                 if (PQresultStatus(res2) != PGRES_COMMAND_OK)
     354              :                                 {
     355            0 :                                         pg_log_error("failed to commit transaction: %s",
     356              :                                                                  PQerrorMessage(conn));
     357            0 :                                         PQclear(res2);
     358            0 :                                         PQclear(res);
     359            0 :                                         PQfinish(conn);
     360            0 :                                         return -1;
     361              :                                 }
     362            0 :                                 PQclear(res2);
     363            0 :                                 res2 = PQexec(conn, "begin");
     364            0 :                                 if (PQresultStatus(res2) != PGRES_COMMAND_OK)
     365              :                                 {
     366            0 :                                         pg_log_error("failed to start transaction: %s",
     367              :                                                                  PQerrorMessage(conn));
     368            0 :                                         PQclear(res2);
     369            0 :                                         PQclear(res);
     370            0 :                                         PQfinish(conn);
     371            0 :                                         return -1;
     372              :                                 }
     373            0 :                                 PQclear(res2);
     374            0 :                         }
     375            0 :                 }
     376              : 
     377            0 :                 PQclear(res);
     378            0 :         } while (success);
     379              : 
     380              :         /*
     381              :          * That's all folks!
     382              :          */
     383            0 :         res = PQexec(conn, "commit");
     384            0 :         if (PQresultStatus(res) != PGRES_COMMAND_OK)
     385              :         {
     386            0 :                 pg_log_error("failed to commit transaction: %s",
     387              :                                          PQerrorMessage(conn));
     388            0 :                 PQclear(res);
     389            0 :                 PQfinish(conn);
     390            0 :                 return -1;
     391              :         }
     392            0 :         PQclear(res);
     393              : 
     394            0 :         PQfinish(conn);
     395              : 
     396            0 :         if (param->verbose)
     397              :         {
     398            0 :                 if (param->dry_run)
     399            0 :                         fprintf(stdout, "\rWould remove %ld large objects from database \"%s\".\n",
     400            0 :                                         deleted, database);
     401            0 :                 else if (success)
     402            0 :                         fprintf(stdout,
     403              :                                         "\rSuccessfully removed %ld large objects from database \"%s\".\n",
     404            0 :                                         deleted, database);
     405              :                 else
     406            0 :                         fprintf(stdout, "\rRemoval from database \"%s\" failed at object %ld of %ld.\n",
     407            0 :                                         database, deleted, matched);
     408            0 :         }
     409              : 
     410            0 :         return ((param->dry_run || success) ? 0 : -1);
     411            0 : }
     412              : 
     413              : static void
     414            0 : usage(const char *progname)
     415              : {
     416            0 :         printf("%s removes unreferenced large objects from databases.\n\n", progname);
     417            0 :         printf("Usage:\n  %s [OPTION]... DBNAME...\n\n", progname);
     418            0 :         printf("Options:\n");
     419            0 :         printf("  -l, --limit=LIMIT         commit after removing each LIMIT large objects\n");
     420            0 :         printf("  -n, --dry-run             don't remove large objects, just show what would be done\n");
     421            0 :         printf("  -v, --verbose             write a lot of progress messages\n");
     422            0 :         printf("  -V, --version             output version information, then exit\n");
     423            0 :         printf("  -?, --help                show this help, then exit\n");
     424            0 :         printf("\nConnection options:\n");
     425            0 :         printf("  -h, --host=HOSTNAME       database server host or socket directory\n");
     426            0 :         printf("  -p, --port=PORT           database server port\n");
     427            0 :         printf("  -U, --username=USERNAME   user name to connect as\n");
     428            0 :         printf("  -w, --no-password         never prompt for password\n");
     429            0 :         printf("  -W, --password            force password prompt\n");
     430            0 :         printf("\n");
     431            0 :         printf("Report bugs to <%s>.\n", PACKAGE_BUGREPORT);
     432            0 :         printf("%s home page: <%s>\n", PACKAGE_NAME, PACKAGE_URL);
     433            0 : }
     434              : 
     435              : 
     436              : int
     437            0 : main(int argc, char **argv)
     438              : {
     439              :         static struct option long_options[] = {
     440              :                 {"host", required_argument, NULL, 'h'},
     441              :                 {"limit", required_argument, NULL, 'l'},
     442              :                 {"dry-run", no_argument, NULL, 'n'},
     443              :                 {"port", required_argument, NULL, 'p'},
     444              :                 {"username", required_argument, NULL, 'U'},
     445              :                 {"verbose", no_argument, NULL, 'v'},
     446              :                 {"version", no_argument, NULL, 'V'},
     447              :                 {"no-password", no_argument, NULL, 'w'},
     448              :                 {"password", no_argument, NULL, 'W'},
     449              :                 {"help", no_argument, NULL, '?'},
     450              :                 {NULL, 0, NULL, 0}
     451              :         };
     452              : 
     453            0 :         int                     rc = 0;
     454            0 :         struct _param param;
     455            0 :         int                     c;
     456            0 :         int                     port;
     457            0 :         const char *progname;
     458            0 :         int                     optindex;
     459              : 
     460            0 :         pg_logging_init(argv[0]);
     461            0 :         progname = get_progname(argv[0]);
     462              : 
     463              :         /* Set default parameter values */
     464            0 :         param.pg_user = NULL;
     465            0 :         param.pg_prompt = TRI_DEFAULT;
     466            0 :         param.pg_host = NULL;
     467            0 :         param.pg_port = NULL;
     468            0 :         param.progname = progname;
     469            0 :         param.verbose = 0;
     470            0 :         param.dry_run = 0;
     471            0 :         param.transaction_limit = 1000;
     472              : 
     473              :         /* Process command-line arguments */
     474            0 :         if (argc > 1)
     475              :         {
     476            0 :                 if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
     477              :                 {
     478            0 :                         usage(progname);
     479            0 :                         exit(0);
     480              :                 }
     481            0 :                 if (strcmp(argv[1], "--version") == 0 || strcmp(argv[1], "-V") == 0)
     482              :                 {
     483            0 :                         puts("vacuumlo (PostgreSQL) " PG_VERSION);
     484            0 :                         exit(0);
     485              :                 }
     486            0 :         }
     487              : 
     488            0 :         while ((c = getopt_long(argc, argv, "h:l:np:U:vwW", long_options, &optindex)) != -1)
     489              :         {
     490            0 :                 switch (c)
     491              :                 {
     492              :                         case 'h':
     493            0 :                                 param.pg_host = pg_strdup(optarg);
     494            0 :                                 break;
     495              :                         case 'l':
     496            0 :                                 param.transaction_limit = strtol(optarg, NULL, 10);
     497            0 :                                 if (param.transaction_limit < 0)
     498            0 :                                         pg_fatal("transaction limit must not be negative (0 disables)");
     499            0 :                                 break;
     500              :                         case 'n':
     501            0 :                                 param.dry_run = 1;
     502            0 :                                 param.verbose = 1;
     503            0 :                                 break;
     504              :                         case 'p':
     505            0 :                                 port = strtol(optarg, NULL, 10);
     506            0 :                                 if ((port < 1) || (port > 65535))
     507            0 :                                         pg_fatal("invalid port number: %s", optarg);
     508            0 :                                 param.pg_port = pg_strdup(optarg);
     509            0 :                                 break;
     510              :                         case 'U':
     511            0 :                                 param.pg_user = pg_strdup(optarg);
     512            0 :                                 break;
     513              :                         case 'v':
     514            0 :                                 param.verbose = 1;
     515            0 :                                 break;
     516              :                         case 'w':
     517            0 :                                 param.pg_prompt = TRI_NO;
     518            0 :                                 break;
     519              :                         case 'W':
     520            0 :                                 param.pg_prompt = TRI_YES;
     521            0 :                                 break;
     522              :                         default:
     523              :                                 /* getopt_long already emitted a complaint */
     524            0 :                                 pg_log_error_hint("Try \"%s --help\" for more information.", progname);
     525            0 :                                 exit(1);
     526              :                 }
     527              :         }
     528              : 
     529              :         /* No database given? Show usage */
     530            0 :         if (optind >= argc)
     531              :         {
     532            0 :                 pg_log_error("missing required argument: database name");
     533            0 :                 pg_log_error_hint("Try \"%s --help\" for more information.", progname);
     534            0 :                 exit(1);
     535              :         }
     536              : 
     537            0 :         for (c = optind; c < argc; c++)
     538              :         {
     539              :                 /* Work on selected database */
     540            0 :                 rc += (vacuumlo(argv[c], &param) != 0);
     541            0 :         }
     542              : 
     543            0 :         return rc;
     544            0 : }
        

Generated by: LCOV version 2.3.2-1