1
0
mirror of https://github.com/pgbackrest/pgbackrest.git synced 2025-10-30 23:37:45 +02:00

Add PostgreSQL query client.

This direct interface to libpq allows simple queries to be run against PostgreSQL and supports timeouts.

Testing is performed using a shim that can use scripted responses to test all aspects of the client code.  The shim will be very useful for testing backup scenarios on complex topologies.

Reviewed by Cynthia Shang.
This commit is contained in:
David Steele
2019-07-25 14:50:02 -04:00
parent 59f135340d
commit 415542b4a3
15 changed files with 1160 additions and 8 deletions

View File

@@ -52,6 +52,14 @@
<p>Add Perl interface to C storage layer.</p>
</release-item>
<release-item>
<release-item-contributor-list>
<release-item-reviewer id="cynthia.shang"/>
</release-item-contributor-list>
<p>Add PostgreSQL query client.</p>
</release-item>
</release-development-list>
</release-core-list>
</release>

View File

@@ -758,14 +758,16 @@
</execute>
<execute if="{[os-type-is-debian]}" user="root" pre="y">
<exe-cmd>apt-get install build-essential libssl-dev libxml2-dev libperl-dev zlib1g-dev</exe-cmd>
<exe-cmd>
apt-get install build-essential libssl-dev libxml2-dev libperl-dev zlib1g-dev
libpq-dev</exe-cmd>
<exe-cmd-extra>-y 2>&amp;1</exe-cmd-extra>
</execute>
<execute if="{[os-type-is-centos6]}" user="root" pre="y">
<exe-cmd>
yum install build-essential gcc openssl-devel libxml2-devel
perl-ExtUtils-Embed
postgresql-devel perl-ExtUtils-Embed
</exe-cmd>
<exe-cmd-extra>-y 2>&amp;1</exe-cmd-extra>
</execute>
@@ -773,7 +775,7 @@
<execute if="{[os-type-is-centos7]}" user="root" pre="y">
<exe-cmd>
yum install build-essential gcc make openssl-devel libxml2-devel
perl-ExtUtils-Embed
postgresql-devel perl-ExtUtils-Embed
</exe-cmd>
<exe-cmd-extra>-y 2>&amp;1</exe-cmd-extra>
</execute>

View File

@@ -127,6 +127,7 @@ SRCS = \
info/infoPg.c \
perl/config.c \
perl/exec.c \
postgres/client.c \
postgres/interface.c \
postgres/interface/v083.c \
postgres/interface/v084.c \
@@ -449,6 +450,9 @@ perl/config.o: perl/config.c build.auto.h common/assert.h common/debug.h common/
perl/exec.o: perl/exec.c ../libc/LibC.h build.auto.h common/assert.h common/compress/gzip/compress.h common/compress/gzip/decompress.h common/crypto/cipherBlock.h common/crypto/common.h common/crypto/hash.h common/debug.h common/encode.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/http/client.h common/io/http/header.h common/io/http/query.h common/io/io.h common/io/read.h common/io/read.intern.h common/io/write.h common/io/write.intern.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/json.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/load.h config/parse.h perl/config.h perl/embed.auto.c perl/exec.h perl/libc.auto.c postgres/interface.h postgres/pageChecksum.h storage/helper.h storage/info.h storage/posix/storage.h storage/read.h storage/read.intern.h storage/s3/storage.h storage/s3/storage.intern.h storage/storage.h storage/storage.intern.h storage/write.h storage/write.intern.h version.h ../libc/xs/common/encode.xsh ../libc/xs/crypto/hash.xsh ../libc/xs/storage/storage.xsh ../libc/xs/storage/storageRead.xsh ../libc/xs/storage/storageWrite.xsh
$(CC) $(CPPFLAGS) $(CFLAGS) $(CMAKE) -c perl/exec.c -o perl/exec.o
postgres/client.o: postgres/client.c build.auto.h common/assert.h common/debug.h common/error.auto.h common/error.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/list.h common/type/string.h common/type/stringList.h common/type/variant.h common/type/variantList.h common/wait.h postgres/client.h
$(CC) $(CPPFLAGS) $(CFLAGS) $(CMAKE) -c postgres/client.c -o postgres/client.o
postgres/interface.o: postgres/interface.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/memContext.h common/regExp.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 postgres/interface.h postgres/interface/version.h postgres/version.h storage/helper.h storage/info.h storage/read.h storage/storage.h storage/write.h
$(CC) $(CPPFLAGS) $(CFLAGS) $(CMAKE) -c postgres/interface.c -o postgres/interface.o

51
src/configure vendored
View File

@@ -2861,6 +2861,57 @@ LIBS="$LIBS_BEFORE_PERL `perl -MExtUtils::Embed -e ldopts`"
CLIBRARY="`perl -MExtUtils::Embed -e ccopts`"
# Check required pq library
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for PQconnectdb in -lpq" >&5
$as_echo_n "checking for PQconnectdb in -lpq... " >&6; }
if ${ac_cv_lib_pq_PQconnectdb+:} false; then :
$as_echo_n "(cached) " >&6
else
ac_check_lib_save_LIBS=$LIBS
LIBS="-lpq $LIBS"
cat confdefs.h - <<_ACEOF >conftest.$ac_ext
/* end confdefs.h. */
/* Override any GCC internal prototype to avoid an error.
Use char because int might match the return type of a GCC
builtin and then its argument prototype would still apply. */
#ifdef __cplusplus
extern "C"
#endif
char PQconnectdb ();
int
main ()
{
return PQconnectdb ();
;
return 0;
}
_ACEOF
if ac_fn_c_try_link "$LINENO"; then :
ac_cv_lib_pq_PQconnectdb=yes
else
ac_cv_lib_pq_PQconnectdb=no
fi
rm -f core conftest.err conftest.$ac_objext \
conftest$ac_exeext conftest.$ac_ext
LIBS=$ac_check_lib_save_LIBS
fi
{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_pq_PQconnectdb" >&5
$as_echo "$ac_cv_lib_pq_PQconnectdb" >&6; }
if test "x$ac_cv_lib_pq_PQconnectdb" = xyes; then :
cat >>confdefs.h <<_ACEOF
#define HAVE_LIBPQ 1
_ACEOF
LIBS="-lpq $LIBS"
else
as_fn_error $? "library 'pq' is required" "$LINENO" 5
fi
CINCLUDE="$CINCLUDE -I`pg_config --includedir`"
# Check required openssl libraries
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for EVP_get_digestbyname in -lcrypto" >&5
$as_echo_n "checking for EVP_get_digestbyname in -lcrypto... " >&6; }

View File

@@ -39,6 +39,10 @@ AC_CHECK_LIB([perl], [perl_parse], [], [AC_MSG_ERROR([library 'perl' is required
LIBS="$LIBS_BEFORE_PERL `perl -MExtUtils::Embed -e ldopts`"
AC_SUBST(CLIBRARY, "`perl -MExtUtils::Embed -e ccopts`")
# Check required pq library
AC_CHECK_LIB([pq], [PQconnectdb], [], [AC_MSG_ERROR([library 'pq' is required])])
AC_SUBST(CINCLUDE, "$CINCLUDE -I`pg_config --includedir`")
# Check required openssl libraries
AC_CHECK_LIB([crypto], [EVP_get_digestbyname], [], [AC_MSG_ERROR([library 'crypto' is required])])
AC_CHECK_LIB([ssl], [SSL_new], [], [AC_MSG_ERROR([library 'ssl' is required])])

371
src/postgres/client.c Normal file
View File

@@ -0,0 +1,371 @@
/***********************************************************************************************************************************
Postgres Client
***********************************************************************************************************************************/
#include "build.auto.h"
#include <libpq-fe.h>
#include "common/debug.h"
#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"
/***********************************************************************************************************************************
Object type
***********************************************************************************************************************************/
struct PgClient
{
MemContext *memContext;
const String *host;
unsigned int port;
const String *database;
const String *user;
TimeMSec queryTimeout;
PGconn *connection;
};
OBJECT_DEFINE_FREE(PG_CLIENT);
/***********************************************************************************************************************************
Close protocol connection
***********************************************************************************************************************************/
OBJECT_DEFINE_FREE_RESOURCE_BEGIN(PG_CLIENT, LOG, logLevelTrace)
{
PQfinish(this->connection);
}
OBJECT_DEFINE_FREE_RESOURCE_END(LOG);
/***********************************************************************************************************************************
Create object
***********************************************************************************************************************************/
PgClient *
pgClientNew(const String *host, const unsigned int port, const String *database, const String *user, const TimeMSec queryTimeout)
{
FUNCTION_LOG_BEGIN(logLevelDebug);
FUNCTION_LOG_PARAM(STRING, host);
FUNCTION_LOG_PARAM(UINT, port);
FUNCTION_LOG_PARAM(STRING, database);
FUNCTION_LOG_PARAM(STRING, user);
FUNCTION_LOG_PARAM(TIME_MSEC, queryTimeout);
FUNCTION_LOG_END();
ASSERT(port >= 1 && port <= 65535);
ASSERT(database != NULL);
PgClient *this = NULL;
MEM_CONTEXT_NEW_BEGIN("PgClient")
{
this = memNew(sizeof(PgClient));
this->memContext = memContextCurrent();
this->host = strDup(host);
this->port = port;
this->database = strDup(database);
this->user = strDup(user);
this->queryTimeout = queryTimeout;
}
MEM_CONTEXT_NEW_END();
FUNCTION_LOG_RETURN(PG_CLIENT, this);
}
/***********************************************************************************************************************************
Just ignore notices and warnings
***********************************************************************************************************************************/
static void
pgClientNoticeProcessor(void *arg, const char *message)
{
(void)arg;
(void)message;
}
/***********************************************************************************************************************************
Encode string to escape ' and \
***********************************************************************************************************************************/
static String *
pgClientEscape(const String *string)
{
FUNCTION_TEST_BEGIN();
FUNCTION_TEST_PARAM(STRING, string);
FUNCTION_TEST_END();
ASSERT(string != NULL);
String *result = strNew("'");
// Iterate all characters in the string
for (unsigned stringIdx = 0; stringIdx < strSize(string); stringIdx++)
{
char stringChar = strPtr(string)[stringIdx];
// These characters are escaped
if (stringChar == '\'' || stringChar == '\\')
strCatChr(result, '\\');
strCatChr(result, stringChar);
}
strCatChr(result, '\'');
FUNCTION_TEST_RETURN(result);
}
/***********************************************************************************************************************************
Open connection to PostgreSQL
***********************************************************************************************************************************/
PgClient *
pgClientOpen(PgClient *this)
{
FUNCTION_LOG_BEGIN(logLevelDebug);
FUNCTION_LOG_PARAM(PG_CLIENT, this);
FUNCTION_LOG_END();
ASSERT(this != NULL);
CHECK(this->connection == NULL);
MEM_CONTEXT_TEMP_BEGIN()
{
// Base connection string
String *connInfo = strNewFmt("dbname=%s port=%u", strPtr(pgClientEscape(this->database)), this->port);
// Add user if specified
if (this->user != NULL)
strCatFmt(connInfo, " user=%s", strPtr(pgClientEscape(this->user)));
// Add host if specified
if (this->host != NULL)
strCatFmt(connInfo, " host=%s", strPtr(pgClientEscape(this->host)));
// Make the connection
this->connection = PQconnectdb(strPtr(connInfo));
// Set a callback to shutdown the connection
memContextCallbackSet(this->memContext, pgClientFreeResource, this);
// Handle errors
if (PQstatus(this->connection) != CONNECTION_OK)
{
THROW_FMT(
DbConnectError, "unable to connect to '%s': %s", strPtr(connInfo),
strPtr(strTrim(strNew(PQerrorMessage(this->connection)))));
}
// Set notice and warning processor
PQsetNoticeProcessor(this->connection, pgClientNoticeProcessor, NULL);
}
MEM_CONTEXT_TEMP_END();
FUNCTION_LOG_RETURN(PG_CLIENT, this);
}
/***********************************************************************************************************************************
Execute a query and return results
***********************************************************************************************************************************/
VariantList *
pgClientQuery(PgClient *this, const String *query)
{
FUNCTION_LOG_BEGIN(logLevelDebug);
FUNCTION_LOG_PARAM(PG_CLIENT, this);
FUNCTION_LOG_PARAM(STRING, query);
FUNCTION_LOG_END();
ASSERT(this != NULL);
CHECK(this->connection != NULL);
ASSERT(query != NULL);
VariantList *result = NULL;
MEM_CONTEXT_TEMP_BEGIN()
{
// Send the query without waiting for results so we can timeout if needed
if (!PQsendQuery(this->connection, strPtr(query)))
{
THROW_FMT(
DbQueryError, "unable to send query '%s': %s", strPtr(query),
strPtr(strTrim(strNew(PQerrorMessage(this->connection)))));
}
// Wait for a result
Wait *wait = waitNew(this->queryTimeout);
bool busy = false;
do
{
PQconsumeInput(this->connection);
busy = PQisBusy(this->connection);
}
while (busy && waitMore(wait));
// If the query is still busy after the timeout attempt to cancel
if (busy)
{
PGcancel *cancel = PQgetCancel(this->connection);
CHECK(cancel != NULL);
TRY_BEGIN()
{
char error[256];
if (!PQcancel(cancel, error, sizeof(error)))
THROW_FMT(DbQueryError, "unable to cancel query '%s': %s", strPtr(query), strPtr(strTrim(strNew(error))));
}
FINALLY()
{
PQfreeCancel(cancel);
}
TRY_END();
}
// Get the result (even if query was cancelled -- to prevent the connection being left in a bad state)
PGresult *pgResult = PQgetResult(this->connection);
TRY_BEGIN()
{
// Throw timeout error if cancelled
if (busy)
THROW_FMT(DbQueryError, "query '%s' timed out after %" PRIu64 "ms", strPtr(query), this->queryTimeout);
// If this was a command that returned no results then we are done
int resultStatus = PQresultStatus(pgResult);
if (resultStatus != PGRES_COMMAND_OK)
{
// Expect some rows to be returned
if (resultStatus != PGRES_TUPLES_OK)
{
THROW_FMT(
DbQueryError, "unable to execute query '%s': %s", strPtr(query),
strPtr(strTrim(strNew(PQresultErrorMessage(pgResult)))));
}
// Fetch row and column values
result = varLstNew();
MEM_CONTEXT_BEGIN(lstMemContext((List *)result))
{
int rowTotal = PQntuples(pgResult);
int columnTotal = PQnfields(pgResult);
// Get column types
Oid *columnType = memNew(sizeof(int) * (size_t)columnTotal);
for (int columnIdx = 0; columnIdx < columnTotal; columnIdx++)
columnType[columnIdx] = PQftype(pgResult, columnIdx);
// Get values
for (int rowIdx = 0; rowIdx < rowTotal; rowIdx++)
{
VariantList *resultRow = varLstNew();
for (int columnIdx = 0; columnIdx < columnTotal; columnIdx++)
{
char *value = PQgetvalue(pgResult, rowIdx, columnIdx);
// If value is zero-length then check if it is null
if (value[0] == '\0' && PQgetisnull(pgResult, rowIdx, columnIdx))
{
varLstAdd(resultRow, NULL);
}
// Else convert the value to a variant
else
{
// Convert column type. Not all PostgreSQL types are supported but these should suffice.
switch (columnType[columnIdx])
{
// Boolean type
case 16: // bool
{
varLstAdd(resultRow, varNewBool(varBoolForce(varNewStrZ(value))));
break;
}
// Text/char types
case 18: // char
case 19: // name
case 25: // text
{
varLstAdd(resultRow, varNewStrZ(value));
break;
}
// Integer types
case 20: // int8
case 21: // int2
case 23: // int4
case 26: // oid
{
varLstAdd(resultRow, varNewInt64(cvtZToInt64(value)));
break;
}
default:
{
THROW_FMT(
FormatError, "unable to parse type %u in column %d for query '%s'",
columnType[columnIdx], columnIdx, strPtr(query));
}
}
}
}
varLstAdd(result, varNewVarLst(resultRow));
}
}
MEM_CONTEXT_END();
}
}
FINALLY()
{
// Free the result
PQclear(pgResult);
// Need to get a NULL result to complete the request
CHECK(PQgetResult(this->connection) == NULL);
}
TRY_END();
varLstMove(result, MEM_CONTEXT_OLD());
}
MEM_CONTEXT_TEMP_END();
FUNCTION_LOG_RETURN(VARIANT_LIST, result);
}
/***********************************************************************************************************************************
Close connection to PostgreSQL
***********************************************************************************************************************************/
void
pgClientClose(PgClient *this)
{
FUNCTION_LOG_BEGIN(logLevelDebug);
FUNCTION_LOG_PARAM(PG_CLIENT, this);
FUNCTION_LOG_END();
ASSERT(this != NULL);
CHECK(this->connection != NULL);
memContextCallbackClear(this->memContext);
PQfinish(this->connection);
this->connection = NULL;
FUNCTION_LOG_RETURN_VOID();
}
/***********************************************************************************************************************************
Render as string for logging
***********************************************************************************************************************************/
String *
pgClientToLog(const PgClient *this)
{
return strNewFmt(
"{host: %s, port: %u, database: %s, user: %s, queryTimeout %" PRIu64 "}", strPtr(strToLog(this->host)), this->port,
strPtr(strToLog(this->database)), strPtr(strToLog(this->user)), this->queryTimeout);
}

47
src/postgres/client.h Normal file
View File

@@ -0,0 +1,47 @@
/***********************************************************************************************************************************
PostgreSQL Client
Connect to a PostgreSQL database and run queries. This is not intended to be a general purpose client but is suitable for
pgBackRest's limited needs. In particular, data type support is limited to text, int, and bool types so it may be necessary to add
casts to queries to output one of these types.
***********************************************************************************************************************************/
#ifndef POSTGRES_QUERY_H
#define POSTGRES_QUERY_H
/***********************************************************************************************************************************
Object type
***********************************************************************************************************************************/
#define PG_CLIENT_TYPE PgClient
#define PG_CLIENT_PREFIX pgClient
typedef struct PgClient PgClient;
/***********************************************************************************************************************************
Constructor
***********************************************************************************************************************************/
PgClient *pgClientNew(
const String *host, const unsigned int port, const String *database, const String *user, const TimeMSec queryTimeout);
/***********************************************************************************************************************************
Functions
***********************************************************************************************************************************/
PgClient *pgClientOpen(PgClient *this);
VariantList *pgClientQuery(PgClient *this, const String *query);
void pgClientClose(PgClient *this);
/***********************************************************************************************************************************
Destructor
***********************************************************************************************************************************/
void pgClientFree(PgClient *this);
/***********************************************************************************************************************************
Macros for function logging
***********************************************************************************************************************************/
String *pgClientToLog(const PgClient *this);
#define FUNCTION_LOG_PG_CLIENT_TYPE \
PgClient *
#define FUNCTION_LOG_PG_CLIENT_FORMAT(value, buffer, bufferSize) \
FUNCTION_LOG_STRING_OBJECT_FORMAT(value, pgClientToLog, buffer, bufferSize)
#endif

2
test/Vagrantfile vendored
View File

@@ -60,7 +60,7 @@ Vagrant.configure(2) do |config|
#---------------------------------------------------------------------------------------------------------------------------
echo 'Install Build Tools' && date
apt-get install -y devscripts build-essential lintian git lcov cloc txt2man debhelper libssl-dev zlib1g-dev libperl-dev \
libxml2-dev liblz4-dev
libxml2-dev liblz4-dev libpq-dev
#---------------------------------------------------------------------------------------------------------------------------
echo 'Install AWS CLI' && date

View File

@@ -315,6 +315,13 @@ unit:
- name: postgres
test:
# ----------------------------------------------------------------------------------------------------------------------------
- name: client
total: 1
coverage:
postgres/client: full
# ----------------------------------------------------------------------------------------------------------------------------
- name: interface
total: 5

View File

@@ -483,7 +483,7 @@ sub containerBuild
" http://yum.postgresql.org/9.1/redhat/rhel-6-x86_64/pgdg-centos91-9.1-6.noarch.rpm \\\n" .
" http://yum.postgresql.org/9.2/redhat/rhel-6-x86_64/pgdg-centos92-9.2-8.noarch.rpm \\\n" .
" https://download.postgresql.org/pub/repos/yum/11/redhat/rhel-6-x86_64/" .
"pgdg-redhat-repo-latest.noarch.rpm";
"pgdg-redhat-repo-latest.noarch.rpm && \\\n";
}
elsif ($strOS eq VM_CO7)
{
@@ -491,8 +491,10 @@ sub containerBuild
" rpm -ivh \\\n" .
" http://yum.postgresql.org/9.2/redhat/rhel-7-x86_64/pgdg-centos92-9.2-3.noarch.rpm \\\n" .
" https://download.postgresql.org/pub/repos/yum/11/redhat/rhel-7-x86_64/" .
"pgdg-redhat-repo-latest.noarch.rpm";
"pgdg-redhat-repo-latest.noarch.rpm && \\\n";
}
$strScript .= " yum -y install postgresql-devel";
}
else
{
@@ -502,7 +504,7 @@ sub containerBuild
"' >> /etc/apt/sources.list.d/pgdg.list && \\\n" .
" wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \\\n" .
" apt-get update && \\\n" .
" apt-get install -y postgresql-common && \\\n" .
" apt-get install -y postgresql-common libpq-dev && \\\n" .
" sed -i 's/^\\#create\\_main\\_cluster.*\$/create\\_main\\_cluster \\= false/' " .
"/etc/postgresql-common/createcluster.conf";
}

View File

@@ -411,6 +411,7 @@ sub run
'-I. -Itest -std=c99 -fPIC -g -Wno-clobbered -D_POSIX_C_SOURCE=200112L' .
' `perl -MExtUtils::Embed -e ccopts`' .
' `xml2-config --cflags`' . ($self->{bProfile} ? " -pg" : '') .
' -I`pg_config --includedir`' .
($self->{oTest}->{&TEST_DEBUG_UNIT_SUPPRESS} ? '' : " -DDEBUG_UNIT") .
(vmWithBackTrace($self->{oTest}->{&TEST_VM}) && $self->{bBackTrace} ? ' -DWITH_BACKTRACE' : '') .
($self->{oTest}->{&TEST_CDEF} ? " $self->{oTest}->{&TEST_CDEF}" : '') .

View File

@@ -1,11 +1,12 @@
--- control
+++ control
@@ -4,11 +4,9 @@
@@ -4,11 +4,10 @@
Maintainer: Debian PostgreSQL Maintainers <team+postgresql@tracker.debian.org>
Uploaders: Adrian Vondendriesch <adrian.vondendriesch@credativ.de>
Build-Depends: debhelper (>= 9),
- libio-socket-ssl-perl,
libperl-dev,
+ libpq-dev,
libssl-dev,
libxml-checker-perl,
- libxml-libxml-perl,

299
test/src/common/harnessPq.c Normal file
View File

@@ -0,0 +1,299 @@
/***********************************************************************************************************************************
Pq Test Harness
***********************************************************************************************************************************/
#ifndef HARNESS_PQ_REAL
#include <string.h>
#include <libpq-fe.h>
#include "common/type/json.h"
#include "common/type/string.h"
#include "common/type/variantList.h"
#include "common/harnessPq.h"
/***********************************************************************************************************************************
Script that defines how shim functions operate
***********************************************************************************************************************************/
HarnessPq *harnessPqScript;
unsigned int harnessPqScriptIdx;
// If there is a script failure change the behavior of cleanup functions to return immediately so the real error will be reported
// rather than a bogus scripting error during cleanup
bool harnessPqScriptFail;
/***********************************************************************************************************************************
Set pq script
***********************************************************************************************************************************/
void
harnessPqScriptSet(HarnessPq *harnessPqScriptParam)
{
if (harnessPqScript != NULL)
THROW(AssertError, "previous pq script has not yet completed");
if (harnessPqScriptParam[0].function == NULL)
THROW(AssertError, "pq script must have entries");
harnessPqScript = harnessPqScriptParam;
harnessPqScriptIdx = 0;
}
/***********************************************************************************************************************************
Run pq script
***********************************************************************************************************************************/
static HarnessPq *
harnessPqScriptRun(const char *function, const VariantList *param, HarnessPq *parent)
{
// Convert params to json for comparison and reporting
String *paramStr = param ? jsonFromVar(varNewVarLst(param), 0) : strNew("");
// Ensure script has not ended
if (harnessPqScript == NULL)
{
harnessPqScriptFail = true;
THROW_FMT(AssertError, "pq script ended before %s (%s)", function, strPtr(paramStr));
}
// Get current script item
HarnessPq *result = &harnessPqScript[harnessPqScriptIdx];
// Check that expected function was called
if (strcmp(result->function, function) != 0)
{
harnessPqScriptFail = true;
THROW_FMT(
AssertError, "pq script [%u] expected function '%s' but got '%s'", harnessPqScriptIdx, result->function, function);
}
// Check that parameters match
if ((param != NULL && result->param == NULL) || (param == NULL && result->param != NULL) ||
(param != NULL && result->param != NULL && !strEqZ(paramStr, result->param)))
{
harnessPqScriptFail = true;
THROW_FMT(
AssertError, "pq script [%u] function '%s', expects param '%s' but got '%s'", harnessPqScriptIdx, result->function,
result->param ? result->param : "NULL", param ? strPtr(paramStr) : "NULL");
}
// Make sure the session matches with the parent as a sanity check
if (parent != NULL && result->session != parent->session)
{
THROW_FMT(
AssertError, "pq script [%u] function '%s', expects session '%u' but got '%u'", harnessPqScriptIdx, result->function,
result->session, parent->session);
}
// Sleep if requested
if (result->sleep > 0)
sleepMSec(result->sleep);
harnessPqScriptIdx++;
if (harnessPqScript[harnessPqScriptIdx].function == NULL)
harnessPqScript = NULL;
return result;
}
/***********************************************************************************************************************************
Shim for PQconnectdb()
***********************************************************************************************************************************/
PGconn *PQconnectdb(const char *conninfo)
{
return (PGconn *)harnessPqScriptRun(HRNPQ_CONNECTDB, varLstAdd(varLstNew(), varNewStrZ(conninfo)), NULL);
}
/***********************************************************************************************************************************
Shim for PQstatus()
***********************************************************************************************************************************/
ConnStatusType PQstatus(const PGconn *conn)
{
return (ConnStatusType)harnessPqScriptRun(HRNPQ_STATUS, NULL, (HarnessPq *)conn)->resultInt;
}
/***********************************************************************************************************************************
Shim for PQerrorMessage()
***********************************************************************************************************************************/
char *PQerrorMessage(const PGconn *conn)
{
return (char *)harnessPqScriptRun(HRNPQ_ERRORMESSAGE, NULL, (HarnessPq *)conn)->resultZ;
}
/***********************************************************************************************************************************
Shim for PQsetNoticeProcessor()
***********************************************************************************************************************************/
PQnoticeProcessor
PQsetNoticeProcessor(PGconn *conn, PQnoticeProcessor proc, void *arg)
{
(void)conn;
// Call the processor that was passed so we have coverage
proc(arg, "test notice");
return NULL;
}
/***********************************************************************************************************************************
Shim for PQsendQuery()
***********************************************************************************************************************************/
int
PQsendQuery(PGconn *conn, const char *query)
{
return harnessPqScriptRun(HRNPQ_SENDQUERY, varLstAdd(varLstNew(), varNewStrZ(query)), (HarnessPq *)conn)->resultInt;
}
/***********************************************************************************************************************************
Shim for PQconsumeInput()
***********************************************************************************************************************************/
int
PQconsumeInput(PGconn *conn)
{
return harnessPqScriptRun(HRNPQ_CONSUMEINPUT, NULL, (HarnessPq *)conn)->resultInt;
}
/***********************************************************************************************************************************
Shim for PQisBusy()
***********************************************************************************************************************************/
int
PQisBusy(PGconn *conn)
{
return harnessPqScriptRun(HRNPQ_ISBUSY, NULL, (HarnessPq *)conn)->resultInt;
}
/***********************************************************************************************************************************
Shim for PQgetCancel()
***********************************************************************************************************************************/
PGcancel *
PQgetCancel(PGconn *conn)
{
return (PGcancel *)harnessPqScriptRun(HRNPQ_GETCANCEL, NULL, (HarnessPq *)conn);
}
/***********************************************************************************************************************************
Shim for PQcancel()
***********************************************************************************************************************************/
int
PQcancel(PGcancel *cancel, char *errbuf, int errbufsize)
{
HarnessPq *harnessPq = harnessPqScriptRun(HRNPQ_CANCEL, NULL, (HarnessPq *)cancel);
if (!harnessPq->resultInt)
{
strncpy(errbuf, harnessPq->resultZ, (size_t)errbufsize);
errbuf[errbufsize - 1] = '\0';
}
return harnessPq->resultInt;
}
/***********************************************************************************************************************************
Shim for PQfreeCancel()
***********************************************************************************************************************************/
void
PQfreeCancel(PGcancel *cancel)
{
harnessPqScriptRun(HRNPQ_FREECANCEL, NULL, (HarnessPq *)cancel);
}
/***********************************************************************************************************************************
Shim for PQgetResult()
***********************************************************************************************************************************/
PGresult *
PQgetResult(PGconn *conn)
{
if (!harnessPqScriptFail)
{
HarnessPq *harnessPq = harnessPqScriptRun(HRNPQ_GETRESULT, NULL, (HarnessPq *)conn);
return harnessPq->resultNull ? NULL : (PGresult *)harnessPq;
}
return NULL;
}
/***********************************************************************************************************************************
Shim for PQresultStatus()
***********************************************************************************************************************************/
ExecStatusType
PQresultStatus(const PGresult *res)
{
return (ExecStatusType)harnessPqScriptRun(HRNPQ_RESULTSTATUS, NULL, (HarnessPq *)res)->resultInt;
}
/***********************************************************************************************************************************
Shim for PQresultErrorMessage()
***********************************************************************************************************************************/
char *
PQresultErrorMessage(const PGresult *res)
{
return (char *)harnessPqScriptRun(HRNPQ_RESULTERRORMESSAGE, NULL, (HarnessPq *)res)->resultZ;
}
/***********************************************************************************************************************************
Shim for PQntuples()
***********************************************************************************************************************************/
int
PQntuples(const PGresult *res)
{
return harnessPqScriptRun(HRNPQ_NTUPLES, NULL, (HarnessPq *)res)->resultInt;
}
/***********************************************************************************************************************************
Shim for PQnfields()
***********************************************************************************************************************************/
int
PQnfields(const PGresult *res)
{
return harnessPqScriptRun(HRNPQ_NFIELDS, NULL, (HarnessPq *)res)->resultInt;
}
/***********************************************************************************************************************************
Shim for PQgetisnull()
***********************************************************************************************************************************/
int
PQgetisnull(const PGresult *res, int tup_num, int field_num)
{
return harnessPqScriptRun(
HRNPQ_GETISNULL, varLstAdd(varLstAdd(varLstNew(), varNewInt(tup_num)), varNewInt(field_num)), (HarnessPq *)res)->resultInt;
}
/***********************************************************************************************************************************
Shim for PQftype()
***********************************************************************************************************************************/
Oid
PQftype(const PGresult *res, int field_num)
{
return (Oid)harnessPqScriptRun(HRNPQ_FTYPE, varLstAdd(varLstNew(), varNewInt(field_num)), (HarnessPq *)res)->resultInt;
}
/***********************************************************************************************************************************
Shim for PQgetvalue()
***********************************************************************************************************************************/
char *
PQgetvalue(const PGresult *res, int tup_num, int field_num)
{
return (char *)harnessPqScriptRun(
HRNPQ_GETVALUE, varLstAdd(varLstAdd(varLstNew(), varNewInt(tup_num)), varNewInt(field_num)), (HarnessPq *)res)->resultZ;
}
/***********************************************************************************************************************************
Shim for PQclear()
***********************************************************************************************************************************/
void
PQclear(PGresult *res)
{
if (!harnessPqScriptFail)
harnessPqScriptRun(HRNPQ_CLEAR, NULL, (HarnessPq *)res);
}
/***********************************************************************************************************************************
Shim for PQfinish()
***********************************************************************************************************************************/
void PQfinish(PGconn *conn)
{
if (!harnessPqScriptFail)
harnessPqScriptRun(HRNPQ_FINISH, NULL, (HarnessPq *)conn);
}
#endif // HARNESS_PQ_REAL

View File

@@ -0,0 +1,65 @@
/***********************************************************************************************************************************
Pq Test Harness
Scripted testing for PostgreSQL pqlib so exact results can be returned for unit testing. See PostgreSQL client unit tests for
usage examples.
***********************************************************************************************************************************/
#ifndef TEST_COMMON_HARNESS_PQ_H
#define TEST_COMMON_HARNESS_PQ_H
#ifndef HARNESS_PQ_REAL
#include "common/time.h"
/***********************************************************************************************************************************
Function constants
***********************************************************************************************************************************/
#define HRNPQ_CANCEL "PQcancel"
#define HRNPQ_CLEAR "PQclear"
#define HRNPQ_CONNECTDB "PQconnectdb"
#define HRNPQ_CONSUMEINPUT "PQconsumeInput"
#define HRNPQ_ERRORMESSAGE "PQerrorMessage"
#define HRNPQ_FINISH "PQfinish"
#define HRNPQ_FREECANCEL "PQfreeCancel"
#define HRNPQ_FTYPE "PQftype"
#define HRNPQ_GETCANCEL "PQgetCancel"
#define HRNPQ_GETISNULL "PQgetisnull"
#define HRNPQ_GETRESULT "PQgetResult"
#define HRNPQ_GETVALUE "PQgetvalue"
#define HRNPQ_ISBUSY "PQisbusy"
#define HRNPQ_NFIELDS "PQnfields"
#define HRNPQ_NTUPLES "PQntuples"
#define HRNPQ_RESULTERRORMESSAGE "PQresultErrorMessage"
#define HRNPQ_RESULTSTATUS "PQresultStatus"
#define HRNPQ_SENDQUERY "PQsendQuery"
#define HRNPQ_STATUS "PQstatus"
/***********************************************************************************************************************************
Data type constants
***********************************************************************************************************************************/
#define HRNPQ_TYPE_BOOL 16
#define HRNPQ_TYPE_INT 20
#define HRNPQ_TYPE_TEXT 25
/***********************************************************************************************************************************
Structure for scripting pq responses
***********************************************************************************************************************************/
typedef struct HarnessPq
{
unsigned int session; // Session number when mutliple sessions are run concurrently
const char *function; // Function call expected
const char *param; // Params expected by the function for verification
int resultInt; // Int result value
const char *resultZ; // Zero-terminated result value
bool resultNull; // Return null from function that normally returns a struct ptr
TimeMSec sleep; // Sleep specified milliseconds before returning from function
} HarnessPq;
/***********************************************************************************************************************************
Functions
***********************************************************************************************************************************/
void harnessPqScriptSet(HarnessPq *harnessPqScriptParam);
#endif // HARNESS_PQ_REAL
#endif

View File

@@ -0,0 +1,290 @@
/***********************************************************************************************************************************
Test PostgreSQL Client
This test can be run two ways:
1) The default uses a pqlib shim to simulate a PostgreSQL connection. This will work with all VM types.
2) Optionally use a real cluster for testing (only works with debian/pg11). The test Makefile must be manually updated with the
-DHARNESS_PQ_REAL flag and -lpq must be added to the libs list. This method does not have 100% coverage but is very close.
***********************************************************************************************************************************/
#include "common/type/json.h"
#include "common/harnessPq.h"
/***********************************************************************************************************************************
Test Run
***********************************************************************************************************************************/
void
testRun(void)
{
FUNCTION_HARNESS_VOID();
// *****************************************************************************************************************************
if (testBegin("pgClient"))
{
// Create and start the test database
// -------------------------------------------------------------------------------------------------------------------------
#ifdef HARNESS_PQ_REAL
if (system("sudo pg_createcluster 11 test") != 0)
THROW(AssertError, "unable to create cluster");
if (system("sudo pg_ctlcluster 11 test start") != 0)
THROW(AssertError, "unable to start cluster");
if (system(strPtr(strNewFmt("sudo -u postgres psql -c 'create user %s superuser'", testUser()))) != 0)
THROW(AssertError, "unable to create superuser");
#endif
// Test connection error
// -------------------------------------------------------------------------------------------------------------------------
#ifndef HARNESS_PQ_REAL
harnessPqScriptSet((HarnessPq [])
{
{.function = HRNPQ_CONNECTDB, .param = "[\"dbname='postg \\\\'\\\\\\\\res' port=5433\"]"},
{.function = HRNPQ_STATUS, .resultInt = CONNECTION_BAD},
{.function = HRNPQ_ERRORMESSAGE, .resultZ =
"could not connect to server: No such file or directory\n"
"\tIs the server running locally and accepting\n"
"\tconnections on Unix domain socket \"/var/run/postgresql/.s.PGSQL.5433\"?\n"},
{.function = HRNPQ_FINISH},
{.function = NULL}
});
#endif
PgClient *client = NULL;
TEST_ASSIGN(client, pgClientNew(NULL, 5433, strNew("postg '\\res"), NULL, 3000), "new client");
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"
"\tIs the server running locally and accepting\n"
"\tconnections on Unix domain socket \"/var/run/postgresql/.s.PGSQL.5433\"?");
TEST_RESULT_VOID(pgClientFree(client), "free client");
// Test send error
// -------------------------------------------------------------------------------------------------------------------------
#ifndef HARNESS_PQ_REAL
harnessPqScriptSet((HarnessPq [])
{
{.function = HRNPQ_CONNECTDB, .param = "[\"dbname='postgres' port=5432\"]"},
{.function = HRNPQ_STATUS, .resultInt = CONNECTION_OK},
{.function = HRNPQ_SENDQUERY, .param = "[\"select bogus from pg_class\"]", .resultInt = 0},
{.function = HRNPQ_ERRORMESSAGE, .resultZ = "another command is already in progress\n"},
{.function = HRNPQ_FINISH},
{.function = NULL}
});
#endif
TEST_ASSIGN(client, pgClientOpen(pgClientNew(NULL, 5432, strNew("postgres"), NULL, 3000)), "new client");
#ifdef HARNESS_PQ_REAL
PQsendQuery(client->connection, "select bogus from pg_class");
#endif
String *query = strNew("select bogus from pg_class");
TEST_ERROR(
pgClientQuery(client, query), DbQueryError,
"unable to send query 'select bogus from pg_class': another command is already in progress");
TEST_RESULT_VOID(pgClientFree(client), "free client");
// Connect
// -------------------------------------------------------------------------------------------------------------------------
#ifndef HARNESS_PQ_REAL
harnessPqScriptSet((HarnessPq [])
{
{.function = HRNPQ_CONNECTDB, .param = strPtr(
strNewFmt("[\"dbname='postgres' port=5432 user='%s' host='/var/run/postgresql'\"]", testUser()))},
{.function = HRNPQ_STATUS, .resultInt = CONNECTION_OK},
{.function = NULL}
});
#endif
TEST_ASSIGN(
client, pgClientOpen(pgClientNew(strNew("/var/run/postgresql"), 5432, strNew("postgres"), strNew(testUser()), 500)),
"new client");
// Invalid query
// -------------------------------------------------------------------------------------------------------------------------
#ifndef HARNESS_PQ_REAL
harnessPqScriptSet((HarnessPq [])
{
{.function = HRNPQ_SENDQUERY, .param = "[\"select bogus from pg_class\"]", .resultInt = 1},
{.function = HRNPQ_CONSUMEINPUT},
{.function = HRNPQ_ISBUSY},
{.function = HRNPQ_GETRESULT},
{.function = HRNPQ_RESULTSTATUS, .resultInt = PGRES_FATAL_ERROR},
{.function = HRNPQ_RESULTERRORMESSAGE, .resultZ =
"ERROR: column \"bogus\" does not exist\n"
"LINE 1: select bogus from pg_class\n"
" ^ \n"},
{.function = HRNPQ_CLEAR},
{.function = HRNPQ_GETRESULT, .resultNull = true},
{.function = NULL}
});
#endif
query = strNew("select bogus from pg_class");
TEST_ERROR(
pgClientQuery(client, query), DbQueryError,
"unable to execute query 'select bogus from pg_class': ERROR: column \"bogus\" does not exist\n"
"LINE 1: select bogus from pg_class\n"
" ^");
// Timeout query
// -------------------------------------------------------------------------------------------------------------------------
#ifndef HARNESS_PQ_REAL
harnessPqScriptSet((HarnessPq [])
{
{.function = HRNPQ_SENDQUERY, .param = "[\"select pg_sleep(3000)\"]", .resultInt = 1},
{.function = HRNPQ_CONSUMEINPUT, .sleep = 600},
{.function = HRNPQ_ISBUSY, .resultInt = 1},
{.function = HRNPQ_CONSUMEINPUT},
{.function = HRNPQ_ISBUSY, .resultInt = 1},
{.function = HRNPQ_GETCANCEL},
{.function = HRNPQ_CANCEL, .resultInt = 1},
{.function = HRNPQ_FREECANCEL},
{.function = HRNPQ_GETRESULT},
{.function = HRNPQ_CLEAR},
{.function = HRNPQ_GETRESULT, .resultNull = true},
{.function = NULL}
});
#endif
query = strNew("select pg_sleep(3000)");
TEST_ERROR(pgClientQuery(client, query), DbQueryError, "query 'select pg_sleep(3000)' timed out after 500ms");
// Cancel error (can only be run with the scripted tests
// -------------------------------------------------------------------------------------------------------------------------
#ifndef HARNESS_PQ_REAL
harnessPqScriptSet((HarnessPq [])
{
{.function = HRNPQ_SENDQUERY, .param = "[\"select pg_sleep(3000)\"]", .resultInt = 1},
{.function = HRNPQ_CONSUMEINPUT, .sleep = 600},
{.function = HRNPQ_ISBUSY, .resultInt = 1},
{.function = HRNPQ_CONSUMEINPUT},
{.function = HRNPQ_ISBUSY, .resultInt = 1},
{.function = HRNPQ_GETCANCEL},
{.function = HRNPQ_CANCEL, .resultInt = 0, .resultZ = "test error"},
{.function = HRNPQ_FREECANCEL},
{.function = NULL}
});
query = strNew("select pg_sleep(3000)");
TEST_ERROR(pgClientQuery(client, query), DbQueryError, "unable to cancel query 'select pg_sleep(3000)': test error");
#endif
// Execute do block and raise notice
// -------------------------------------------------------------------------------------------------------------------------
#ifndef HARNESS_PQ_REAL
harnessPqScriptSet((HarnessPq [])
{
{.function = HRNPQ_SENDQUERY, .param = "[\"do $$ begin raise notice 'mememe'; end $$\"]", .resultInt = 1},
{.function = HRNPQ_CONSUMEINPUT},
{.function = HRNPQ_ISBUSY},
{.function = HRNPQ_GETRESULT},
{.function = HRNPQ_RESULTSTATUS, .resultInt = PGRES_COMMAND_OK},
{.function = HRNPQ_CLEAR},
{.function = HRNPQ_GETRESULT, .resultNull = true},
{.function = NULL}
});
#endif
query = strNew("do $$ begin raise notice 'mememe'; end $$");
TEST_RESULT_PTR(pgClientQuery(client, query), NULL, "execute do block");
// Unsupported type
// -------------------------------------------------------------------------------------------------------------------------
#ifndef HARNESS_PQ_REAL
harnessPqScriptSet((HarnessPq [])
{
{.function = HRNPQ_SENDQUERY, .param = "[\"select clock_timestamp()\"]", .resultInt = 1},
{.function = HRNPQ_CONSUMEINPUT},
{.function = HRNPQ_ISBUSY},
{.function = HRNPQ_GETRESULT},
{.function = HRNPQ_RESULTSTATUS, .resultInt = PGRES_TUPLES_OK},
{.function = HRNPQ_NTUPLES, .resultInt = 1},
{.function = HRNPQ_NFIELDS, .resultInt = 1},
{.function = HRNPQ_FTYPE, .param = "[0]", .resultInt = 1184},
{.function = HRNPQ_GETVALUE, .param = "[0,0]", .resultZ = "2019-07-25 12:06:09.000282+00"},
{.function = HRNPQ_CLEAR},
{.function = HRNPQ_GETRESULT, .resultNull = true},
{.function = NULL}
});
#endif
query = strNew("select clock_timestamp()");
TEST_ERROR(
pgClientQuery(client, query), FormatError,
"unable to parse type 1184 in column 0 for query 'select clock_timestamp()'");
// Successful query
// -------------------------------------------------------------------------------------------------------------------------
#ifndef HARNESS_PQ_REAL
harnessPqScriptSet((HarnessPq [])
{
{.function = HRNPQ_SENDQUERY, .param =
"[\"select oid, case when relname = 'pg_class' then null::text else '' end, relname, relname = 'pg_class'"
" from pg_class where relname in ('pg_class', 'pg_proc')"
" order by relname\"]",
.resultInt = 1},
{.function = HRNPQ_CONSUMEINPUT},
{.function = HRNPQ_ISBUSY},
{.function = HRNPQ_GETRESULT},
{.function = HRNPQ_RESULTSTATUS, .resultInt = PGRES_TUPLES_OK},
{.function = HRNPQ_NTUPLES, .resultInt = 2},
{.function = HRNPQ_NFIELDS, .resultInt = 4},
{.function = HRNPQ_FTYPE, .param = "[0]", .resultInt = HRNPQ_TYPE_INT},
{.function = HRNPQ_FTYPE, .param = "[1]", .resultInt = HRNPQ_TYPE_TEXT},
{.function = HRNPQ_FTYPE, .param = "[2]", .resultInt = HRNPQ_TYPE_TEXT},
{.function = HRNPQ_FTYPE, .param = "[3]", .resultInt = HRNPQ_TYPE_BOOL},
{.function = HRNPQ_GETVALUE, .param = "[0,0]", .resultZ = "1259"},
{.function = HRNPQ_GETVALUE, .param = "[0,1]", .resultZ = ""},
{.function = HRNPQ_GETISNULL, .param = "[0,1]", .resultInt = 1},
{.function = HRNPQ_GETVALUE, .param = "[0,2]", .resultZ = "pg_class"},
{.function = HRNPQ_GETVALUE, .param = "[0,3]", .resultZ = "t"},
{.function = HRNPQ_GETVALUE, .param = "[1,0]", .resultZ = "1255"},
{.function = HRNPQ_GETVALUE, .param = "[1,1]", .resultZ = ""},
{.function = HRNPQ_GETISNULL, .param = "[1,1]", .resultInt = 0},
{.function = HRNPQ_GETVALUE, .param = "[1,2]", .resultZ = "pg_proc"},
{.function = HRNPQ_GETVALUE, .param = "[1,3]", .resultZ = "f"},
{.function = HRNPQ_CLEAR},
{.function = HRNPQ_GETRESULT, .resultNull = true},
{.function = NULL}
});
#endif
query = strNew(
"select oid, case when relname = 'pg_class' then null::text else '' end, relname, relname = 'pg_class'"
" from pg_class where relname in ('pg_class', 'pg_proc')"
" order by relname");
TEST_RESULT_STR(
strPtr(jsonFromVar(varNewVarLst(pgClientQuery(client, query)), 0)),
"[[1259,null,\"pg_class\",true],[1255,\"\",\"pg_proc\",false]]", "simple query");
// Close connection
// -------------------------------------------------------------------------------------------------------------------------
#ifndef HARNESS_PQ_REAL
harnessPqScriptSet((HarnessPq [])
{
{.function = HRNPQ_FINISH},
{.function = HRNPQ_GETRESULT, .resultNull = true},
{.function = NULL}
});
#endif
TEST_RESULT_VOID(pgClientClose(client), "close client");
}
FUNCTION_HARNESS_RESULT_VOID();
}