LCOV - code coverage report
Current view: top level - contrib/vacuumlo - vacuumlo.c (source / functions) Hit Total Coverage
Test: PostgreSQL 17devel Lines: 43 273 15.8 %
Date: 2024-04-25 23:11:32 Functions: 2 3 66.7 %
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-2024, 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             :     PGconn     *conn;
      65             :     PGresult   *res,
      66             :                *res2;
      67             :     char        buf[BUFSIZE];
      68             :     long        matched;
      69             :     long        deleted;
      70             :     int         i;
      71             :     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             :     do
      84             :     {
      85             : #define PARAMS_ARRAY_SIZE      7
      86             : 
      87             :         const char *keywords[PARAMS_ARRAY_SIZE];
      88             :         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             :         }
     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             :     }
     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             :         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             :                  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             :     }
     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             :     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             :             }
     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             :                 }
     343             :                 else
     344           0 :                     deleted++;
     345             :             }
     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             :             }
     375             :         }
     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             :                     deleted, database);
     401           0 :         else if (success)
     402           0 :             fprintf(stdout,
     403             :                     "\rSuccessfully removed %ld large objects from database \"%s\".\n",
     404             :                     deleted, database);
     405             :         else
     406           0 :             fprintf(stdout, "\rRemoval from database \"%s\" failed at object %ld of %ld.\n",
     407             :                     database, deleted, matched);
     408             :     }
     409             : 
     410           0 :     return ((param->dry_run || success) ? 0 : -1);
     411             : }
     412             : 
     413             : static void
     414           2 : usage(const char *progname)
     415             : {
     416           2 :     printf("%s removes unreferenced large objects from databases.\n\n", progname);
     417           2 :     printf("Usage:\n  %s [OPTION]... DBNAME...\n\n", progname);
     418           2 :     printf("Options:\n");
     419           2 :     printf("  -l, --limit=LIMIT         commit after removing each LIMIT large objects\n");
     420           2 :     printf("  -n, --dry-run             don't remove large objects, just show what would be done\n");
     421           2 :     printf("  -v, --verbose             write a lot of progress messages\n");
     422           2 :     printf("  -V, --version             output version information, then exit\n");
     423           2 :     printf("  -?, --help                show this help, then exit\n");
     424           2 :     printf("\nConnection options:\n");
     425           2 :     printf("  -h, --host=HOSTNAME       database server host or socket directory\n");
     426           2 :     printf("  -p, --port=PORT           database server port\n");
     427           2 :     printf("  -U, --username=USERNAME   user name to connect as\n");
     428           2 :     printf("  -w, --no-password         never prompt for password\n");
     429           2 :     printf("  -W, --password            force password prompt\n");
     430           2 :     printf("\n");
     431           2 :     printf("Report bugs to <%s>.\n", PACKAGE_BUGREPORT);
     432           2 :     printf("%s home page: <%s>\n", PACKAGE_NAME, PACKAGE_URL);
     433           2 : }
     434             : 
     435             : 
     436             : int
     437           6 : 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           6 :     int         rc = 0;
     454             :     struct _param param;
     455             :     int         c;
     456             :     int         port;
     457             :     const char *progname;
     458             :     int         optindex;
     459             : 
     460           6 :     pg_logging_init(argv[0]);
     461           6 :     progname = get_progname(argv[0]);
     462             : 
     463             :     /* Set default parameter values */
     464           6 :     param.pg_user = NULL;
     465           6 :     param.pg_prompt = TRI_DEFAULT;
     466           6 :     param.pg_host = NULL;
     467           6 :     param.pg_port = NULL;
     468           6 :     param.progname = progname;
     469           6 :     param.verbose = 0;
     470           6 :     param.dry_run = 0;
     471           6 :     param.transaction_limit = 1000;
     472             : 
     473             :     /* Process command-line arguments */
     474           6 :     if (argc > 1)
     475             :     {
     476           6 :         if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0)
     477             :         {
     478           2 :             usage(progname);
     479           2 :             exit(0);
     480             :         }
     481           4 :         if (strcmp(argv[1], "--version") == 0 || strcmp(argv[1], "-V") == 0)
     482             :         {
     483           2 :             puts("vacuumlo (PostgreSQL) " PG_VERSION);
     484           2 :             exit(0);
     485             :         }
     486             :     }
     487             : 
     488           2 :     while ((c = getopt_long(argc, argv, "h:l:np:U:vwW", long_options, &optindex)) != -1)
     489             :     {
     490           2 :         switch (c)
     491             :         {
     492           0 :             case 'h':
     493           0 :                 param.pg_host = pg_strdup(optarg);
     494           0 :                 break;
     495           0 :             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           0 :             case 'n':
     501           0 :                 param.dry_run = 1;
     502           0 :                 param.verbose = 1;
     503           0 :                 break;
     504           0 :             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           0 :             case 'U':
     511           0 :                 param.pg_user = pg_strdup(optarg);
     512           0 :                 break;
     513           0 :             case 'v':
     514           0 :                 param.verbose = 1;
     515           0 :                 break;
     516           0 :             case 'w':
     517           0 :                 param.pg_prompt = TRI_NO;
     518           0 :                 break;
     519           0 :             case 'W':
     520           0 :                 param.pg_prompt = TRI_YES;
     521           0 :                 break;
     522           2 :             default:
     523             :                 /* getopt_long already emitted a complaint */
     524           2 :                 pg_log_error_hint("Try \"%s --help\" for more information.", progname);
     525           2 :                 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             :     }
     542             : 
     543           0 :     return rc;
     544             : }

Generated by: LCOV version 1.14