diff --git a/doc/xml/release.xml b/doc/xml/release.xml
index 83860ad31..2292f71cf 100644
--- a/doc/xml/release.xml
+++ b/doc/xml/release.xml
@@ -70,6 +70,14 @@
Add Perl interface to C storage layer.
+
+
+
+
+
+ Add Db
object to encapsulate queries and commands.
+
+
diff --git a/src/Makefile.in b/src/Makefile.in
index c6c411141..54063ee21 100644
--- a/src/Makefile.in
+++ b/src/Makefile.in
@@ -120,6 +120,9 @@ SRCS = \
config/load.c \
config/parse.c \
config/protocol.c \
+ db/db.c \
+ db/helper.c \
+ db/protocol.c \
info/info.c \
info/infoArchive.c \
info/infoBackup.c \
@@ -249,7 +252,7 @@ command/info/info.o: command/info/info.c build.auto.h command/archive/common.h c
command/local/local.o: command/local/local.c build.auto.h command/archive/get/protocol.h command/archive/push/protocol.h command/backup/protocol.h command/restore/protocol.h common/assert.h common/debug.h common/error.auto.h common/error.h common/io/filter/filter.h common/io/filter/group.h common/io/handleRead.h common/io/handleWrite.h common/io/read.h common/io/write.h common/lock.h common/log.h common/logLevel.h common/memContext.h common/stackTrace.h common/time.h common/type/buffer.h common/type/convert.h common/type/keyValue.h common/type/string.h common/type/stringList.h common/type/variant.h common/type/variantList.h config/config.auto.h config/config.h config/define.auto.h config/define.h config/protocol.h protocol/client.h protocol/command.h protocol/helper.h protocol/server.h
$(CC) $(CPPFLAGS) $(CFLAGS) $(CMAKE) -c command/local/local.c -o command/local/local.o
-command/remote/remote.o: command/remote/remote.c build.auto.h common/assert.h common/debug.h common/error.auto.h common/error.h common/io/filter/filter.h common/io/filter/group.h common/io/handleRead.h common/io/handleWrite.h common/io/read.h common/io/write.h common/lock.h common/log.h common/logLevel.h common/memContext.h common/stackTrace.h common/time.h common/type/buffer.h common/type/convert.h common/type/keyValue.h common/type/string.h common/type/stringList.h common/type/variant.h common/type/variantList.h config/config.auto.h config/config.h config/define.auto.h config/define.h config/protocol.h protocol/client.h protocol/command.h protocol/helper.h protocol/server.h storage/remote/protocol.h
+command/remote/remote.o: command/remote/remote.c build.auto.h common/assert.h common/debug.h common/error.auto.h common/error.h common/io/filter/filter.h common/io/filter/group.h common/io/handleRead.h common/io/handleWrite.h common/io/read.h common/io/write.h common/lock.h common/log.h common/logLevel.h common/memContext.h common/stackTrace.h common/time.h common/type/buffer.h common/type/convert.h common/type/keyValue.h common/type/string.h common/type/stringList.h common/type/variant.h common/type/variantList.h config/config.auto.h config/config.h config/define.auto.h config/define.h config/protocol.h db/protocol.h protocol/client.h protocol/command.h protocol/helper.h protocol/server.h storage/remote/protocol.h
$(CC) $(CPPFLAGS) $(CFLAGS) $(CMAKE) -c command/remote/remote.c -o command/remote/remote.o
command/restore/file.o: command/restore/file.c build.auto.h command/restore/file.h common/assert.h common/compress/gzip/common.h common/compress/gzip/decompress.h common/crypto/cipherBlock.h common/crypto/common.h common/crypto/hash.h common/debug.h common/error.auto.h common/error.h common/io/filter/filter.h common/io/filter/group.h common/io/filter/size.h common/io/io.h common/io/read.h common/io/write.h common/lock.h common/log.h common/logLevel.h common/memContext.h common/stackTrace.h common/time.h common/type/buffer.h common/type/convert.h common/type/keyValue.h common/type/string.h common/type/stringList.h common/type/variant.h common/type/variantList.h config/config.auto.h config/config.h config/define.auto.h config/define.h storage/helper.h storage/info.h storage/read.h storage/storage.h storage/write.h
@@ -426,6 +429,15 @@ config/parse.o: config/parse.c build.auto.h common/assert.h common/debug.h commo
config/protocol.o: config/protocol.c build.auto.h common/assert.h common/debug.h common/error.auto.h common/error.h common/io/filter/filter.h common/io/filter/group.h common/io/io.h common/io/read.h common/io/write.h common/lock.h common/log.h common/logLevel.h common/memContext.h common/stackTrace.h common/time.h common/type/buffer.h common/type/convert.h common/type/keyValue.h common/type/string.h common/type/stringList.h common/type/variant.h common/type/variantList.h config/config.auto.h config/config.h config/define.auto.h config/define.h config/protocol.h protocol/client.h protocol/command.h protocol/server.h
$(CC) $(CPPFLAGS) $(CFLAGS) $(CMAKE) -c config/protocol.c -o config/protocol.o
+db/db.o: db/db.c build.auto.h common/assert.h common/debug.h common/error.auto.h common/error.h common/io/filter/filter.h common/io/filter/group.h common/io/read.h common/io/write.h common/log.h common/logLevel.h common/macro.h common/memContext.h common/object.h common/stackTrace.h common/time.h common/type/buffer.h common/type/convert.h common/type/keyValue.h common/type/string.h common/type/stringList.h common/type/variant.h common/type/variantList.h db/db.h db/protocol.h postgres/client.h postgres/interface.h postgres/version.h protocol/client.h protocol/command.h protocol/server.h version.h
+ $(CC) $(CPPFLAGS) $(CFLAGS) $(CMAKE) -c db/db.c -o db/db.o
+
+db/helper.o: db/helper.c build.auto.h common/assert.h common/debug.h common/error.auto.h common/error.h common/io/filter/filter.h common/io/filter/group.h common/io/read.h common/io/write.h common/lock.h common/log.h common/logLevel.h common/memContext.h common/stackTrace.h common/time.h common/type/buffer.h common/type/convert.h common/type/keyValue.h common/type/string.h common/type/stringList.h common/type/variant.h common/type/variantList.h config/config.auto.h config/config.h config/define.auto.h config/define.h db/db.h db/helper.h postgres/client.h postgres/interface.h protocol/client.h protocol/command.h protocol/helper.h version.h
+ $(CC) $(CPPFLAGS) $(CFLAGS) $(CMAKE) -c db/helper.c -o db/helper.o
+
+db/protocol.o: db/protocol.c build.auto.h common/assert.h common/debug.h common/error.auto.h common/error.h common/io/filter/filter.h common/io/filter/group.h common/io/io.h common/io/read.h common/io/write.h common/lock.h common/log.h common/logLevel.h common/memContext.h common/stackTrace.h common/time.h common/type/buffer.h common/type/convert.h common/type/keyValue.h common/type/list.h common/type/string.h common/type/stringList.h common/type/variant.h common/type/variantList.h config/config.auto.h config/config.h config/define.auto.h config/define.h db/protocol.h postgres/client.h postgres/interface.h protocol/client.h protocol/command.h protocol/server.h
+ $(CC) $(CPPFLAGS) $(CFLAGS) $(CMAKE) -c db/protocol.c -o db/protocol.o
+
info/info.o: info/info.c build.auto.h common/assert.h common/crypto/cipherBlock.h common/crypto/common.h common/crypto/hash.h common/debug.h common/error.auto.h common/error.h common/ini.h common/io/filter/filter.h common/io/filter/filter.intern.h common/io/filter/group.h common/io/read.h common/io/write.h common/log.h common/logLevel.h common/macro.h common/memContext.h common/object.h common/stackTrace.h common/time.h common/type/buffer.h common/type/convert.h common/type/json.h common/type/keyValue.h common/type/string.h common/type/stringList.h common/type/variant.h common/type/variantList.h info/info.h storage/helper.h storage/info.h storage/read.h storage/storage.h storage/write.h version.h
$(CC) $(CPPFLAGS) $(CFLAGS) $(CMAKE) -c info/info.c -o info/info.o
diff --git a/src/command/remote/remote.c b/src/command/remote/remote.c
index 79fcacfa4..58fbbca01 100644
--- a/src/command/remote/remote.c
+++ b/src/command/remote/remote.c
@@ -11,6 +11,7 @@ Remote Command
#include "common/log.h"
#include "config/config.h"
#include "config/protocol.h"
+#include "db/protocol.h"
#include "protocol/helper.h"
#include "protocol/server.h"
#include "storage/remote/protocol.h"
@@ -33,6 +34,7 @@ cmdRemote(int handleRead, int handleWrite)
ProtocolServer *server = protocolServerNew(name, PROTOCOL_SERVICE_REMOTE_STR, read, write);
protocolServerHandlerAdd(server, storageRemoteProtocol);
+ protocolServerHandlerAdd(server, dbProtocol);
protocolServerHandlerAdd(server, configProtocol);
// Acquire a lock if this command needs one. We'll use the noop that is always sent from the client right after the
diff --git a/src/db/db.c b/src/db/db.c
new file mode 100644
index 000000000..1e0d67c3c
--- /dev/null
+++ b/src/db/db.c
@@ -0,0 +1,313 @@
+/***********************************************************************************************************************************
+Database Client
+***********************************************************************************************************************************/
+#include "build.auto.h"
+
+#include "common/debug.h"
+#include "common/log.h"
+#include "common/memContext.h"
+#include "common/object.h"
+#include "db/db.h"
+#include "db/protocol.h"
+#include "postgres/interface.h"
+#include "postgres/version.h"
+#include "version.h"
+
+/***********************************************************************************************************************************
+Object type
+***********************************************************************************************************************************/
+struct Db
+{
+ MemContext *memContext;
+ PgClient *client; // Local PostgreSQL client
+ ProtocolClient *remoteClient; // Protocol client for remote db queries
+ unsigned int remoteIdx; // Index provided by the remote on open for subsequent calls
+ const String *applicationName; // Used to identify this connection in PostgreSQL
+
+ unsigned int pgVersion; // Version as reported by the database
+ const String *pgDataPath; // Data directory reported by the database
+};
+
+OBJECT_DEFINE_FREE(DB);
+
+/***********************************************************************************************************************************
+Close protocol connection. No need to close a locally created PgClient since it has its own destructor.
+***********************************************************************************************************************************/
+OBJECT_DEFINE_FREE_RESOURCE_BEGIN(DB, LOG, logLevelTrace)
+{
+ ProtocolCommand *command = protocolCommandNew(PROTOCOL_COMMAND_DB_CLOSE_STR);
+ protocolCommandParamAdd(command, VARUINT(this->remoteIdx));
+
+ protocolClientExecute(this->remoteClient, command, false);
+}
+OBJECT_DEFINE_FREE_RESOURCE_END(LOG);
+
+/***********************************************************************************************************************************
+Create object
+***********************************************************************************************************************************/
+Db *
+dbNew(PgClient *client, ProtocolClient *remoteClient, const String *applicationName)
+{
+ FUNCTION_LOG_BEGIN(logLevelDebug);
+ FUNCTION_LOG_PARAM(PG_CLIENT, client);
+ FUNCTION_LOG_PARAM(PROTOCOL_CLIENT, remoteClient);
+ FUNCTION_LOG_PARAM(STRING, applicationName);
+ FUNCTION_LOG_END();
+
+ ASSERT((client != NULL && remoteClient == NULL) || (client == NULL && remoteClient != NULL));
+ ASSERT(applicationName != NULL);
+
+ Db *this = NULL;
+
+ MEM_CONTEXT_NEW_BEGIN("Db")
+ {
+ this = memNew(sizeof(Db));
+ this->memContext = memContextCurrent();
+
+ this->client = pgClientMove(client, this->memContext);
+ this->remoteClient = remoteClient;
+ this->applicationName = strDup(applicationName);
+ }
+ MEM_CONTEXT_NEW_END();
+
+ FUNCTION_LOG_RETURN(DB, this);
+}
+
+/***********************************************************************************************************************************
+Execute a query
+***********************************************************************************************************************************/
+static VariantList *
+dbQuery(Db *this, const String *query)
+{
+ FUNCTION_LOG_BEGIN(logLevelDebug);
+ FUNCTION_LOG_PARAM(DB, this);
+ FUNCTION_LOG_PARAM(STRING, query);
+ FUNCTION_LOG_END();
+
+ ASSERT(this != NULL);
+ ASSERT(query != NULL);
+
+ VariantList *result = NULL;
+
+ // Query remotely
+ if (this->remoteClient != NULL)
+ {
+ ProtocolCommand *command = protocolCommandNew(PROTOCOL_COMMAND_DB_QUERY_STR);
+ protocolCommandParamAdd(command, VARUINT(this->remoteIdx));
+ protocolCommandParamAdd(command, VARSTR(query));
+
+ result = varVarLst(protocolClientExecute(this->remoteClient, command, true));
+ }
+ // Else locally
+ else
+ result = pgClientQuery(this->client, query);
+
+ FUNCTION_LOG_RETURN(VARIANT_LIST, result);
+}
+
+/***********************************************************************************************************************************
+Execute a command that expects no output
+***********************************************************************************************************************************/
+static void
+dbExec(Db *this, const String *command)
+{
+ FUNCTION_LOG_BEGIN(logLevelDebug);
+ FUNCTION_LOG_PARAM(DB, this);
+ FUNCTION_LOG_PARAM(STRING, command);
+ FUNCTION_LOG_END();
+
+ ASSERT(this != NULL);
+ ASSERT(command != NULL);
+
+ CHECK(dbQuery(this, command) == NULL);
+
+ FUNCTION_LOG_RETURN_VOID();
+}
+
+/***********************************************************************************************************************************
+Execute a query that returns a single row and column
+***********************************************************************************************************************************/
+static Variant *
+dbQueryColumn(Db *this, const String *query)
+{
+ FUNCTION_LOG_BEGIN(logLevelDebug);
+ FUNCTION_LOG_PARAM(DB, this);
+ FUNCTION_LOG_PARAM(STRING, query);
+ FUNCTION_LOG_END();
+
+ ASSERT(this != NULL);
+ ASSERT(query != NULL);
+
+ VariantList *result = dbQuery(this, query);
+
+ CHECK(varLstSize(result) == 1);
+ CHECK(varLstSize(varVarLst(varLstGet(result, 0))) == 1);
+
+ FUNCTION_LOG_RETURN(VARIANT, varLstGet(varVarLst(varLstGet(result, 0)), 0));
+}
+
+/***********************************************************************************************************************************
+Execute a query that returns a single row
+***********************************************************************************************************************************/
+static VariantList *
+dbQueryRow(Db *this, const String *query)
+{
+ FUNCTION_LOG_BEGIN(logLevelDebug);
+ FUNCTION_LOG_PARAM(DB, this);
+ FUNCTION_LOG_PARAM(STRING, query);
+ FUNCTION_LOG_END();
+
+ ASSERT(this != NULL);
+ ASSERT(query != NULL);
+
+ VariantList *result = dbQuery(this, query);
+
+ CHECK(varLstSize(result) == 1);
+
+ FUNCTION_LOG_RETURN(VARIANT_LIST, varVarLst(varLstGet(result, 0)));
+}
+
+/***********************************************************************************************************************************
+Open the db connection
+***********************************************************************************************************************************/
+void
+dbOpen(Db *this)
+{
+ FUNCTION_LOG_BEGIN(logLevelDebug);
+ FUNCTION_LOG_PARAM(DB, this);
+ FUNCTION_LOG_END();
+
+ ASSERT(this != NULL);
+
+ MEM_CONTEXT_TEMP_BEGIN()
+ {
+ // Open the connection
+ if (this->remoteClient != NULL)
+ {
+ ProtocolCommand *command = protocolCommandNew(PROTOCOL_COMMAND_DB_OPEN_STR);
+ this->remoteIdx = varUIntForce(protocolClientExecute(this->remoteClient, command, true));
+
+ // Set a callback to notify the remote when a connection is closed
+ memContextCallbackSet(this->memContext, dbFreeResource, this);
+ }
+ else
+ pgClientOpen(this->client);
+
+ // Set search_path to prevent overrides of the functions we expect to call. All queries should also be schema-qualified,
+ // but this is an extra level protection.
+ dbExec(this, STRDEF("set search_path = 'pg_catalog'"));
+
+ // Query the version and data_directory
+ VariantList *row = dbQueryRow(
+ this,
+ STRDEF(
+ "select (select setting from pg_catalog.pg_settings where name = 'server_version_num')::int4,"
+ " (select setting from pg_catalog.pg_settings where name = 'data_directory')::text"));
+
+ // Strip the minor version off since we don't need it. In the future it might be a good idea to warn users when they are
+ // running an old minor version.
+ this->pgVersion = varUIntForce(varLstGet(row, 0)) / 100 * 100;
+
+ // Store the data directory that PostgreSQL is running in. This can be compared to the configured pgBackRest directory when
+ // validating the configuration.
+ MEM_CONTEXT_BEGIN(this->memContext)
+ {
+ this->pgDataPath = strDup(varStr(varLstGet(row, 1)));
+ }
+ MEM_CONTEXT_END();
+
+ if (this->pgVersion >= PG_VERSION_APPLICATION_NAME)
+ dbExec(this, strNewFmt("set application_name = '%s'", strPtr(this->applicationName)));
+ }
+ MEM_CONTEXT_TEMP_END();
+
+ FUNCTION_LOG_RETURN_VOID();
+}
+
+/***********************************************************************************************************************************
+Is this instance a standby?
+***********************************************************************************************************************************/
+bool
+dbIsStandby(Db *this)
+{
+ FUNCTION_LOG_BEGIN(logLevelDebug);
+ FUNCTION_LOG_PARAM(DB, this);
+ FUNCTION_LOG_END();
+
+ ASSERT(this != NULL);
+
+ bool result = false;
+
+ if (this->pgVersion >= PG_VERSION_HOT_STANDBY)
+ {
+ result = varBool(dbQueryColumn(this, STRDEF("select pg_catalog.pg_is_in_recovery()")));
+ }
+
+ FUNCTION_LOG_RETURN(BOOL, result);
+}
+
+/***********************************************************************************************************************************
+Switch the WAL segment and return the segment that should have been archived
+***********************************************************************************************************************************/
+String *
+dbWalSwitch(Db *this)
+{
+ FUNCTION_LOG_BEGIN(logLevelDebug);
+ FUNCTION_LOG_PARAM(DB, this);
+ FUNCTION_LOG_END();
+
+ ASSERT(this != NULL);
+
+ String *result = NULL;
+
+ MEM_CONTEXT_TEMP_BEGIN()
+ {
+ // Create a restore point to ensure current WAL will be archived. For versions < 9.1 activity will need to be generated by
+ // the user if there have been no writes since the last WAL switch.
+ if (this->pgVersion >= PG_VERSION_RESTORE_POINT)
+ dbQueryColumn(this, STRDEF("select pg_catalog.pg_create_restore_point('" PROJECT_NAME " Archive Check')::text"));
+
+ // Request a WAL segment switch
+ const char *walName = strPtr(pgWalName(this->pgVersion));
+ const String *walFileName = varStr(
+ dbQueryColumn(this, strNewFmt("select pg_catalog.pg_%sfile_name(pg_catalog.pg_switch_%s())::text", walName, walName)));
+
+ // Copy WAL segment name to the calling context
+ memContextSwitch(MEM_CONTEXT_OLD());
+ result = strDup(walFileName);
+ memContextSwitch(MEM_CONTEXT_TEMP());
+ }
+ MEM_CONTEXT_TEMP_END();
+
+ FUNCTION_LOG_RETURN(STRING, result);
+}
+
+/***********************************************************************************************************************************
+Move the object to a new context
+***********************************************************************************************************************************/
+Db *
+dbMove(Db *this, MemContext *parentNew)
+{
+ FUNCTION_TEST_BEGIN();
+ FUNCTION_TEST_PARAM(DB, this);
+ FUNCTION_TEST_PARAM(MEM_CONTEXT, parentNew);
+ FUNCTION_TEST_END();
+
+ ASSERT(parentNew != NULL);
+
+ if (this != NULL)
+ memContextMove(this->memContext, parentNew);
+
+ FUNCTION_TEST_RETURN(this);
+}
+
+/***********************************************************************************************************************************
+Render as string for logging
+***********************************************************************************************************************************/
+String *
+dbToLog(const Db *this)
+{
+ return strNewFmt(
+ "{client: %s, remoteClient: %s}", this->client == NULL ? "null" : strPtr(pgClientToLog(this->client)),
+ this->remoteClient == NULL ? "null" : strPtr(protocolClientToLog(this->remoteClient)));
+}
diff --git a/src/db/db.h b/src/db/db.h
new file mode 100644
index 000000000..e7b1dd4dc
--- /dev/null
+++ b/src/db/db.h
@@ -0,0 +1,51 @@
+/***********************************************************************************************************************************
+Database Client
+
+Implements the required PostgreSQL queries and commands. Notice that there is no general purpose query function -- all queries are
+expected to be embedded in this object.
+***********************************************************************************************************************************/
+#ifndef DB_DB_H
+#define DB_DB_H
+
+#include "postgres/client.h"
+#include "protocol/client.h"
+
+/***********************************************************************************************************************************
+Object type
+***********************************************************************************************************************************/
+#define DB_TYPE Db
+#define DB_PREFIX db
+
+typedef struct Db Db;
+
+/***********************************************************************************************************************************
+Constructor
+***********************************************************************************************************************************/
+Db *dbNew(PgClient *client, ProtocolClient *remoteClient, const String *applicationName);
+
+/***********************************************************************************************************************************
+Functions
+***********************************************************************************************************************************/
+void dbOpen(Db *this);
+bool dbIsStandby(Db *this);
+String *dbWalSwitch(Db *this);
+void dbClose(Db *this);
+
+Db *dbMove(Db *this, MemContext *parentNew);
+
+/***********************************************************************************************************************************
+Destructor
+***********************************************************************************************************************************/
+void dbFree(Db *this);
+
+/***********************************************************************************************************************************
+Macros for function logging
+***********************************************************************************************************************************/
+String *dbToLog(const Db *this);
+
+#define FUNCTION_LOG_DB_TYPE \
+ Db *
+#define FUNCTION_LOG_DB_FORMAT(value, buffer, bufferSize) \
+ FUNCTION_LOG_STRING_OBJECT_FORMAT(value, dbToLog, buffer, bufferSize)
+
+#endif
diff --git a/src/db/helper.c b/src/db/helper.c
new file mode 100644
index 000000000..d2f18f796
--- /dev/null
+++ b/src/db/helper.c
@@ -0,0 +1,127 @@
+/***********************************************************************************************************************************
+Database Helper
+***********************************************************************************************************************************/
+#include "build.auto.h"
+
+#include "common/debug.h"
+#include "config/config.h"
+#include "db/helper.h"
+#include "postgres/interface.h"
+#include "protocol/helper.h"
+#include "version.h"
+
+/***********************************************************************************************************************************
+Get specified cluster
+***********************************************************************************************************************************/
+static Db *
+dbGetId(unsigned int pgId)
+{
+ FUNCTION_LOG_BEGIN(logLevelDebug);
+ FUNCTION_LOG_PARAM(UINT, pgId);
+ FUNCTION_LOG_END();
+
+ ASSERT(pgId > 0);
+
+ Db *result = NULL;
+
+ MEM_CONTEXT_TEMP_BEGIN()
+ {
+ const String *applicationName = strNewFmt(PROJECT_NAME " [%s]", cfgCommandName(cfgCommand()));
+
+ if (pgIsLocal(pgId))
+ {
+ result = dbNew(
+ pgClientNew(
+ cfgOptionStr(cfgOptPgSocketPath + pgId - 1), cfgOptionUInt(cfgOptPgPort + pgId - 1), PG_DB_POSTGRES_STR, NULL,
+ (TimeMSec)(cfgOptionDbl(cfgOptDbTimeout) * MSEC_PER_SEC)),
+ NULL, applicationName);
+ }
+ else
+ result = dbNew(NULL, protocolRemoteGet(protocolStorageTypePg, pgId), applicationName);
+
+ dbMove(result, MEM_CONTEXT_OLD());
+ }
+ MEM_CONTEXT_TEMP_END();
+
+ FUNCTION_LOG_RETURN(DB, result);
+}
+
+/***********************************************************************************************************************************
+Get primary cluster or primary and standby cluster
+***********************************************************************************************************************************/
+DbGetResult
+dbGet(bool primaryOnly, bool primaryRequired)
+{
+ FUNCTION_LOG_BEGIN(logLevelDebug);
+ FUNCTION_LOG_PARAM(BOOL, primaryOnly);
+ FUNCTION_LOG_PARAM(BOOL, primaryRequired);
+ FUNCTION_LOG_END();
+
+ DbGetResult result = {0};
+
+ MEM_CONTEXT_TEMP_BEGIN()
+ {
+ // Loop through to look for primary and standby (if required)
+ for (unsigned int pgIdx = 0; pgIdx < cfgOptionIndexTotal(cfgOptPgPath); pgIdx++)
+ {
+ if (cfgOptionTest(cfgOptPgHost + pgIdx) || cfgOptionTest(cfgOptPgPath + pgIdx))
+ {
+ Db *db = NULL;
+ bool standby = false;
+
+ TRY_BEGIN()
+ {
+ db = dbGetId(pgIdx + 1);
+ dbOpen(db);
+ standby = dbIsStandby(db);
+ }
+ CATCH_ANY()
+ {
+ dbFree(db);
+ db = NULL;
+
+ LOG_WARN("unable to check pg-%u: [%s] %s", pgIdx + 1, errorTypeName(errorType()), errorMessage());
+ }
+ TRY_END();
+
+ // Was the connection successful
+ if (db != NULL)
+ {
+ // Is this cluster a standby
+ if (standby)
+ {
+ // If a standby has not already been found then assign it
+ if (result.standbyId == 0 && !primaryOnly)
+ {
+ result.standbyId = pgIdx + 1;
+ result.standby = db;
+ }
+ // Else close the connection since we don't need it
+ else
+ dbFree(db);
+ }
+ // Else is a primary
+ else
+ {
+ // Error if more than one primary was found
+ if (result.primaryId != 0)
+ THROW(DbConnectError, "more than one primary cluster found");
+
+ result.primaryId = pgIdx + 1;
+ result.primary = db;
+ }
+ }
+ }
+ }
+
+ // Error if no primary was found
+ if (result.primaryId == 0 && primaryRequired)
+ THROW(DbConnectError, "unable to find primary cluster - cannot proceed");
+
+ dbMove(result.primary, MEM_CONTEXT_OLD());
+ dbMove(result.standby, MEM_CONTEXT_OLD());
+ }
+ MEM_CONTEXT_TEMP_END();
+
+ FUNCTION_LOG_RETURN(DB_GET_RESULT, result);
+}
diff --git a/src/db/helper.h b/src/db/helper.h
new file mode 100644
index 000000000..820004062
--- /dev/null
+++ b/src/db/helper.h
@@ -0,0 +1,34 @@
+/***********************************************************************************************************************************
+Database Helper
+
+Helper functions for getting connections to PostgreSQL.
+***********************************************************************************************************************************/
+#ifndef DB_HELPER_H
+#define DB_HELPER_H
+
+#include
+
+#include "db/db.h"
+
+/***********************************************************************************************************************************
+Functions
+***********************************************************************************************************************************/
+typedef struct DbGetResult
+{
+ unsigned int primaryId;
+ Db *primary;
+ unsigned int standbyId;
+ Db *standby;
+} DbGetResult;
+
+DbGetResult dbGet(bool primaryOnly, bool primaryRequired);
+
+/***********************************************************************************************************************************
+Macros for function logging
+***********************************************************************************************************************************/
+#define FUNCTION_LOG_DB_GET_RESULT_TYPE \
+ DbGetResult
+#define FUNCTION_LOG_DB_GET_RESULT_FORMAT(value, buffer, bufferSize) \
+ objToLog(&value, "DbGetResult", buffer, bufferSize)
+
+#endif
diff --git a/src/db/protocol.c b/src/db/protocol.c
new file mode 100644
index 000000000..20cdfd209
--- /dev/null
+++ b/src/db/protocol.c
@@ -0,0 +1,96 @@
+/***********************************************************************************************************************************
+Db Protocol Handler
+***********************************************************************************************************************************/
+#include "build.auto.h"
+
+#include "common/debug.h"
+#include "common/io/io.h"
+#include "common/log.h"
+#include "common/memContext.h"
+#include "common/type/list.h"
+#include "config/config.h"
+#include "db/protocol.h"
+#include "postgres/client.h"
+#include "postgres/interface.h"
+
+/***********************************************************************************************************************************
+Constants
+***********************************************************************************************************************************/
+STRING_EXTERN(PROTOCOL_COMMAND_DB_OPEN_STR, PROTOCOL_COMMAND_DB_OPEN);
+STRING_EXTERN(PROTOCOL_COMMAND_DB_QUERY_STR, PROTOCOL_COMMAND_DB_QUERY);
+STRING_EXTERN(PROTOCOL_COMMAND_DB_CLOSE_STR, PROTOCOL_COMMAND_DB_CLOSE);
+
+/***********************************************************************************************************************************
+Local variables
+***********************************************************************************************************************************/
+static struct
+{
+ List *pgClientList; // List of db objects
+} dbProtocolLocal;
+
+/***********************************************************************************************************************************
+Process db protocol requests
+***********************************************************************************************************************************/
+bool
+dbProtocol(const String *command, const VariantList *paramList, ProtocolServer *server)
+{
+ FUNCTION_LOG_BEGIN(logLevelDebug);
+ FUNCTION_LOG_PARAM(STRING, command);
+ FUNCTION_LOG_PARAM(VARIANT_LIST, paramList);
+ FUNCTION_LOG_PARAM(PROTOCOL_SERVER, server);
+ FUNCTION_LOG_END();
+
+ ASSERT(command != NULL);
+
+ // Attempt to satisfy the request -- we may get requests that are meant for other handlers
+ bool found = true;
+
+ MEM_CONTEXT_TEMP_BEGIN()
+ {
+ if (strEq(command, PROTOCOL_COMMAND_DB_OPEN_STR))
+ {
+ // If the db list does not exist then create it in the calling context (which should be persistent)
+ if (dbProtocolLocal.pgClientList == NULL)
+ {
+ memContextSwitch(MEM_CONTEXT_OLD());
+ dbProtocolLocal.pgClientList = lstNew(sizeof(PgClient *));
+ memContextSwitch(MEM_CONTEXT_TEMP());
+ }
+
+ // Add db to the list
+ unsigned int dbIdx = lstSize(dbProtocolLocal.pgClientList);
+
+ MEM_CONTEXT_BEGIN(lstMemContext(dbProtocolLocal.pgClientList))
+ {
+ // Only a single db is passed to the remote
+ PgClient *pgClient = pgClientNew(
+ cfgOptionStr(cfgOptPgSocketPath), cfgOptionUInt(cfgOptPgPort), PG_DB_POSTGRES_STR, NULL,
+ (TimeMSec)(cfgOptionDbl(cfgOptDbTimeout) * MSEC_PER_SEC));
+ pgClientOpen(pgClient);
+
+ lstAdd(dbProtocolLocal.pgClientList, &pgClient);
+ }
+ MEM_CONTEXT_END();
+
+ // Return db index which should be included in subsequent calls
+ protocolServerResponse(server, VARUINT(dbIdx));
+ }
+ else if (strEq(command, PROTOCOL_COMMAND_DB_QUERY_STR) || strEq(command, PROTOCOL_COMMAND_DB_CLOSE_STR))
+ {
+ PgClient *pgClient = *(PgClient **)lstGet(dbProtocolLocal.pgClientList, varUIntForce(varLstGet(paramList, 0)));
+
+ if (strEq(command, PROTOCOL_COMMAND_DB_QUERY_STR))
+ protocolServerResponse(server, varNewVarLst(pgClientQuery(pgClient, varStr(varLstGet(paramList, 1)))));
+ else
+ {
+ pgClientClose(pgClient);
+ protocolServerResponse(server, NULL);
+ }
+ }
+ else
+ found = false;
+ }
+ MEM_CONTEXT_TEMP_END();
+
+ FUNCTION_LOG_RETURN(BOOL, found);
+}
diff --git a/src/db/protocol.h b/src/db/protocol.h
new file mode 100644
index 000000000..8a4ff8ab9
--- /dev/null
+++ b/src/db/protocol.h
@@ -0,0 +1,27 @@
+/***********************************************************************************************************************************
+Db Protocol Handler
+***********************************************************************************************************************************/
+#ifndef DB_PROTOCOL_H
+#define DB_PROTOCOL_H
+
+#include "common/type/string.h"
+#include "common/type/variantList.h"
+#include "protocol/client.h"
+#include "protocol/server.h"
+
+/***********************************************************************************************************************************
+Constants
+***********************************************************************************************************************************/
+#define PROTOCOL_COMMAND_DB_OPEN "dbOpen"
+ STRING_DECLARE(PROTOCOL_COMMAND_DB_OPEN_STR);
+#define PROTOCOL_COMMAND_DB_QUERY "dbQuery"
+ STRING_DECLARE(PROTOCOL_COMMAND_DB_QUERY_STR);
+#define PROTOCOL_COMMAND_DB_CLOSE "dbClose"
+ STRING_DECLARE(PROTOCOL_COMMAND_DB_CLOSE_STR);
+
+/***********************************************************************************************************************************
+Functions
+***********************************************************************************************************************************/
+bool dbProtocol(const String *command, const VariantList *paramList, ProtocolServer *server);
+
+#endif
diff --git a/src/postgres/client.c b/src/postgres/client.c
index faf562789..e46b4e5fa 100644
--- a/src/postgres/client.c
+++ b/src/postgres/client.c
@@ -9,10 +9,7 @@ Postgres Client
#include "common/log.h"
#include "common/memContext.h"
#include "common/object.h"
-#include "common/time.h"
#include "common/type/list.h"
-#include "common/type/string.h"
-#include "common/type/variantList.h"
#include "common/wait.h"
#include "postgres/client.h"
@@ -350,15 +347,36 @@ pgClientClose(PgClient *this)
FUNCTION_LOG_END();
ASSERT(this != NULL);
- CHECK(this->connection != NULL);
- memContextCallbackClear(this->memContext);
- PQfinish(this->connection);
- this->connection = NULL;
+ if (this->connection != NULL)
+ {
+ memContextCallbackClear(this->memContext);
+ PQfinish(this->connection);
+ this->connection = NULL;
+ }
FUNCTION_LOG_RETURN_VOID();
}
+/***********************************************************************************************************************************
+Move the pg client object to a new context
+***********************************************************************************************************************************/
+PgClient *
+pgClientMove(PgClient *this, MemContext *parentNew)
+{
+ FUNCTION_TEST_BEGIN();
+ FUNCTION_TEST_PARAM(PG_CLIENT, this);
+ FUNCTION_TEST_PARAM(MEM_CONTEXT, parentNew);
+ FUNCTION_TEST_END();
+
+ ASSERT(parentNew != NULL);
+
+ if (this != NULL)
+ memContextMove(this->memContext, parentNew);
+
+ FUNCTION_TEST_RETURN(this);
+}
+
/***********************************************************************************************************************************
Render as string for logging
***********************************************************************************************************************************/
diff --git a/src/postgres/client.h b/src/postgres/client.h
index 37b74b4f8..9c4a6e236 100644
--- a/src/postgres/client.h
+++ b/src/postgres/client.h
@@ -8,6 +8,10 @@ casts to queries to output one of these types.
#ifndef POSTGRES_QUERY_H
#define POSTGRES_QUERY_H
+#include "common/type/string.h"
+#include "common/type/variantList.h"
+#include "common/time.h"
+
/***********************************************************************************************************************************
Object type
***********************************************************************************************************************************/
@@ -29,6 +33,8 @@ PgClient *pgClientOpen(PgClient *this);
VariantList *pgClientQuery(PgClient *this, const String *query);
void pgClientClose(PgClient *this);
+PgClient *pgClientMove(PgClient *this, MemContext *parentNew);
+
/***********************************************************************************************************************************
Destructor
***********************************************************************************************************************************/
diff --git a/src/postgres/interface.c b/src/postgres/interface.c
index 0f44da130..fb2ca0a05 100644
--- a/src/postgres/interface.c
+++ b/src/postgres/interface.c
@@ -14,6 +14,12 @@ PostgreSQL Interface
#include "postgres/version.h"
#include "storage/helper.h"
+/***********************************************************************************************************************************
+Defines for various Postgres paths and files
+***********************************************************************************************************************************/
+STRING_EXTERN(PG_NAME_WAL_STR, PG_NAME_WAL);
+STRING_EXTERN(PG_NAME_XLOG_STR, PG_NAME_XLOG);
+
/***********************************************************************************************************************************
Define default wal segment size
@@ -35,6 +41,11 @@ something far larger needed but <= the minimum read size on just about any syste
***********************************************************************************************************************************/
#define PG_WAL_HEADER_SIZE ((unsigned int)(512))
+/***********************************************************************************************************************************
+Name of default PostgreSQL database used for running all queries and commands
+***********************************************************************************************************************************/
+STRING_EXTERN(PG_DB_POSTGRES_STR, PG_DB_POSTGRES);
+
/***********************************************************************************************************************************
PostgreSQL interface definitions
@@ -407,6 +418,19 @@ pgWalFromFile(const String *walFile)
FUNCTION_LOG_RETURN(PG_WAL, result);
}
+/***********************************************************************************************************************************
+Get WAL name (wal/xlog) for a PostgreSQL version
+***********************************************************************************************************************************/
+const String *
+pgWalName(unsigned int pgVersion)
+{
+ FUNCTION_TEST_BEGIN();
+ FUNCTION_TEST_PARAM(UINT, pgVersion);
+ FUNCTION_TEST_END();
+
+ FUNCTION_TEST_RETURN(pgVersion >= PG_VERSION_WAL_RENAME ? PG_NAME_WAL_STR : PG_NAME_XLOG_STR);
+}
+
/***********************************************************************************************************************************
Create pg_control for testing
***********************************************************************************************************************************/
diff --git a/src/postgres/interface.h b/src/postgres/interface.h
index a164c42eb..3b2db1264 100644
--- a/src/postgres/interface.h
+++ b/src/postgres/interface.h
@@ -17,6 +17,17 @@ Defines for various Postgres paths and files
#define PG_PATH_ARCHIVE_STATUS "archive_status"
#define PG_PATH_GLOBAL "global"
+#define PG_NAME_WAL "wal"
+ STRING_DECLARE(PG_NAME_WAL_STR);
+#define PG_NAME_XLOG "xlog"
+ STRING_DECLARE(PG_NAME_XLOG_STR);
+
+/***********************************************************************************************************************************
+Name of default PostgreSQL database used for running all queries and commands
+***********************************************************************************************************************************/
+#define PG_DB_POSTGRES "postgres"
+ STRING_DECLARE(PG_DB_POSTGRES_STR);
+
/***********************************************************************************************************************************
Define default page size
@@ -69,6 +80,8 @@ String *pgVersionToStr(unsigned int version);
PgWal pgWalFromFile(const String *walFile);
PgWal pgWalFromBuffer(const Buffer *walBuffer);
+const String *pgWalName(unsigned int pgVersion);
+
/***********************************************************************************************************************************
Test Functions
***********************************************************************************************************************************/
diff --git a/src/postgres/version.h b/src/postgres/version.h
index b83c84afc..2e72f6351 100644
--- a/src/postgres/version.h
+++ b/src/postgres/version.h
@@ -26,6 +26,21 @@ PostgreSQL version constants
#define PG_VERSION_MAX PG_VERSION_11
+/***********************************************************************************************************************************
+Version where various PostgreSQL capabilities were introduced
+***********************************************************************************************************************************/
+// application_name can be set to show the application name in pg_stat_activity
+#define PG_VERSION_APPLICATION_NAME PG_VERSION_90
+
+// pg_is_in_recovery() supported
+#define PG_VERSION_HOT_STANDBY PG_VERSION_91
+
+// pg_create_restore_point() supported
+#define PG_VERSION_RESTORE_POINT PG_VERSION_91
+
+// xlog was renamed to wal
+#define PG_VERSION_WAL_RENAME PG_VERSION_10
+
/***********************************************************************************************************************************
PostgreSQL version string constants for use in error messages
***********************************************************************************************************************************/
diff --git a/test/define.yaml b/test/define.yaml
index 0f8a7ad97..85e5d08e4 100644
--- a/test/define.yaml
+++ b/test/define.yaml
@@ -326,7 +326,7 @@ unit:
# ----------------------------------------------------------------------------------------------------------------------------
- name: interface
- total: 5
+ total: 6
coverage:
postgres/interface: full
@@ -539,6 +539,20 @@ unit:
- name: info-backup-perl
total: 3
+ # ********************************************************************************************************************************
+ - name: db
+
+ test:
+ # ----------------------------------------------------------------------------------------------------------------------------
+ - name: db
+ total: 2
+ perlReq: true
+
+ coverage:
+ db/db: full
+ db/helper: full
+ db/protocol: full
+
# ********************************************************************************************************************************
- name: command
diff --git a/test/src/common/harnessPq.c b/test/src/common/harnessPq.c
index 74491d9b2..c04807c4d 100644
--- a/test/src/common/harnessPq.c
+++ b/test/src/common/harnessPq.c
@@ -64,7 +64,8 @@ harnessPqScriptRun(const char *function, const VariantList *param, HarnessPq *pa
harnessPqScriptFail = true;
THROW_FMT(
- AssertError, "pq script [%u] expected function '%s' but got '%s'", harnessPqScriptIdx, result->function, function);
+ AssertError, "pq script [%u] expected function %s (%s) but got %s (%s)", harnessPqScriptIdx, result->function,
+ result->param == NULL ? "" : result->param, function, strPtr(paramStr));
}
// Check that parameters match
diff --git a/test/src/common/harnessPq.h b/test/src/common/harnessPq.h
index 1ca736176..7e32a13ee 100644
--- a/test/src/common/harnessPq.h
+++ b/test/src/common/harnessPq.h
@@ -9,7 +9,11 @@ usage examples.
#ifndef HARNESS_PQ_REAL
+#include
+
+#include "common/macro.h"
#include "common/time.h"
+#include "version.h"
/***********************************************************************************************************************************
Function constants
@@ -34,6 +38,115 @@ Function constants
#define HRNPQ_SENDQUERY "PQsendQuery"
#define HRNPQ_STATUS "PQstatus"
+/***********************************************************************************************************************************
+Macros for defining groups of functions that implement various queries and commands
+***********************************************************************************************************************************/
+#define HRNPQ_MACRO_OPEN(sessionParam, connectParam) \
+ {.session = sessionParam, .function = HRNPQ_CONNECTDB, .param = "[\"" connectParam "\"]"}, \
+ {.session = sessionParam, .function = HRNPQ_STATUS, .resultInt = CONNECTION_OK}
+
+#define HRNPQ_MACRO_SET_SEARCH_PATH(sessionParam) \
+ {.session = sessionParam, .function = HRNPQ_SENDQUERY, .param = "[\"set search_path = 'pg_catalog'\"]", .resultInt = 1}, \
+ {.session = sessionParam, .function = HRNPQ_CONSUMEINPUT}, \
+ {.session = sessionParam, .function = HRNPQ_ISBUSY}, \
+ {.session = sessionParam, .function = HRNPQ_GETRESULT}, \
+ {.session = sessionParam, .function = HRNPQ_RESULTSTATUS, .resultInt = PGRES_COMMAND_OK}, \
+ {.session = sessionParam, .function = HRNPQ_CLEAR}, \
+ {.session = sessionParam, .function = HRNPQ_GETRESULT, .resultNull = true}
+
+#define HRNPQ_MACRO_VALIDATE_QUERY(sessionParam, versionParam, pgPathParam) \
+ {.session = sessionParam, .function = HRNPQ_SENDQUERY, .param = \
+ "[\"select (select setting from pg_catalog.pg_settings where name = 'server_version_num')::int4," \
+ " (select setting from pg_catalog.pg_settings where name = 'data_directory')::text\"]", \
+ .resultInt = 1}, \
+ {.session = sessionParam, .function = HRNPQ_CONSUMEINPUT}, \
+ {.session = sessionParam, .function = HRNPQ_ISBUSY}, \
+ {.session = sessionParam, .function = HRNPQ_GETRESULT}, \
+ {.session = sessionParam, .function = HRNPQ_RESULTSTATUS, .resultInt = PGRES_TUPLES_OK}, \
+ {.session = sessionParam, .function = HRNPQ_NTUPLES, .resultInt = 1}, \
+ {.session = sessionParam, .function = HRNPQ_NFIELDS, .resultInt = 2}, \
+ {.session = sessionParam, .function = HRNPQ_FTYPE, .param = "[0]", .resultInt = HRNPQ_TYPE_INT}, \
+ {.session = sessionParam, .function = HRNPQ_FTYPE, .param = "[1]", .resultInt = HRNPQ_TYPE_TEXT}, \
+ {.session = sessionParam, .function = HRNPQ_GETVALUE, .param = "[0,0]", .resultZ = STRINGIFY(versionParam)}, \
+ {.session = sessionParam, .function = HRNPQ_GETVALUE, .param = "[0,1]", .resultZ = pgPathParam}, \
+ {.session = sessionParam, .function = HRNPQ_CLEAR}, \
+ {.session = sessionParam, .function = HRNPQ_GETRESULT, .resultNull = true}
+
+#define HRNPQ_MACRO_SET_APPLICATION_NAME(sessionParam) \
+ {.session = sessionParam, .function = HRNPQ_SENDQUERY, \
+ .param = strPtr(strNewFmt("[\"set application_name = '" PROJECT_NAME " [%s]'\"]", cfgCommandName(cfgCommand()))), \
+ .resultInt = 1}, \
+ {.session = sessionParam, .function = HRNPQ_CONSUMEINPUT}, \
+ {.session = sessionParam, .function = HRNPQ_ISBUSY}, \
+ {.session = sessionParam, .function = HRNPQ_GETRESULT}, \
+ {.session = sessionParam, .function = HRNPQ_RESULTSTATUS, .resultInt = PGRES_COMMAND_OK}, \
+ {.session = sessionParam, .function = HRNPQ_CLEAR}, \
+ {.session = sessionParam, .function = HRNPQ_GETRESULT, .resultNull = true}
+
+#define HRNPQ_MACRO_IS_STANDBY_QUERY(sessionParam, standbyParam) \
+ {.session = sessionParam, .function = HRNPQ_SENDQUERY, .param = "[\"select pg_catalog.pg_is_in_recovery()\"]", .resultInt = 1},\
+ {.session = sessionParam, .function = HRNPQ_CONSUMEINPUT}, \
+ {.session = sessionParam, .function = HRNPQ_ISBUSY}, \
+ {.session = sessionParam, .function = HRNPQ_GETRESULT}, \
+ {.session = sessionParam, .function = HRNPQ_RESULTSTATUS, .resultInt = PGRES_TUPLES_OK}, \
+ {.session = sessionParam, .function = HRNPQ_NTUPLES, .resultInt = 1}, \
+ {.session = sessionParam, .function = HRNPQ_NFIELDS, .resultInt = 1}, \
+ {.session = sessionParam, .function = HRNPQ_FTYPE, .param = "[0]", .resultInt = HRNPQ_TYPE_BOOL}, \
+ {.session = sessionParam, .function = HRNPQ_GETVALUE, .param = "[0,0]", .resultZ = STRINGIFY(standbyParam)}, \
+ {.session = sessionParam, .function = HRNPQ_CLEAR}, \
+ {.session = sessionParam, .function = HRNPQ_GETRESULT, .resultNull = true}
+
+#define HRNPQ_MACRO_CREATE_RESTORE_POINT(sessionParam, lsnParam) \
+ {.session = sessionParam, \
+ .function = HRNPQ_SENDQUERY, .param = "[\"select pg_catalog.pg_create_restore_point('pgBackRest Archive Check')::text\"]", \
+ .resultInt = 1}, \
+ {.session = sessionParam, .function = HRNPQ_CONSUMEINPUT}, \
+ {.session = sessionParam, .function = HRNPQ_ISBUSY}, \
+ {.session = sessionParam, .function = HRNPQ_GETRESULT}, \
+ {.session = sessionParam, .function = HRNPQ_RESULTSTATUS, .resultInt = PGRES_TUPLES_OK}, \
+ {.session = sessionParam, .function = HRNPQ_NTUPLES, .resultInt = 1}, \
+ {.session = sessionParam, .function = HRNPQ_NFIELDS, .resultInt = 1}, \
+ {.session = sessionParam, .function = HRNPQ_FTYPE, .param = "[0]", .resultInt = HRNPQ_TYPE_TEXT}, \
+ {.session = sessionParam, .function = HRNPQ_GETVALUE, .param = "[0,0]", .resultZ = lsnParam}, \
+ {.session = sessionParam, .function = HRNPQ_CLEAR}, \
+ {.session = sessionParam, .function = HRNPQ_GETRESULT, .resultNull = true}
+
+#define HRNPQ_MACRO_WAL_SWITCH(sessionParam, walNameParam, walFileNameParam) \
+ {.session = sessionParam, .function = HRNPQ_SENDQUERY, \
+ .param = "[\"select pg_catalog.pg_" walNameParam "file_name(pg_catalog.pg_switch_" walNameParam "())::text\"]", \
+ .resultInt = 1}, \
+ {.session = sessionParam, .function = HRNPQ_CONSUMEINPUT}, \
+ {.session = sessionParam, .function = HRNPQ_ISBUSY}, \
+ {.session = sessionParam, .function = HRNPQ_GETRESULT}, \
+ {.session = sessionParam, .function = HRNPQ_RESULTSTATUS, .resultInt = PGRES_TUPLES_OK}, \
+ {.session = sessionParam, .function = HRNPQ_NTUPLES, .resultInt = 1}, \
+ {.session = sessionParam, .function = HRNPQ_NFIELDS, .resultInt = 1}, \
+ {.session = sessionParam, .function = HRNPQ_FTYPE, .param = "[0]", .resultInt = HRNPQ_TYPE_TEXT}, \
+ {.session = sessionParam, .function = HRNPQ_GETVALUE, .param = "[0,0]", .resultZ = walFileNameParam}, \
+ {.session = sessionParam, .function = HRNPQ_CLEAR}, \
+ {.session = sessionParam, .function = HRNPQ_GETRESULT, .resultNull = true}
+
+#define HRNPQ_MACRO_CLOSE(sessionParam) \
+ {.session = sessionParam, .function = HRNPQ_FINISH}
+
+#define HRNPQ_MACRO_DONE() \
+ {.function = NULL}
+
+/***********************************************************************************************************************************
+Macros to simplify dbOpen() for specific database versions
+***********************************************************************************************************************************/
+#define HRNPQ_MACRO_OPEN_84(sessionParam, connectParam, pgPathParam) \
+ HRNPQ_MACRO_OPEN(sessionParam, connectParam), \
+ HRNPQ_MACRO_SET_SEARCH_PATH(sessionParam), \
+ HRNPQ_MACRO_VALIDATE_QUERY(sessionParam, PG_VERSION_84, pgPathParam)
+
+#define HRNPQ_MACRO_OPEN_92(sessionParam, connectParam, pgPathParam, standbyParam) \
+ HRNPQ_MACRO_OPEN(sessionParam, connectParam), \
+ HRNPQ_MACRO_SET_SEARCH_PATH(sessionParam), \
+ HRNPQ_MACRO_VALIDATE_QUERY(sessionParam, PG_VERSION_92, pgPathParam), \
+ HRNPQ_MACRO_SET_APPLICATION_NAME(sessionParam), \
+ HRNPQ_MACRO_IS_STANDBY_QUERY(sessionParam, standbyParam)
+
/***********************************************************************************************************************************
Data type constants
***********************************************************************************************************************************/
diff --git a/test/src/module/db/dbTest.c b/test/src/module/db/dbTest.c
new file mode 100644
index 000000000..f32c304a4
--- /dev/null
+++ b/test/src/module/db/dbTest.c
@@ -0,0 +1,279 @@
+/***********************************************************************************************************************************
+Test Database
+***********************************************************************************************************************************/
+#include "common/harnessConfig.h"
+#include "common/harnessFork.h"
+#include "common/harnessLog.h"
+#include "common/harnessPq.h"
+
+#include "common/io/handleRead.h"
+#include "common/io/handleWrite.h"
+
+/***********************************************************************************************************************************
+Test Run
+***********************************************************************************************************************************/
+void
+testRun(void)
+{
+ FUNCTION_HARNESS_VOID();
+
+ // *****************************************************************************************************************************
+ if (testBegin("Db and dbProtocol()"))
+ {
+ HARNESS_FORK_BEGIN()
+ {
+ HARNESS_FORK_CHILD_BEGIN(0, true)
+ {
+ IoRead *read = ioHandleReadNew(strNew("client read"), HARNESS_FORK_CHILD_READ(), 2000);
+ ioReadOpen(read);
+ IoWrite *write = ioHandleWriteNew(strNew("client write"), HARNESS_FORK_CHILD_WRITE());
+ ioWriteOpen(write);
+
+ // Set options
+ StringList *argList = strLstNew();
+ strLstAddZ(argList, "pgbackrest");
+ strLstAddZ(argList, "--stanza=test1");
+ strLstAddZ(argList, "--pg1-path=/path/to/pg");
+ strLstAddZ(argList, "--command=backup");
+ strLstAddZ(argList, "--type=db");
+ strLstAddZ(argList, "--process=0");
+ strLstAddZ(argList, "remote");
+ harnessCfgLoad(strLstSize(argList), strLstPtr(argList));
+
+ // Set script
+ harnessPqScriptSet((HarnessPq [])
+ {
+ HRNPQ_MACRO_OPEN(1, "dbname='postgres' port=5432"),
+ HRNPQ_MACRO_SET_SEARCH_PATH(1),
+ HRNPQ_MACRO_VALIDATE_QUERY(1, PG_VERSION_84, "/pgdata"),
+ HRNPQ_MACRO_CLOSE(1),
+
+ HRNPQ_MACRO_OPEN(1, "dbname='postgres' port=5432"),
+ HRNPQ_MACRO_SET_SEARCH_PATH(1),
+ HRNPQ_MACRO_VALIDATE_QUERY(1, PG_VERSION_84, "/pgdata"),
+ HRNPQ_MACRO_WAL_SWITCH(1, "xlog", "000000030000000200000003"),
+ HRNPQ_MACRO_CLOSE(1),
+
+ HRNPQ_MACRO_DONE()
+ });
+
+ // Create server
+ ProtocolServer *server = NULL;
+
+ TEST_ASSIGN(server, protocolServerNew(strNew("db test server"), strNew("test"), read, write), "create server");
+ TEST_RESULT_VOID(protocolServerHandlerAdd(server, dbProtocol), "add handler");
+ TEST_RESULT_VOID(protocolServerProcess(server), "run process loop");
+ TEST_RESULT_VOID(protocolServerFree(server), "free server");
+ }
+ HARNESS_FORK_CHILD_END();
+
+ HARNESS_FORK_PARENT_BEGIN()
+ {
+ IoRead *read = ioHandleReadNew(strNew("server read"), HARNESS_FORK_PARENT_READ_PROCESS(0), 2000);
+ ioReadOpen(read);
+ IoWrite *write = ioHandleWriteNew(strNew("server write"), HARNESS_FORK_PARENT_WRITE_PROCESS(0));
+ ioWriteOpen(write);
+
+ // Create client
+ ProtocolClient *client = NULL;
+ Db *db = NULL;
+
+ TEST_ASSIGN(client, protocolClientNew(strNew("db test client"), strNew("test"), read, write), "create client");
+
+ // Open and free database
+ TEST_ASSIGN(db, dbNew(NULL, client, strNew("test")), "create db");
+ TEST_RESULT_VOID(dbOpen(db), "open db");
+ TEST_RESULT_VOID(dbFree(db), "free db");
+
+ // Open the database, but don't free it so the server is force to do it on shutdown
+ TEST_ASSIGN(db, dbNew(NULL, client, strNew("test")), "create db");
+ TEST_RESULT_VOID(dbOpen(db), "open db");
+ TEST_RESULT_STR(strPtr(dbWalSwitch(db)), "000000030000000200000003", " wal switch");
+ TEST_RESULT_VOID(memContextCallbackClear(db->memContext), "clear context so close is not called");
+
+ TEST_RESULT_VOID(protocolClientFree(client), "free client");
+ }
+ HARNESS_FORK_PARENT_END();
+ }
+ HARNESS_FORK_END();
+ }
+
+ // *****************************************************************************************************************************
+ if (testBegin("dbGet()"))
+ {
+ DbGetResult result = {0};
+
+ // Error connecting to primary
+ // -------------------------------------------------------------------------------------------------------------------------
+ StringList *argList = strLstNew();
+ strLstAddZ(argList, "pgbackrest");
+ strLstAddZ(argList, "--stanza=test1");
+ strLstAddZ(argList, "--repo1-retention-full=1");
+ strLstAddZ(argList, "--pg1-path=/path/to/pg");
+ strLstAddZ(argList, "backup");
+ harnessCfgLoad(strLstSize(argList), strLstPtr(argList));
+
+ harnessPqScriptSet((HarnessPq [])
+ {
+ {.function = HRNPQ_CONNECTDB, .param = "[\"dbname='postgres' port=5432\"]"},
+ {.function = HRNPQ_STATUS, .resultInt = CONNECTION_BAD},
+ {.function = HRNPQ_ERRORMESSAGE, .resultZ = "error"},
+ {.function = HRNPQ_FINISH},
+ {.function = NULL}
+ });
+
+ TEST_ERROR(dbGet(true, true), DbConnectError, "unable to find primary cluster - cannot proceed");
+ harnessLogResult(
+ "P00 WARN: unable to check pg-1: [DbConnectError] unable to connect to 'dbname='postgres' port=5432': error");
+
+ // Only cluster is a standby
+ // -------------------------------------------------------------------------------------------------------------------------
+ harnessPqScriptSet((HarnessPq [])
+ {
+ HRNPQ_MACRO_OPEN(1, "dbname='postgres' port=5432"),
+ HRNPQ_MACRO_SET_SEARCH_PATH(1),
+ HRNPQ_MACRO_VALIDATE_QUERY(1, PG_VERSION_94, "/pgdata"),
+ HRNPQ_MACRO_SET_APPLICATION_NAME(1),
+ HRNPQ_MACRO_IS_STANDBY_QUERY(1, true),
+ HRNPQ_MACRO_CLOSE(1),
+ HRNPQ_MACRO_DONE()
+ });
+
+ TEST_ERROR(dbGet(true, true), DbConnectError, "unable to find primary cluster - cannot proceed");
+
+ // Primary cluster found
+ // -------------------------------------------------------------------------------------------------------------------------
+ harnessPqScriptSet((HarnessPq [])
+ {
+ HRNPQ_MACRO_OPEN_84(1, "dbname='postgres' port=5432", "/pgdata"),
+ HRNPQ_MACRO_CLOSE(1),
+ HRNPQ_MACRO_DONE()
+ });
+
+ TEST_ASSIGN(result, dbGet(true, true), "get primary only");
+
+ TEST_RESULT_INT(result.primaryId, 1, " check primary id");
+ TEST_RESULT_BOOL(result.primary != NULL, true, " check primary");
+ TEST_RESULT_INT(result.standbyId, 0, " check standby id");
+ TEST_RESULT_BOOL(result.standby == NULL, true, " check standby");
+
+ TEST_RESULT_VOID(dbFree(result.primary), "free primary");
+
+ // More than one primary found
+ // -------------------------------------------------------------------------------------------------------------------------
+ argList = strLstNew();
+ strLstAddZ(argList, "pgbackrest");
+ strLstAddZ(argList, "--stanza=test1");
+ strLstAddZ(argList, "--repo1-retention-full=1");
+ strLstAddZ(argList, "--pg1-path=/path/to/pg1");
+ strLstAddZ(argList, "--pg8-path=/path/to/pg2");
+ strLstAddZ(argList, "--pg8-port=5433");
+ strLstAddZ(argList, "backup");
+ harnessCfgLoad(strLstSize(argList), strLstPtr(argList));
+
+ harnessPqScriptSet((HarnessPq [])
+ {
+ HRNPQ_MACRO_OPEN_84(1, "dbname='postgres' port=5432", "/pgdata"),
+ HRNPQ_MACRO_OPEN_84(8, "dbname='postgres' port=5433", "/pgdata"),
+
+ HRNPQ_MACRO_CLOSE(1),
+ HRNPQ_MACRO_CLOSE(8),
+
+ HRNPQ_MACRO_DONE()
+ });
+
+ TEST_ERROR(dbGet(true, true), DbConnectError, "more than one primary cluster found");
+
+ // Two standbys found but no primary
+ // -------------------------------------------------------------------------------------------------------------------------
+ harnessPqScriptSet((HarnessPq [])
+ {
+ HRNPQ_MACRO_OPEN_92(1, "dbname='postgres' port=5432", "/pgdata", true),
+ HRNPQ_MACRO_OPEN_92(8, "dbname='postgres' port=5433", "/pgdata", true),
+
+ HRNPQ_MACRO_CLOSE(8),
+ HRNPQ_MACRO_CLOSE(1),
+
+ HRNPQ_MACRO_DONE()
+ });
+
+ TEST_ERROR(dbGet(false, true), DbConnectError, "unable to find primary cluster - cannot proceed");
+
+ // Two standbys and primary not required
+ // -------------------------------------------------------------------------------------------------------------------------
+ harnessPqScriptSet((HarnessPq [])
+ {
+ HRNPQ_MACRO_OPEN_92(1, "dbname='postgres' port=5432", "/pgdata", true),
+ HRNPQ_MACRO_OPEN_92(8, "dbname='postgres' port=5433", "/pgdata", true),
+
+ HRNPQ_MACRO_CLOSE(8),
+ HRNPQ_MACRO_CLOSE(1),
+
+ HRNPQ_MACRO_DONE()
+ });
+
+ TEST_ASSIGN(result, dbGet(false, false), "get standbys");
+
+ TEST_RESULT_INT(result.primaryId, 0, " check primary id");
+ TEST_RESULT_BOOL(result.primary == NULL, true, " check primary");
+ TEST_RESULT_INT(result.standbyId, 1, " check standby id");
+ TEST_RESULT_BOOL(result.standby != NULL, true, " check standby");
+
+ TEST_RESULT_VOID(dbFree(result.standby), "free standby");
+
+ // Primary and standby found
+ // -------------------------------------------------------------------------------------------------------------------------
+ argList = strLstNew();
+ strLstAddZ(argList, "pgbackrest");
+ strLstAddZ(argList, "--stanza=test1");
+ strLstAddZ(argList, "--repo1-retention-full=1");
+ strLstAddZ(argList, "--pg1-path=/path/to/pg1");
+ strLstAddZ(argList, "--pg4-path=/path/to/pg4");
+ strLstAddZ(argList, "--pg4-port=5433");
+ strLstAddZ(argList, "--pg5-host=localhost");
+ strLstAdd(argList, strNewFmt("--pg5-host-user=%s", testUser()));
+ strLstAddZ(argList, "--pg5-path=/path/to/pg5");
+ strLstAddZ(argList, "--pg8-path=/path/to/pg8");
+ strLstAddZ(argList, "--pg8-port=5434");
+ strLstAddZ(argList, "backup");
+ harnessCfgLoad(strLstSize(argList), strLstPtr(argList));
+
+ harnessPqScriptSet((HarnessPq [])
+ {
+ HRNPQ_MACRO_OPEN_92(1, "dbname='postgres' port=5432", "/pgdata", true),
+
+ // pg-4 error
+ {.session = 4, .function = HRNPQ_CONNECTDB, .param = "[\"dbname='postgres' port=5433\"]"},
+ {.session = 4, .function = HRNPQ_STATUS, .resultInt = CONNECTION_BAD},
+ {.session = 4, .function = HRNPQ_ERRORMESSAGE, .resultZ = "error"},
+ {.session = 4, .function = HRNPQ_FINISH},
+
+ HRNPQ_MACRO_OPEN_92(8, "dbname='postgres' port=5434", "/pgdata", false),
+
+ HRNPQ_MACRO_CREATE_RESTORE_POINT(8, "2/3"),
+ HRNPQ_MACRO_WAL_SWITCH(8, "xlog", "000000010000000200000003"),
+
+ HRNPQ_MACRO_CLOSE(8),
+ HRNPQ_MACRO_CLOSE(1),
+
+ HRNPQ_MACRO_DONE()
+ });
+
+ TEST_ASSIGN(result, dbGet(false, true), "get primary and standy");
+ harnessLogResultRegExp(
+ "P00 WARN: unable to check pg-4: \\[DbConnectError\\] unable to connect to 'dbname='postgres' port=5433': error\n"
+ "P00 WARN: unable to check pg-5: \\[DbConnectError\\] raised from remote-0 protocol on 'localhost':"
+ " unable to connect to 'dbname='postgres' port=5432': could not connect to server: No such file or directory.*");
+
+ TEST_RESULT_INT(result.primaryId, 8, " check primary id");
+ TEST_RESULT_BOOL(result.primary != NULL, true, " check primary");
+ TEST_RESULT_STR(strPtr(dbWalSwitch(result.primary)), "000000010000000200000003", " wal switch");
+ TEST_RESULT_INT(result.standbyId, 1, " check standby id");
+ TEST_RESULT_BOOL(result.standby != NULL, true, " check standby");
+
+ TEST_RESULT_VOID(dbFree(result.primary), "free primary");
+ TEST_RESULT_VOID(dbFree(result.standby), "free standby");
+ }
+
+ FUNCTION_HARNESS_RESULT_VOID();
+}
diff --git a/test/src/module/postgres/clientTest.c b/test/src/module/postgres/clientTest.c
index 9c1bf3b03..5c645af37 100644
--- a/test/src/module/postgres/clientTest.c
+++ b/test/src/module/postgres/clientTest.c
@@ -53,7 +53,15 @@ testRun(void)
#endif
PgClient *client = NULL;
- TEST_ASSIGN(client, pgClientNew(NULL, 5433, strNew("postg '\\res"), NULL, 3000), "new client");
+
+ MEM_CONTEXT_TEMP_BEGIN()
+ {
+ TEST_ASSIGN(client, pgClientNew(NULL, 5433, strNew("postg '\\res"), NULL, 3000), "new client");
+ TEST_RESULT_VOID(pgClientMove(client, MEM_CONTEXT_OLD()), "move client");
+ TEST_RESULT_VOID(pgClientMove(NULL, MEM_CONTEXT_OLD()), "move null client");
+ }
+ MEM_CONTEXT_TEMP_END();
+
TEST_ERROR(
pgClientOpen(client), DbConnectError,
"unable to connect to 'dbname='postg \\'\\\\res' port=5433': could not connect to server: No such file or directory\n"
@@ -284,6 +292,7 @@ testRun(void)
});
#endif
TEST_RESULT_VOID(pgClientClose(client), "close client");
+ TEST_RESULT_VOID(pgClientClose(client), "close client again");
}
FUNCTION_HARNESS_RESULT_VOID();
diff --git a/test/src/module/postgres/interfaceTest.c b/test/src/module/postgres/interfaceTest.c
index 03790b0c7..57a167012 100644
--- a/test/src/module/postgres/interfaceTest.c
+++ b/test/src/module/postgres/interfaceTest.c
@@ -94,6 +94,13 @@ testRun(void)
TEST_RESULT_INT(info.version, PG_VERSION_83, " check version");
}
+ // *****************************************************************************************************************************
+ if (testBegin("pgWalName()"))
+ {
+ TEST_RESULT_STR(strPtr(pgWalName(PG_VERSION_96)), "xlog", "check xlog name");
+ TEST_RESULT_STR(strPtr(pgWalName(PG_VERSION_10)), "wal", "check wal name");
+ }
+
// *****************************************************************************************************************************
if (testBegin("pgWalFromBuffer() and pgWalFromFile()"))
{
@@ -157,7 +164,6 @@ testRun(void)
"{version: 110000, systemId: 1030522662895, walSegmentSize: 16777216, pageChecksum: true}", "check log");
}
-
// *****************************************************************************************************************************
if (testBegin("pgWalToLog()"))
{