Line data Source code
1 : /*-------------------------------------------------------------------------
2 : *
3 : * pg_backup_db.c
4 : *
5 : * Implements the basic DB functions used by the archiver.
6 : *
7 : * IDENTIFICATION
8 : * src/bin/pg_dump/pg_backup_db.c
9 : *
10 : *-------------------------------------------------------------------------
11 : */
12 : #include "postgres_fe.h"
13 :
14 : #include <unistd.h>
15 : #include <ctype.h>
16 : #ifdef HAVE_TERMIOS_H
17 : #include <termios.h>
18 : #endif
19 :
20 : #include "common/connect.h"
21 : #include "common/string.h"
22 : #include "dumputils.h"
23 : #include "fe_utils/string_utils.h"
24 : #include "parallel.h"
25 : #include "pg_backup_archiver.h"
26 : #include "pg_backup_db.h"
27 : #include "pg_backup_utils.h"
28 :
29 : static void _check_database_version(ArchiveHandle *AH);
30 : static void notice_processor(void *arg, const char *message);
31 :
32 : static void
33 472 : _check_database_version(ArchiveHandle *AH)
34 : {
35 : const char *remoteversion_str;
36 : int remoteversion;
37 : PGresult *res;
38 :
39 472 : remoteversion_str = PQparameterStatus(AH->connection, "server_version");
40 472 : remoteversion = PQserverVersion(AH->connection);
41 472 : if (remoteversion == 0 || !remoteversion_str)
42 0 : pg_fatal("could not get \"server_version\" from libpq");
43 :
44 472 : AH->public.remoteVersionStr = pg_strdup(remoteversion_str);
45 472 : AH->public.remoteVersion = remoteversion;
46 472 : if (!AH->archiveRemoteVersion)
47 342 : AH->archiveRemoteVersion = AH->public.remoteVersionStr;
48 :
49 472 : if (remoteversion != PG_VERSION_NUM
50 0 : && (remoteversion < AH->public.minRemoteVersion ||
51 0 : remoteversion > AH->public.maxRemoteVersion))
52 : {
53 0 : pg_log_error("aborting because of server version mismatch");
54 0 : pg_log_error_detail("server version: %s; %s version: %s",
55 : remoteversion_str, progname, PG_VERSION);
56 0 : exit(1);
57 : }
58 :
59 : /*
60 : * Check if server is in recovery mode, which means we are on a hot
61 : * standby.
62 : */
63 472 : res = ExecuteSqlQueryForSingleRow((Archive *) AH,
64 : "SELECT pg_catalog.pg_is_in_recovery()");
65 472 : AH->public.isStandby = (strcmp(PQgetvalue(res, 0, 0), "t") == 0);
66 472 : PQclear(res);
67 472 : }
68 :
69 : /*
70 : * Reconnect to the server. If dbname is not NULL, use that database,
71 : * else the one associated with the archive handle.
72 : */
73 : void
74 42 : ReconnectToServer(ArchiveHandle *AH, const char *dbname)
75 : {
76 42 : PGconn *oldConn = AH->connection;
77 42 : RestoreOptions *ropt = AH->public.ropt;
78 :
79 : /*
80 : * Save the dbname, if given, in override_dbname so that it will also
81 : * affect any later reconnection attempt.
82 : */
83 42 : if (dbname)
84 42 : ropt->cparams.override_dbname = pg_strdup(dbname);
85 :
86 : /*
87 : * Note: we want to establish the new connection, and in particular update
88 : * ArchiveHandle's connCancel, before closing old connection. Otherwise
89 : * an ill-timed SIGINT could try to access a dead connection.
90 : */
91 42 : AH->connection = NULL; /* dodge error check in ConnectDatabase */
92 :
93 42 : ConnectDatabase((Archive *) AH, &ropt->cparams, true);
94 :
95 42 : PQfinish(oldConn);
96 42 : }
97 :
98 : /*
99 : * Make, or remake, a database connection with the given parameters.
100 : *
101 : * The resulting connection handle is stored in AHX->connection.
102 : *
103 : * An interactive password prompt is automatically issued if required.
104 : * We store the results of that in AHX->savedPassword.
105 : * Note: it's not really all that sensible to use a single-entry password
106 : * cache if the username keeps changing. In current usage, however, the
107 : * username never does change, so one savedPassword is sufficient.
108 : */
109 : void
110 476 : ConnectDatabase(Archive *AHX,
111 : const ConnParams *cparams,
112 : bool isReconnect)
113 : {
114 476 : ArchiveHandle *AH = (ArchiveHandle *) AHX;
115 : trivalue prompt_password;
116 : char *password;
117 : bool new_pass;
118 :
119 476 : if (AH->connection)
120 0 : pg_fatal("already connected to a database");
121 :
122 : /* Never prompt for a password during a reconnection */
123 476 : prompt_password = isReconnect ? TRI_NO : cparams->promptPassword;
124 :
125 476 : password = AH->savedPassword;
126 :
127 476 : if (prompt_password == TRI_YES && password == NULL)
128 0 : password = simple_prompt("Password: ", false);
129 :
130 : /*
131 : * Start the connection. Loop until we have a password if requested by
132 : * backend.
133 : */
134 : do
135 : {
136 : const char *keywords[8];
137 : const char *values[8];
138 476 : int i = 0;
139 :
140 : /*
141 : * If dbname is a connstring, its entries can override the other
142 : * values obtained from cparams; but in turn, override_dbname can
143 : * override the dbname component of it.
144 : */
145 476 : keywords[i] = "host";
146 476 : values[i++] = cparams->pghost;
147 476 : keywords[i] = "port";
148 476 : values[i++] = cparams->pgport;
149 476 : keywords[i] = "user";
150 476 : values[i++] = cparams->username;
151 476 : keywords[i] = "password";
152 476 : values[i++] = password;
153 476 : keywords[i] = "dbname";
154 476 : values[i++] = cparams->dbname;
155 476 : if (cparams->override_dbname)
156 : {
157 48 : keywords[i] = "dbname";
158 48 : values[i++] = cparams->override_dbname;
159 : }
160 476 : keywords[i] = "fallback_application_name";
161 476 : values[i++] = progname;
162 476 : keywords[i] = NULL;
163 476 : values[i++] = NULL;
164 : Assert(i <= lengthof(keywords));
165 :
166 476 : new_pass = false;
167 476 : AH->connection = PQconnectdbParams(keywords, values, true);
168 :
169 476 : if (!AH->connection)
170 0 : pg_fatal("could not connect to database");
171 :
172 480 : if (PQstatus(AH->connection) == CONNECTION_BAD &&
173 4 : PQconnectionNeedsPassword(AH->connection) &&
174 0 : password == NULL &&
175 : prompt_password != TRI_NO)
176 : {
177 0 : PQfinish(AH->connection);
178 0 : password = simple_prompt("Password: ", false);
179 0 : new_pass = true;
180 : }
181 476 : } while (new_pass);
182 :
183 : /* check to see that the backend connection was successfully made */
184 476 : if (PQstatus(AH->connection) == CONNECTION_BAD)
185 : {
186 4 : if (isReconnect)
187 0 : pg_fatal("reconnection failed: %s",
188 : PQerrorMessage(AH->connection));
189 : else
190 4 : pg_fatal("%s",
191 : PQerrorMessage(AH->connection));
192 : }
193 :
194 : /* Start strict; later phases may override this. */
195 472 : PQclear(ExecuteSqlQueryForSingleRow((Archive *) AH,
196 : ALWAYS_SECURE_SEARCH_PATH_SQL));
197 :
198 472 : if (password && password != AH->savedPassword)
199 0 : free(password);
200 :
201 : /*
202 : * We want to remember connection's actual password, whether or not we got
203 : * it by prompting. So we don't just store the password variable.
204 : */
205 472 : if (PQconnectionUsedPassword(AH->connection))
206 : {
207 0 : free(AH->savedPassword);
208 0 : AH->savedPassword = pg_strdup(PQpass(AH->connection));
209 : }
210 :
211 : /* check for version mismatch */
212 472 : _check_database_version(AH);
213 :
214 472 : PQsetNoticeProcessor(AH->connection, notice_processor, NULL);
215 :
216 : /* arrange for SIGINT to issue a query cancel on this connection */
217 472 : set_archive_cancel_info(AH, AH->connection);
218 472 : }
219 :
220 : /*
221 : * Close the connection to the database and also cancel off the query if we
222 : * have one running.
223 : */
224 : void
225 430 : DisconnectDatabase(Archive *AHX)
226 : {
227 430 : ArchiveHandle *AH = (ArchiveHandle *) AHX;
228 : char errbuf[1];
229 :
230 430 : if (!AH->connection)
231 0 : return;
232 :
233 430 : if (AH->connCancel)
234 : {
235 : /*
236 : * If we have an active query, send a cancel before closing, ignoring
237 : * any errors. This is of no use for a normal exit, but might be
238 : * helpful during pg_fatal().
239 : */
240 426 : if (PQtransactionStatus(AH->connection) == PQTRANS_ACTIVE)
241 0 : (void) PQcancel(AH->connCancel, errbuf, sizeof(errbuf));
242 :
243 : /*
244 : * Prevent signal handler from sending a cancel after this.
245 : */
246 426 : set_archive_cancel_info(AH, NULL);
247 : }
248 :
249 430 : PQfinish(AH->connection);
250 430 : AH->connection = NULL;
251 : }
252 :
253 : PGconn *
254 7782 : GetConnection(Archive *AHX)
255 : {
256 7782 : ArchiveHandle *AH = (ArchiveHandle *) AHX;
257 :
258 7782 : return AH->connection;
259 : }
260 :
261 : static void
262 4 : notice_processor(void *arg, const char *message)
263 : {
264 4 : pg_log_info("%s", message);
265 4 : }
266 :
267 : /* Like pg_fatal(), but with a complaint about a particular query. */
268 : static void
269 4 : die_on_query_failure(ArchiveHandle *AH, const char *query)
270 : {
271 4 : pg_log_error("query failed: %s",
272 : PQerrorMessage(AH->connection));
273 4 : pg_log_error_detail("Query was: %s", query);
274 4 : exit(1);
275 : }
276 :
277 : void
278 5908 : ExecuteSqlStatement(Archive *AHX, const char *query)
279 : {
280 5908 : ArchiveHandle *AH = (ArchiveHandle *) AHX;
281 : PGresult *res;
282 :
283 5908 : res = PQexec(AH->connection, query);
284 5908 : if (PQresultStatus(res) != PGRES_COMMAND_OK)
285 2 : die_on_query_failure(AH, query);
286 5906 : PQclear(res);
287 5906 : }
288 :
289 : PGresult *
290 57102 : ExecuteSqlQuery(Archive *AHX, const char *query, ExecStatusType status)
291 : {
292 57102 : ArchiveHandle *AH = (ArchiveHandle *) AHX;
293 : PGresult *res;
294 :
295 57102 : res = PQexec(AH->connection, query);
296 57102 : if (PQresultStatus(res) != status)
297 2 : die_on_query_failure(AH, query);
298 57100 : return res;
299 : }
300 :
301 : /*
302 : * Execute an SQL query and verify that we got exactly one row back.
303 : */
304 : PGresult *
305 26028 : ExecuteSqlQueryForSingleRow(Archive *fout, const char *query)
306 : {
307 : PGresult *res;
308 : int ntups;
309 :
310 26028 : res = ExecuteSqlQuery(fout, query, PGRES_TUPLES_OK);
311 :
312 : /* Expecting a single result only */
313 26028 : ntups = PQntuples(res);
314 26028 : if (ntups != 1)
315 0 : pg_fatal(ngettext("query returned %d row instead of one: %s",
316 : "query returned %d rows instead of one: %s",
317 : ntups),
318 : ntups, query);
319 :
320 26028 : return res;
321 : }
322 :
323 : /*
324 : * Convenience function to send a query.
325 : * Monitors result to detect COPY statements
326 : */
327 : static void
328 14094 : ExecuteSqlCommand(ArchiveHandle *AH, const char *qry, const char *desc)
329 : {
330 14094 : PGconn *conn = AH->connection;
331 : PGresult *res;
332 :
333 : #ifdef NOT_USED
334 : fprintf(stderr, "Executing: '%s'\n\n", qry);
335 : #endif
336 14094 : res = PQexec(conn, qry);
337 :
338 14094 : switch (PQresultStatus(res))
339 : {
340 14076 : case PGRES_COMMAND_OK:
341 : case PGRES_TUPLES_OK:
342 : case PGRES_EMPTY_QUERY:
343 : /* A-OK */
344 14076 : break;
345 18 : case PGRES_COPY_IN:
346 : /* Assume this is an expected result */
347 18 : AH->pgCopyIn = true;
348 18 : break;
349 0 : default:
350 : /* trouble */
351 0 : warn_or_exit_horribly(AH, "%s: %sCommand was: %s",
352 : desc, PQerrorMessage(conn), qry);
353 0 : break;
354 : }
355 :
356 14094 : PQclear(res);
357 14094 : }
358 :
359 :
360 : /*
361 : * Process non-COPY table data (that is, INSERT commands).
362 : *
363 : * The commands have been run together as one long string for compressibility,
364 : * and we are receiving them in bufferloads with arbitrary boundaries, so we
365 : * have to locate command boundaries and save partial commands across calls.
366 : * All state must be kept in AH->sqlparse, not in local variables of this
367 : * routine. We assume that AH->sqlparse was filled with zeroes when created.
368 : *
369 : * We have to lex the data to the extent of identifying literals and quoted
370 : * identifiers, so that we can recognize statement-terminating semicolons.
371 : * We assume that INSERT data will not contain SQL comments, E'' literals,
372 : * or dollar-quoted strings, so this is much simpler than a full SQL lexer.
373 : *
374 : * Note: when restoring from a pre-9.0 dump file, this code is also used to
375 : * process BLOB COMMENTS data, which has the same problem of containing
376 : * multiple SQL commands that might be split across bufferloads. Fortunately,
377 : * that data won't contain anything complicated to lex either.
378 : */
379 : static void
380 74 : ExecuteSimpleCommands(ArchiveHandle *AH, const char *buf, size_t bufLen)
381 : {
382 74 : const char *qry = buf;
383 74 : const char *eos = buf + bufLen;
384 :
385 : /* initialize command buffer if first time through */
386 74 : if (AH->sqlparse.curCmd == NULL)
387 6 : AH->sqlparse.curCmd = createPQExpBuffer();
388 :
389 259460 : for (; qry < eos; qry++)
390 : {
391 259386 : char ch = *qry;
392 :
393 : /* For neatness, we skip any newlines between commands */
394 259386 : if (!(ch == '\n' && AH->sqlparse.curCmd->len == 0))
395 253358 : appendPQExpBufferChar(AH->sqlparse.curCmd, ch);
396 :
397 259386 : switch (AH->sqlparse.state)
398 : {
399 251386 : case SQL_SCAN: /* Default state == 0, set in _allocAH */
400 251386 : if (ch == ';')
401 : {
402 : /*
403 : * We've found the end of a statement. Send it and reset
404 : * the buffer.
405 : */
406 6000 : ExecuteSqlCommand(AH, AH->sqlparse.curCmd->data,
407 : "could not execute query");
408 6000 : resetPQExpBuffer(AH->sqlparse.curCmd);
409 : }
410 245386 : else if (ch == '\'')
411 : {
412 4000 : AH->sqlparse.state = SQL_IN_SINGLE_QUOTE;
413 4000 : AH->sqlparse.backSlash = false;
414 : }
415 241386 : else if (ch == '"')
416 : {
417 0 : AH->sqlparse.state = SQL_IN_DOUBLE_QUOTE;
418 : }
419 251386 : break;
420 :
421 8000 : case SQL_IN_SINGLE_QUOTE:
422 : /* We needn't handle '' specially */
423 8000 : if (ch == '\'' && !AH->sqlparse.backSlash)
424 4000 : AH->sqlparse.state = SQL_SCAN;
425 4000 : else if (ch == '\\' && !AH->public.std_strings)
426 0 : AH->sqlparse.backSlash = !AH->sqlparse.backSlash;
427 : else
428 4000 : AH->sqlparse.backSlash = false;
429 8000 : break;
430 :
431 0 : case SQL_IN_DOUBLE_QUOTE:
432 : /* We needn't handle "" specially */
433 0 : if (ch == '"')
434 0 : AH->sqlparse.state = SQL_SCAN;
435 0 : break;
436 : }
437 259386 : }
438 74 : }
439 :
440 :
441 : /*
442 : * Implement ahwrite() for direct-to-DB restore
443 : */
444 : int
445 7952 : ExecuteSqlCommandBuf(Archive *AHX, const char *buf, size_t bufLen)
446 : {
447 7952 : ArchiveHandle *AH = (ArchiveHandle *) AHX;
448 :
449 7952 : if (AH->outputKind == OUTPUT_COPYDATA)
450 : {
451 : /*
452 : * COPY data.
453 : *
454 : * We drop the data on the floor if libpq has failed to enter COPY
455 : * mode; this allows us to behave reasonably when trying to continue
456 : * after an error in a COPY command.
457 : */
458 40 : if (AH->pgCopyIn &&
459 20 : PQputCopyData(AH->connection, buf, bufLen) <= 0)
460 0 : pg_fatal("error returned by PQputCopyData: %s",
461 : PQerrorMessage(AH->connection));
462 : }
463 7932 : else if (AH->outputKind == OUTPUT_OTHERDATA)
464 : {
465 : /*
466 : * Table data expressed as INSERT commands; or, in old dump files,
467 : * BLOB COMMENTS data (which is expressed as COMMENT ON commands).
468 : */
469 74 : ExecuteSimpleCommands(AH, buf, bufLen);
470 : }
471 : else
472 : {
473 : /*
474 : * General SQL commands; we assume that commands will not be split
475 : * across calls.
476 : *
477 : * In most cases the data passed to us will be a null-terminated
478 : * string, but if it's not, we have to add a trailing null.
479 : */
480 7858 : if (buf[bufLen] == '\0')
481 7858 : ExecuteSqlCommand(AH, buf, "could not execute query");
482 : else
483 : {
484 0 : char *str = (char *) pg_malloc(bufLen + 1);
485 :
486 0 : memcpy(str, buf, bufLen);
487 0 : str[bufLen] = '\0';
488 0 : ExecuteSqlCommand(AH, str, "could not execute query");
489 0 : free(str);
490 : }
491 : }
492 :
493 7952 : return bufLen;
494 : }
495 :
496 : /*
497 : * Terminate a COPY operation during direct-to-DB restore
498 : */
499 : void
500 18 : EndDBCopyMode(Archive *AHX, const char *tocEntryTag)
501 : {
502 18 : ArchiveHandle *AH = (ArchiveHandle *) AHX;
503 :
504 18 : if (AH->pgCopyIn)
505 : {
506 : PGresult *res;
507 :
508 18 : if (PQputCopyEnd(AH->connection, NULL) <= 0)
509 0 : pg_fatal("error returned by PQputCopyEnd: %s",
510 : PQerrorMessage(AH->connection));
511 :
512 : /* Check command status and return to normal libpq state */
513 18 : res = PQgetResult(AH->connection);
514 18 : if (PQresultStatus(res) != PGRES_COMMAND_OK)
515 0 : warn_or_exit_horribly(AH, "COPY failed for table \"%s\": %s",
516 0 : tocEntryTag, PQerrorMessage(AH->connection));
517 18 : PQclear(res);
518 :
519 : /* Do this to ensure we've pumped libpq back to idle state */
520 18 : if (PQgetResult(AH->connection) != NULL)
521 0 : pg_log_warning("unexpected extra results during COPY of table \"%s\"",
522 : tocEntryTag);
523 :
524 18 : AH->pgCopyIn = false;
525 : }
526 18 : }
527 :
528 : void
529 118 : StartTransaction(Archive *AHX)
530 : {
531 118 : ArchiveHandle *AH = (ArchiveHandle *) AHX;
532 :
533 118 : ExecuteSqlCommand(AH, "BEGIN", "could not start database transaction");
534 118 : }
535 :
536 : void
537 118 : CommitTransaction(Archive *AHX)
538 : {
539 118 : ArchiveHandle *AH = (ArchiveHandle *) AHX;
540 :
541 118 : ExecuteSqlCommand(AH, "COMMIT", "could not commit database transaction");
542 118 : }
543 :
544 : /*
545 : * Issue per-blob commands for the large object(s) listed in the TocEntry
546 : *
547 : * The TocEntry's defn string is assumed to consist of large object OIDs,
548 : * one per line. Wrap these in the given SQL command fragments and issue
549 : * the commands. (cmdEnd need not include a semicolon.)
550 : */
551 : void
552 288 : IssueCommandPerBlob(ArchiveHandle *AH, TocEntry *te,
553 : const char *cmdBegin, const char *cmdEnd)
554 : {
555 : /* Make a writable copy of the command string */
556 288 : char *buf = pg_strdup(te->defn);
557 288 : RestoreOptions *ropt = AH->public.ropt;
558 : char *st;
559 : char *en;
560 :
561 288 : st = buf;
562 616 : while ((en = strchr(st, '\n')) != NULL)
563 : {
564 328 : *en++ = '\0';
565 328 : ahprintf(AH, "%s%s%s;\n", cmdBegin, st, cmdEnd);
566 :
567 : /* In --transaction-size mode, count each command as an action */
568 328 : if (ropt && ropt->txn_size > 0)
569 : {
570 12 : if (++AH->txnCount >= ropt->txn_size)
571 : {
572 0 : if (AH->connection)
573 : {
574 0 : CommitTransaction(&AH->public);
575 0 : StartTransaction(&AH->public);
576 : }
577 : else
578 0 : ahprintf(AH, "COMMIT;\nBEGIN;\n\n");
579 0 : AH->txnCount = 0;
580 : }
581 : }
582 :
583 328 : st = en;
584 : }
585 288 : ahprintf(AH, "\n");
586 288 : pg_free(buf);
587 288 : }
588 :
589 : /*
590 : * Process a "LARGE OBJECTS" ACL TocEntry.
591 : *
592 : * To save space in the dump file, the TocEntry contains only one copy
593 : * of the required GRANT/REVOKE commands, written to apply to the first
594 : * blob in the group (although we do not depend on that detail here).
595 : * We must expand the text to generate commands for all the blobs listed
596 : * in the associated BLOB METADATA entry.
597 : */
598 : void
599 0 : IssueACLPerBlob(ArchiveHandle *AH, TocEntry *te)
600 : {
601 0 : TocEntry *blobte = getTocEntryByDumpId(AH, te->dependencies[0]);
602 : char *buf;
603 : char *st;
604 : char *st2;
605 : char *en;
606 : bool inquotes;
607 :
608 0 : if (!blobte)
609 0 : pg_fatal("could not find entry for ID %d", te->dependencies[0]);
610 : Assert(strcmp(blobte->desc, "BLOB METADATA") == 0);
611 :
612 : /* Make a writable copy of the ACL commands string */
613 0 : buf = pg_strdup(te->defn);
614 :
615 : /*
616 : * We have to parse out the commands sufficiently to locate the blob OIDs
617 : * and find the command-ending semicolons. The commands should not
618 : * contain anything hard to parse except for double-quoted role names,
619 : * which are easy to ignore. Once we've split apart the first and second
620 : * halves of a command, apply IssueCommandPerBlob. (This means the
621 : * updates on the blobs are interleaved if there's multiple commands, but
622 : * that should cause no trouble.)
623 : */
624 0 : inquotes = false;
625 0 : st = en = buf;
626 0 : st2 = NULL;
627 0 : while (*en)
628 : {
629 : /* Ignore double-quoted material */
630 0 : if (*en == '"')
631 0 : inquotes = !inquotes;
632 0 : if (inquotes)
633 : {
634 0 : en++;
635 0 : continue;
636 : }
637 : /* If we found "LARGE OBJECT", that's the end of the first half */
638 0 : if (strncmp(en, "LARGE OBJECT ", 13) == 0)
639 : {
640 : /* Terminate the first-half string */
641 0 : en += 13;
642 : Assert(isdigit((unsigned char) *en));
643 0 : *en++ = '\0';
644 : /* Skip the rest of the blob OID */
645 0 : while (isdigit((unsigned char) *en))
646 0 : en++;
647 : /* Second half starts here */
648 : Assert(st2 == NULL);
649 0 : st2 = en;
650 : }
651 : /* If we found semicolon, that's the end of the second half */
652 0 : else if (*en == ';')
653 : {
654 : /* Terminate the second-half string */
655 0 : *en++ = '\0';
656 : Assert(st2 != NULL);
657 : /* Issue this command for each blob */
658 0 : IssueCommandPerBlob(AH, blobte, st, st2);
659 : /* For neatness, skip whitespace before the next command */
660 0 : while (isspace((unsigned char) *en))
661 0 : en++;
662 : /* Reset for new command */
663 0 : st = en;
664 0 : st2 = NULL;
665 : }
666 : else
667 0 : en++;
668 : }
669 0 : pg_free(buf);
670 0 : }
671 :
672 : void
673 0 : DropLOIfExists(ArchiveHandle *AH, Oid oid)
674 : {
675 0 : ahprintf(AH,
676 : "SELECT pg_catalog.lo_unlink(oid) "
677 : "FROM pg_catalog.pg_largeobject_metadata "
678 : "WHERE oid = '%u';\n",
679 : oid);
680 0 : }
|