mirror of https://github.com/Gnucash/gnucash
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
550 lines
15 KiB
550 lines
15 KiB
/*
|
|
* PostgressBackend.c
|
|
*
|
|
* Implements the callbacks for the postgress backend.
|
|
* this is somewhat seriously broken, poorly designed code.
|
|
* its meant to be a quick hack just ot check things out.
|
|
* it needs extensive review and design checking
|
|
*
|
|
*/
|
|
|
|
#include <pgsql/libpq-fe.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include "BackendP.h"
|
|
#include "Group.h"
|
|
#include "Session.h"
|
|
#include "guid.h"
|
|
#include "util.h"
|
|
|
|
#include "PostgresBackend.h"
|
|
|
|
static short module = MOD_BACKEND;
|
|
|
|
/* ============================================================= */
|
|
|
|
#define SEND_QUERY(be) { \
|
|
int rc; \
|
|
rc = PQsendQuery (be->connection, be->buff); \
|
|
if (!rc) \
|
|
{ \
|
|
/* hack alert -- we need knider, gentler error handling */\
|
|
PERR("send query failed:\n" \
|
|
"\t%s", PQerrorMessage(be->connection)); \
|
|
PQfinish (be->connection); \
|
|
return; \
|
|
} \
|
|
}
|
|
|
|
#define FLUSH(conn) { \
|
|
PGresult *result; \
|
|
/* complete/commit the transaction, check the status */ \
|
|
do { \
|
|
ExecStatusType status; \
|
|
result = PQgetResult((conn)); \
|
|
if (!result) break; \
|
|
PINFO ("got result\n"); \
|
|
status = PQresultStatus(result); \
|
|
if (PGRES_COMMAND_OK != status) { \
|
|
PERR("bad status\n"); \
|
|
PQclear(result); \
|
|
PQfinish ((conn)); \
|
|
} \
|
|
PQclear(result); \
|
|
} while (result); \
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* This routine stores the indicated group structure into the database.
|
|
* It does *not* chase pointers, traverse the tree, etc.
|
|
* It performs no locking.
|
|
*/
|
|
|
|
static void
|
|
pgendStoreOneGroupOnly (PGBackend *be, AccountGroup *grp)
|
|
{
|
|
Account *parent;
|
|
const char *parent_guid, *grp_guid;
|
|
int i, nacc;
|
|
|
|
ENTER ("be=%p, grp=%p\n", be, grp);
|
|
if (!be || !grp) return;
|
|
|
|
grp_guid = guid_to_string(xaccGroupGetGUID (grp));
|
|
parent = xaccGroupGetParentAccount(grp);
|
|
parent_guid = guid_to_string(xaccAccountGetGUID (parent));
|
|
nacc = xaccGroupGetNumAccounts(grp);
|
|
|
|
for (i=0; i<nacc; i++) {
|
|
Account *acc = xaccGroupGetAccount(grp, i);
|
|
const char * acc_guid = guid_to_string(xaccAccountGetGUID (acc));
|
|
|
|
/* hack alert -- values should be escaped so that no '' apear in them */
|
|
snprintf (be->buff, be->bufflen,
|
|
"INSERT INTO gncGroup "
|
|
"(groupGuid, parentGuid, childGuid)"
|
|
" values "
|
|
"('%s', '%s', '%s');",
|
|
grp_guid,
|
|
parent_guid,
|
|
acc_guid
|
|
);
|
|
|
|
free ((char *) acc_guid);
|
|
SEND_QUERY(be);
|
|
|
|
/* complete/commit the transaction, check the status */
|
|
FLUSH(be->connection);
|
|
}
|
|
free ((char *) grp_guid);
|
|
free ((char *) parent_guid);
|
|
|
|
LEAVE ("\n");
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* This routine stores the indicated account structure into the database.
|
|
* It does *not* chase pointers, traverse the tree, etc.
|
|
* It performs no locking.
|
|
*/
|
|
|
|
static void
|
|
pgendStoreOneAccountOnly (PGBackend *be, Account *acct)
|
|
{
|
|
const char *acct_guid, *parent_guid, *child_guid;
|
|
ENTER ("be=%p, acct=%p\n", be, acct);
|
|
if (!be || !acct) return;
|
|
|
|
acct_guid = guid_to_string(xaccAccountGetGUID (acct));
|
|
parent_guid = guid_to_string(xaccGroupGetGUID (xaccAccountGetParent(acct)));
|
|
child_guid = guid_to_string(xaccGroupGetGUID (xaccAccountGetChildren(acct)));
|
|
|
|
/* hack alert -- values should be escaped so that no '' apear in them */
|
|
snprintf (be->buff, be->bufflen,
|
|
"INSERT INTO gncAccount "
|
|
"(accountGuid, parentGuid, childrenGuid, "
|
|
"accountName, accountCode, description, notes, "
|
|
"type, currency, security)"
|
|
" values "
|
|
"('%s', '%s', '%s', '%s', '%s', '%s', '%s', "
|
|
"%d, '%s', '%s');",
|
|
acct_guid,
|
|
parent_guid,
|
|
child_guid,
|
|
xaccAccountGetName (acct),
|
|
xaccAccountGetCode (acct),
|
|
xaccAccountGetDescription (acct),
|
|
xaccAccountGetNotes (acct),
|
|
xaccAccountGetType (acct),
|
|
xaccAccountGetCurrency (acct),
|
|
xaccAccountGetSecurity (acct)
|
|
);
|
|
free ((char *) acct_guid);
|
|
free ((char *) parent_guid);
|
|
free ((char *) child_guid);
|
|
|
|
SEND_QUERY (be);
|
|
|
|
/* complete/commit the transaction, check the status */
|
|
FLUSH(be->connection);
|
|
|
|
LEAVE ("\n");
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* This routine stores the indicated transaction structure into the database.
|
|
* It does *not* chase pointers, traverse the tree, etc.
|
|
* It performs no locking.
|
|
*/
|
|
|
|
static void
|
|
pgendStoreOneTransactionOnly (PGBackend *be, Transaction *trans)
|
|
{
|
|
const char * trans_guid;
|
|
|
|
ENTER ("be=%p, trans=%p\n", be, trans);
|
|
if (!be || !trans) return;
|
|
|
|
trans_guid = guid_to_string(xaccTransGetGUID (trans));
|
|
/* hack alert -- values should be escaped so that no '' apear in them */
|
|
snprintf (be->buff, be->bufflen,
|
|
"INSERT INTO gncTransaction "
|
|
"(transGuid, num, description)"
|
|
" values "
|
|
"('%s', '%s', '%s');",
|
|
trans_guid,
|
|
xaccTransGetNum (trans),
|
|
xaccTransGetDescription (trans)
|
|
);
|
|
|
|
free ((char *) trans_guid);
|
|
|
|
SEND_QUERY (be);
|
|
|
|
/* complete/commit the transaction, check the status */
|
|
FLUSH(be->connection);
|
|
|
|
LEAVE ("\n");
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* this routine stores the indicated split in the database
|
|
*/
|
|
|
|
static void
|
|
pgendStoreOneSplitOnly (PGBackend *be, Split *split)
|
|
{
|
|
Timespec ts;
|
|
const char *split_guid, *acct_guid, *trans_guid;
|
|
|
|
ENTER ("be=%p, split=%p\n", be, split);
|
|
if (!be || !split) return;
|
|
|
|
split_guid = guid_to_string(xaccSplitGetGUID (split));
|
|
acct_guid = guid_to_string(xaccAccountGetGUID (xaccSplitGetAccount(split)));
|
|
trans_guid = guid_to_string(xaccTransGetGUID (xaccSplitGetParent(split)));
|
|
|
|
/* hack alert date is not stored ... */
|
|
xaccSplitGetDateReconciledTS (split, &ts);
|
|
|
|
/* hack alert -- values should be escaped so that no '' apear in them */
|
|
snprintf (be->buff, be->bufflen,
|
|
"INSERT INTO gncEntry "
|
|
"(entryGuid, accountGuid, transGuid, memo, action,"
|
|
"reconciled, amount, share_price)"
|
|
" values "
|
|
"('%s', '%s', '%s', '%s', '%s', '%c', %g, %g);",
|
|
split_guid,
|
|
acct_guid,
|
|
trans_guid,
|
|
xaccSplitGetMemo(split),
|
|
xaccSplitGetAction(split),
|
|
xaccSplitGetReconcile(split),
|
|
xaccSplitGetShareAmount(split),
|
|
xaccSplitGetSharePrice(split)
|
|
);
|
|
free ((char *) split_guid);
|
|
free ((char *) acct_guid);
|
|
free ((char *) trans_guid);
|
|
|
|
SEND_QUERY (be);
|
|
|
|
/* complete/commit the transaction, check the status */
|
|
FLUSH(be->connection);
|
|
|
|
LEAVE ("\n");
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* this routine routine returns non-zero if the indicated split
|
|
* differs fropm that in the SQL database.
|
|
* this routine grabs no locks.
|
|
*/
|
|
|
|
static int
|
|
pgendCompareOneSplitOnly (PGBackend *be, Split *split)
|
|
{
|
|
const char *split_guid;
|
|
PGresult *result;
|
|
int i, nrows, ncols, ndiffs=0;
|
|
|
|
ENTER ("be=%p, split=%p\n", be, split);
|
|
if (!be || !split) return;
|
|
|
|
split_guid = guid_to_string(xaccSplitGetGUID (split));
|
|
|
|
/* try to find this split in the database */
|
|
/* hack alert -- values should be escaped so that no '' apear in them */
|
|
snprintf (be->buff, be->bufflen,
|
|
"SELECT accountGuid, transGuid, memo, action, "
|
|
"reconciled, amount, share_price "
|
|
"FROM gncEntry "
|
|
"WHERE entryGuid = '%s';",
|
|
split_guid
|
|
);
|
|
free ((char *) split_guid);
|
|
|
|
SEND_QUERY (be);
|
|
|
|
i=0; nrows=0;
|
|
do {
|
|
ExecStatusType status;
|
|
|
|
result = PQgetResult (be->connection);
|
|
if (!result) break;
|
|
|
|
status = PQresultStatus(result);
|
|
|
|
if ((PGRES_COMMAND_OK != status) &&
|
|
(PGRES_TUPLES_OK != status))
|
|
{
|
|
PERR ("failed to get result to query\n");
|
|
PQclear (result);
|
|
/* hack alert need gentler, kinder error recovery */
|
|
PQfinish (be->connection);
|
|
break;
|
|
}
|
|
|
|
nrows += PQntuples (result);
|
|
ncols = PQnfields (result);
|
|
|
|
PINFO ("query result %d has %d rows and %d cols\n",
|
|
i, nrows, ncols);
|
|
if (1 < nrows) {
|
|
PERR ("unexpected duplicate records\n");
|
|
break;
|
|
} else if (1 == nrows) {
|
|
|
|
#define GETV(str) (PQgetvalue (result, 0, PQfnumber (result, str)))
|
|
#define COMP(sqlname,fun) if (strcmp (GETV(sqlname),fun)) { \
|
|
PINFO("%s sql='%s', split='%s'\n", sqlname, GETV(sqlname), fun); \
|
|
ndiffs++; }
|
|
#define GCOMP(sqlname,fun) { \
|
|
const char *tmp = guid_to_string(fun); \
|
|
if (strcmp (GETV(sqlname),tmp)) { \
|
|
PINFO("%s sql='%s', split='%s'\n", sqlname, GETV(sqlname), tmp); \
|
|
ndiffs++; } \
|
|
free ((char *) tmp); }
|
|
|
|
/* compared queried values to input values */
|
|
COMP ("memo", xaccSplitGetMemo(split));
|
|
COMP ("action", xaccSplitGetAction(split));
|
|
GCOMP ("accountGuid",
|
|
xaccAccountGetGUID (xaccSplitGetAccount(split)));
|
|
GCOMP ("transGuid",
|
|
xaccTransGetGUID (xaccSplitGetParent(split)));
|
|
|
|
PINFO ("recn=%s amt=%s price=%s\n", GETV("reconciled"), GETV("amount"),
|
|
GETV("share_price"));
|
|
}
|
|
|
|
PQclear (result);
|
|
i++;
|
|
} while (result);
|
|
|
|
if (0 == nrows) ndiffs = -1;
|
|
|
|
/*
|
|
xaccSplitGetReconcile(split),
|
|
xaccSplitGetShareAmount(split),
|
|
xaccSplitGetSharePrice(split)
|
|
*/
|
|
|
|
|
|
LEAVE ("\n");
|
|
return ndiffs;
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* This routine traverses the group structure and stores it into
|
|
* the database. The NoLock version doesn't lock up the tables.
|
|
*/
|
|
|
|
static int
|
|
traverse_cb (Transaction *trans, void *cb_data)
|
|
{
|
|
PGBackend *be = (PGBackend *) cb_data;
|
|
int i, nsplits;
|
|
|
|
if (!be || !trans) return;
|
|
|
|
pgendStoreOneTransactionOnly (be, trans);
|
|
|
|
/* walk over the list of splits */
|
|
nsplits = xaccTransCountSplits (trans);
|
|
for (i=0; i<nsplits; i++) {
|
|
Split * s = xaccTransGetSplit (trans, i);
|
|
int ndiffs = pgendCompareOneSplitOnly (be, s);
|
|
PINFO ("ndiffs = %d\n", ndiffs);
|
|
pgendStoreOneSplitOnly (be, s);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
pgendStoreGroupNoLock (PGBackend *be, AccountGroup *grp)
|
|
{
|
|
int i, nacc;
|
|
|
|
if (!be || !grp) return;
|
|
|
|
/* first, store the top-group */
|
|
pgendStoreOneGroupOnly (be, grp);
|
|
|
|
/* next, walk the account tree, and store subaccounts */
|
|
nacc = xaccGroupGetNumAccounts(grp);
|
|
|
|
for (i=0; i<nacc; i++) {
|
|
AccountGroup *subgrp;
|
|
Account *acc = xaccGroupGetAccount(grp, i);
|
|
pgendStoreOneAccountOnly (be, acc);
|
|
|
|
/* recursively walk to child accounts */
|
|
subgrp = xaccAccountGetChildren (acc);
|
|
if (subgrp) pgendStoreGroupNoLock(be, subgrp);
|
|
}
|
|
}
|
|
|
|
|
|
static void
|
|
pgendStoreGroup (PGBackend *be, AccountGroup *grp)
|
|
{
|
|
ENTER ("be=%p, grp=%p\n", be, grp);
|
|
if (!be || !grp) return;
|
|
|
|
/* lock it up so that we store atomically */
|
|
snprintf (be->buff, be->bufflen, "BEGIN;");
|
|
SEND_QUERY (be);
|
|
FLUSH(be->connection);
|
|
|
|
/* reset the write flags. We use this to amek sure we don't
|
|
* get caught in infinite recursion */
|
|
xaccGroupBeginStagedTransactionTraversals(grp);
|
|
pgendStoreGroupNoLock (be, grp);
|
|
|
|
/* recursively walk transactions */
|
|
xaccGroupStagedTransactionTraversal (grp, 1, traverse_cb, be);
|
|
|
|
snprintf (be->buff, be->bufflen, "COMMIT;");
|
|
SEND_QUERY (be);
|
|
FLUSH(be->connection);
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* this routine fills in the structure pointed at by split
|
|
* with data sucked out of the database. It does only that
|
|
* one split,
|
|
*/
|
|
|
|
static void
|
|
pgendGetOneSplitOnly (PGBackend *be, Split *split, GUID *guid)
|
|
{
|
|
int rc;
|
|
|
|
ENTER ("be=%p, split=%p\n", be, split);
|
|
if (!be || !split || !guid) return;
|
|
|
|
rc = PQsendQuery (be->connection, "SELECT * FROM gncEntry;");
|
|
if (!rc)
|
|
{
|
|
PERR("send query failed:\n"
|
|
"\t%s", PQerrorMessage(be->connection));
|
|
PQfinish (be->connection);
|
|
return;
|
|
}
|
|
|
|
|
|
LEAVE ("\n");
|
|
|
|
}
|
|
|
|
/* ============================================================= */
|
|
|
|
AccountGroup *
|
|
pgend_session_begin (Session *sess, const char * sessionid)
|
|
{
|
|
PGBackend *be;
|
|
|
|
if (!sess) return;
|
|
be = (PGBackend *) xaccSessionGetBackend (sess);
|
|
|
|
ENTER("sessionid=%s\n", sessionid);
|
|
/* connect to a bogus database ... */
|
|
/* hack alert -- we should be parsing the sessionid for the
|
|
* hostname, port number, db name, etc... clean this up ... */
|
|
be->dbName = strdup ("gnc_bogus");
|
|
be->connection = PQsetdbLogin (NULL, NULL, NULL, NULL, be->dbName, NULL, NULL);
|
|
|
|
/* check the conmnection status */
|
|
if (CONNECTION_BAD == PQstatus(be->connection))
|
|
{
|
|
PERR("Connection to database '%s' failed:\n"
|
|
"\t%s",
|
|
be->dbName, PQerrorMessage(be->connection));
|
|
PQfinish (be->connection);
|
|
return NULL;
|
|
}
|
|
|
|
DEBUGCMD (PQtrace(be->connection, stderr));
|
|
|
|
/* hack alert --- */
|
|
/* just a quickie place to duimp stuff */
|
|
xaccGroupSetBackend (xaccSessionGetGroup(sess), &(be->be));
|
|
pgendStoreGroup (be, xaccSessionGetGroup(sess));
|
|
|
|
LEAVE("\n");
|
|
return NULL;
|
|
}
|
|
|
|
/* ============================================================= */
|
|
|
|
int
|
|
pgend_trans_commit_edit (Backend * bend, Transaction * trans)
|
|
{
|
|
PGBackend *be = (PGBackend *)bend;
|
|
int i, nsplits;
|
|
int rc;
|
|
|
|
ENTER ("be=%p, trans=%p\n", be, trans);
|
|
if (!be || !trans) return 1; /* hack alert hardcode literal */
|
|
|
|
nsplits = xaccTransCountSplits (trans);
|
|
for (i=0; i<nsplits; i++) {
|
|
Split *s = xaccTransGetSplit (trans, i);
|
|
int ndiffs = pgendCompareOneSplitOnly (be, s);
|
|
PINFO ("ndiffs = %d\n", ndiffs);
|
|
pgendStoreOneSplitOnly (be, s);
|
|
}
|
|
|
|
#if 0
|
|
rc = PQsendQuery (be->connection, "SELECT * FROM gncAccount;");
|
|
if (!rc)
|
|
{
|
|
PERR("send query failed:\n"
|
|
"\t%s", PQerrorMessage(be->connection));
|
|
PQfinish (be->connection);
|
|
return 2; /* hack alert hardcode literal */
|
|
}
|
|
#endif
|
|
|
|
|
|
LEAVE ("\n");
|
|
}
|
|
|
|
/* ============================================================= */
|
|
|
|
/* hack alert -- this is the query buffer, it might be too small.
|
|
we need to make it dynamic sized */
|
|
#define QBUFSIZE 16350
|
|
|
|
Backend *
|
|
pgendNew (void)
|
|
{
|
|
PGBackend *be;
|
|
|
|
be = (PGBackend *) malloc (sizeof (PGBackend));
|
|
|
|
/* generic backend handlers */
|
|
be->be.session_begin = pgend_session_begin;
|
|
be->be.session_end = NULL;
|
|
|
|
be->be.account_begin_edit = NULL;
|
|
be->be.account_commit_edit = NULL;
|
|
be->be.trans_begin_edit = NULL;
|
|
be->be.trans_commit_edit = pgend_trans_commit_edit;
|
|
be->be.trans_rollback_edit= NULL;
|
|
|
|
/* postgres specific data */
|
|
be->dbName = NULL;
|
|
be->connection = NULL;
|
|
|
|
be->buff = malloc (QBUFSIZE);
|
|
be->bufflen = QBUFSIZE;
|
|
|
|
return (Backend *) be;
|
|
}
|
|
|
|
/* ======================== END OF FILE ======================== */
|