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