diff options
Diffstat (limited to 'src/bin/pg_dump/pg_backup_db.c')
-rw-r--r-- | src/bin/pg_dump/pg_backup_db.c | 573 |
1 files changed, 573 insertions, 0 deletions
diff --git a/src/bin/pg_dump/pg_backup_db.c b/src/bin/pg_dump/pg_backup_db.c new file mode 100644 index 0000000..2856c16 --- /dev/null +++ b/src/bin/pg_dump/pg_backup_db.c @@ -0,0 +1,573 @@ +/*------------------------------------------------------------------------- + * + * pg_backup_db.c + * + * Implements the basic DB functions used by the archiver. + * + * IDENTIFICATION + * src/bin/pg_dump/pg_backup_db.c + * + *------------------------------------------------------------------------- + */ +#include "postgres_fe.h" + +#include <unistd.h> +#include <ctype.h> +#ifdef HAVE_TERMIOS_H +#include <termios.h> +#endif + +#include "common/connect.h" +#include "common/string.h" +#include "dumputils.h" +#include "fe_utils/string_utils.h" +#include "parallel.h" +#include "pg_backup_archiver.h" +#include "pg_backup_db.h" +#include "pg_backup_utils.h" + +static void _check_database_version(ArchiveHandle *AH); +static void notice_processor(void *arg, const char *message); + +static void +_check_database_version(ArchiveHandle *AH) +{ + const char *remoteversion_str; + int remoteversion; + PGresult *res; + + remoteversion_str = PQparameterStatus(AH->connection, "server_version"); + remoteversion = PQserverVersion(AH->connection); + if (remoteversion == 0 || !remoteversion_str) + fatal("could not get server_version from libpq"); + + AH->public.remoteVersionStr = pg_strdup(remoteversion_str); + AH->public.remoteVersion = remoteversion; + if (!AH->archiveRemoteVersion) + AH->archiveRemoteVersion = AH->public.remoteVersionStr; + + if (remoteversion != PG_VERSION_NUM + && (remoteversion < AH->public.minRemoteVersion || + remoteversion > AH->public.maxRemoteVersion)) + { + pg_log_error("server version: %s; %s version: %s", + remoteversion_str, progname, PG_VERSION); + fatal("aborting because of server version mismatch"); + } + + /* + * When running against 9.0 or later, check if we are in recovery mode, + * which means we are on a hot standby. + */ + if (remoteversion >= 90000) + { + res = ExecuteSqlQueryForSingleRow((Archive *) AH, "SELECT pg_catalog.pg_is_in_recovery()"); + + AH->public.isStandby = (strcmp(PQgetvalue(res, 0, 0), "t") == 0); + PQclear(res); + } + else + AH->public.isStandby = false; +} + +/* + * Reconnect to the server. If dbname is not NULL, use that database, + * else the one associated with the archive handle. + */ +void +ReconnectToServer(ArchiveHandle *AH, const char *dbname) +{ + PGconn *oldConn = AH->connection; + RestoreOptions *ropt = AH->public.ropt; + + /* + * Save the dbname, if given, in override_dbname so that it will also + * affect any later reconnection attempt. + */ + if (dbname) + ropt->cparams.override_dbname = pg_strdup(dbname); + + /* + * Note: we want to establish the new connection, and in particular update + * ArchiveHandle's connCancel, before closing old connection. Otherwise + * an ill-timed SIGINT could try to access a dead connection. + */ + AH->connection = NULL; /* dodge error check in ConnectDatabase */ + + ConnectDatabase((Archive *) AH, &ropt->cparams, true); + + PQfinish(oldConn); +} + +/* + * Make, or remake, a database connection with the given parameters. + * + * The resulting connection handle is stored in AHX->connection. + * + * An interactive password prompt is automatically issued if required. + * We store the results of that in AHX->savedPassword. + * Note: it's not really all that sensible to use a single-entry password + * cache if the username keeps changing. In current usage, however, the + * username never does change, so one savedPassword is sufficient. + */ +void +ConnectDatabase(Archive *AHX, + const ConnParams *cparams, + bool isReconnect) +{ + ArchiveHandle *AH = (ArchiveHandle *) AHX; + trivalue prompt_password; + char *password; + bool new_pass; + + if (AH->connection) + fatal("already connected to a database"); + + /* Never prompt for a password during a reconnection */ + prompt_password = isReconnect ? TRI_NO : cparams->promptPassword; + + password = AH->savedPassword; + + if (prompt_password == TRI_YES && password == NULL) + password = simple_prompt("Password: ", false); + + /* + * Start the connection. Loop until we have a password if requested by + * backend. + */ + do + { + const char *keywords[8]; + const char *values[8]; + int i = 0; + + /* + * If dbname is a connstring, its entries can override the other + * values obtained from cparams; but in turn, override_dbname can + * override the dbname component of it. + */ + keywords[i] = "host"; + values[i++] = cparams->pghost; + keywords[i] = "port"; + values[i++] = cparams->pgport; + keywords[i] = "user"; + values[i++] = cparams->username; + keywords[i] = "password"; + values[i++] = password; + keywords[i] = "dbname"; + values[i++] = cparams->dbname; + if (cparams->override_dbname) + { + keywords[i] = "dbname"; + values[i++] = cparams->override_dbname; + } + keywords[i] = "fallback_application_name"; + values[i++] = progname; + keywords[i] = NULL; + values[i++] = NULL; + Assert(i <= lengthof(keywords)); + + new_pass = false; + AH->connection = PQconnectdbParams(keywords, values, true); + + if (!AH->connection) + fatal("could not connect to database"); + + if (PQstatus(AH->connection) == CONNECTION_BAD && + PQconnectionNeedsPassword(AH->connection) && + password == NULL && + prompt_password != TRI_NO) + { + PQfinish(AH->connection); + password = simple_prompt("Password: ", false); + new_pass = true; + } + } while (new_pass); + + /* check to see that the backend connection was successfully made */ + if (PQstatus(AH->connection) == CONNECTION_BAD) + { + if (isReconnect) + fatal("reconnection failed: %s", + PQerrorMessage(AH->connection)); + else + fatal("%s", + PQerrorMessage(AH->connection)); + } + + /* Start strict; later phases may override this. */ + PQclear(ExecuteSqlQueryForSingleRow((Archive *) AH, + ALWAYS_SECURE_SEARCH_PATH_SQL)); + + if (password && password != AH->savedPassword) + free(password); + + /* + * We want to remember connection's actual password, whether or not we got + * it by prompting. So we don't just store the password variable. + */ + if (PQconnectionUsedPassword(AH->connection)) + { + if (AH->savedPassword) + free(AH->savedPassword); + AH->savedPassword = pg_strdup(PQpass(AH->connection)); + } + + /* check for version mismatch */ + _check_database_version(AH); + + PQsetNoticeProcessor(AH->connection, notice_processor, NULL); + + /* arrange for SIGINT to issue a query cancel on this connection */ + set_archive_cancel_info(AH, AH->connection); +} + +/* + * Close the connection to the database and also cancel off the query if we + * have one running. + */ +void +DisconnectDatabase(Archive *AHX) +{ + ArchiveHandle *AH = (ArchiveHandle *) AHX; + char errbuf[1]; + + if (!AH->connection) + return; + + if (AH->connCancel) + { + /* + * If we have an active query, send a cancel before closing, ignoring + * any errors. This is of no use for a normal exit, but might be + * helpful during fatal(). + */ + if (PQtransactionStatus(AH->connection) == PQTRANS_ACTIVE) + (void) PQcancel(AH->connCancel, errbuf, sizeof(errbuf)); + + /* + * Prevent signal handler from sending a cancel after this. + */ + set_archive_cancel_info(AH, NULL); + } + + PQfinish(AH->connection); + AH->connection = NULL; +} + +PGconn * +GetConnection(Archive *AHX) +{ + ArchiveHandle *AH = (ArchiveHandle *) AHX; + + return AH->connection; +} + +static void +notice_processor(void *arg, const char *message) +{ + pg_log_generic(PG_LOG_INFO, "%s", message); +} + +/* Like fatal(), but with a complaint about a particular query. */ +static void +die_on_query_failure(ArchiveHandle *AH, const char *query) +{ + pg_log_error("query failed: %s", + PQerrorMessage(AH->connection)); + fatal("query was: %s", query); +} + +void +ExecuteSqlStatement(Archive *AHX, const char *query) +{ + ArchiveHandle *AH = (ArchiveHandle *) AHX; + PGresult *res; + + res = PQexec(AH->connection, query); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + die_on_query_failure(AH, query); + PQclear(res); +} + +PGresult * +ExecuteSqlQuery(Archive *AHX, const char *query, ExecStatusType status) +{ + ArchiveHandle *AH = (ArchiveHandle *) AHX; + PGresult *res; + + res = PQexec(AH->connection, query); + if (PQresultStatus(res) != status) + die_on_query_failure(AH, query); + return res; +} + +/* + * Execute an SQL query and verify that we got exactly one row back. + */ +PGresult * +ExecuteSqlQueryForSingleRow(Archive *fout, const char *query) +{ + PGresult *res; + int ntups; + + res = ExecuteSqlQuery(fout, query, PGRES_TUPLES_OK); + + /* Expecting a single result only */ + ntups = PQntuples(res); + if (ntups != 1) + fatal(ngettext("query returned %d row instead of one: %s", + "query returned %d rows instead of one: %s", + ntups), + ntups, query); + + return res; +} + +/* + * Convenience function to send a query. + * Monitors result to detect COPY statements + */ +static void +ExecuteSqlCommand(ArchiveHandle *AH, const char *qry, const char *desc) +{ + PGconn *conn = AH->connection; + PGresult *res; + +#ifdef NOT_USED + fprintf(stderr, "Executing: '%s'\n\n", qry); +#endif + res = PQexec(conn, qry); + + switch (PQresultStatus(res)) + { + case PGRES_COMMAND_OK: + case PGRES_TUPLES_OK: + case PGRES_EMPTY_QUERY: + /* A-OK */ + break; + case PGRES_COPY_IN: + /* Assume this is an expected result */ + AH->pgCopyIn = true; + break; + default: + /* trouble */ + warn_or_exit_horribly(AH, "%s: %sCommand was: %s", + desc, PQerrorMessage(conn), qry); + break; + } + + PQclear(res); +} + + +/* + * Process non-COPY table data (that is, INSERT commands). + * + * The commands have been run together as one long string for compressibility, + * and we are receiving them in bufferloads with arbitrary boundaries, so we + * have to locate command boundaries and save partial commands across calls. + * All state must be kept in AH->sqlparse, not in local variables of this + * routine. We assume that AH->sqlparse was filled with zeroes when created. + * + * We have to lex the data to the extent of identifying literals and quoted + * identifiers, so that we can recognize statement-terminating semicolons. + * We assume that INSERT data will not contain SQL comments, E'' literals, + * or dollar-quoted strings, so this is much simpler than a full SQL lexer. + * + * Note: when restoring from a pre-9.0 dump file, this code is also used to + * process BLOB COMMENTS data, which has the same problem of containing + * multiple SQL commands that might be split across bufferloads. Fortunately, + * that data won't contain anything complicated to lex either. + */ +static void +ExecuteSimpleCommands(ArchiveHandle *AH, const char *buf, size_t bufLen) +{ + const char *qry = buf; + const char *eos = buf + bufLen; + + /* initialize command buffer if first time through */ + if (AH->sqlparse.curCmd == NULL) + AH->sqlparse.curCmd = createPQExpBuffer(); + + for (; qry < eos; qry++) + { + char ch = *qry; + + /* For neatness, we skip any newlines between commands */ + if (!(ch == '\n' && AH->sqlparse.curCmd->len == 0)) + appendPQExpBufferChar(AH->sqlparse.curCmd, ch); + + switch (AH->sqlparse.state) + { + case SQL_SCAN: /* Default state == 0, set in _allocAH */ + if (ch == ';') + { + /* + * We've found the end of a statement. Send it and reset + * the buffer. + */ + ExecuteSqlCommand(AH, AH->sqlparse.curCmd->data, + "could not execute query"); + resetPQExpBuffer(AH->sqlparse.curCmd); + } + else if (ch == '\'') + { + AH->sqlparse.state = SQL_IN_SINGLE_QUOTE; + AH->sqlparse.backSlash = false; + } + else if (ch == '"') + { + AH->sqlparse.state = SQL_IN_DOUBLE_QUOTE; + } + break; + + case SQL_IN_SINGLE_QUOTE: + /* We needn't handle '' specially */ + if (ch == '\'' && !AH->sqlparse.backSlash) + AH->sqlparse.state = SQL_SCAN; + else if (ch == '\\' && !AH->public.std_strings) + AH->sqlparse.backSlash = !AH->sqlparse.backSlash; + else + AH->sqlparse.backSlash = false; + break; + + case SQL_IN_DOUBLE_QUOTE: + /* We needn't handle "" specially */ + if (ch == '"') + AH->sqlparse.state = SQL_SCAN; + break; + } + } +} + + +/* + * Implement ahwrite() for direct-to-DB restore + */ +int +ExecuteSqlCommandBuf(Archive *AHX, const char *buf, size_t bufLen) +{ + ArchiveHandle *AH = (ArchiveHandle *) AHX; + + if (AH->outputKind == OUTPUT_COPYDATA) + { + /* + * COPY data. + * + * We drop the data on the floor if libpq has failed to enter COPY + * mode; this allows us to behave reasonably when trying to continue + * after an error in a COPY command. + */ + if (AH->pgCopyIn && + PQputCopyData(AH->connection, buf, bufLen) <= 0) + fatal("error returned by PQputCopyData: %s", + PQerrorMessage(AH->connection)); + } + else if (AH->outputKind == OUTPUT_OTHERDATA) + { + /* + * Table data expressed as INSERT commands; or, in old dump files, + * BLOB COMMENTS data (which is expressed as COMMENT ON commands). + */ + ExecuteSimpleCommands(AH, buf, bufLen); + } + else + { + /* + * General SQL commands; we assume that commands will not be split + * across calls. + * + * In most cases the data passed to us will be a null-terminated + * string, but if it's not, we have to add a trailing null. + */ + if (buf[bufLen] == '\0') + ExecuteSqlCommand(AH, buf, "could not execute query"); + else + { + char *str = (char *) pg_malloc(bufLen + 1); + + memcpy(str, buf, bufLen); + str[bufLen] = '\0'; + ExecuteSqlCommand(AH, str, "could not execute query"); + free(str); + } + } + + return bufLen; +} + +/* + * Terminate a COPY operation during direct-to-DB restore + */ +void +EndDBCopyMode(Archive *AHX, const char *tocEntryTag) +{ + ArchiveHandle *AH = (ArchiveHandle *) AHX; + + if (AH->pgCopyIn) + { + PGresult *res; + + if (PQputCopyEnd(AH->connection, NULL) <= 0) + fatal("error returned by PQputCopyEnd: %s", + PQerrorMessage(AH->connection)); + + /* Check command status and return to normal libpq state */ + res = PQgetResult(AH->connection); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + warn_or_exit_horribly(AH, "COPY failed for table \"%s\": %s", + tocEntryTag, PQerrorMessage(AH->connection)); + PQclear(res); + + /* Do this to ensure we've pumped libpq back to idle state */ + if (PQgetResult(AH->connection) != NULL) + pg_log_warning("unexpected extra results during COPY of table \"%s\"", + tocEntryTag); + + AH->pgCopyIn = false; + } +} + +void +StartTransaction(Archive *AHX) +{ + ArchiveHandle *AH = (ArchiveHandle *) AHX; + + ExecuteSqlCommand(AH, "BEGIN", "could not start database transaction"); +} + +void +CommitTransaction(Archive *AHX) +{ + ArchiveHandle *AH = (ArchiveHandle *) AHX; + + ExecuteSqlCommand(AH, "COMMIT", "could not commit database transaction"); +} + +void +DropBlobIfExists(ArchiveHandle *AH, Oid oid) +{ + /* + * If we are not restoring to a direct database connection, we have to + * guess about how to detect whether the blob exists. Assume new-style. + */ + if (AH->connection == NULL || + PQserverVersion(AH->connection) >= 90000) + { + ahprintf(AH, + "SELECT pg_catalog.lo_unlink(oid) " + "FROM pg_catalog.pg_largeobject_metadata " + "WHERE oid = '%u';\n", + oid); + } + else + { + /* Restoring to pre-9.0 server, so do it the old way */ + ahprintf(AH, + "SELECT CASE WHEN EXISTS(" + "SELECT 1 FROM pg_catalog.pg_largeobject WHERE loid = '%u'" + ") THEN pg_catalog.lo_unlink('%u') END;\n", + oid, oid); + } +} |