bug fixes

git-svn-id: svn+ssh://svn.gnucash.org/repo/gnucash/trunk@3611 57a11ea4-9604-0410-9ed3-97b8803252fd
This commit is contained in:
Linas Vepstas 2001-02-07 06:12:33 +00:00
parent 75cf84a107
commit cc8044c8fc
7 changed files with 701 additions and 526 deletions

View File

@ -2,9 +2,12 @@
# Build the postgres backend as its own loadable shared object.
lib_LTLIBRARIES = libgnc_postgres.la
libgnc_postgres_la_LDFLAGS = -version-info 5:2:5
libgnc_postgres_la_SOURCES = \
builder.c \
checkpoint.c \
gncquery.c \
PostgresBackend.c

View File

@ -3,9 +3,10 @@
* PostgressBackend.c
*
* FUNCTION:
* Implements the callbacks for the postgress backend.
* this is code kinda usually works.
* it needs review and design checking
* Implements the callbacks for the Postgres backend.
* The SINGLE modes mostly work and are mostly feature complete.
* The multi-user modes are mostly in disrepair, and marginally
* functional.
*
* HISTORY:
* Copyright (c) 2000, 2001 Linas Vepstas
@ -40,6 +41,8 @@
#include "gncquery.h"
#include "PostgresBackend.h"
#include "putil.h"
static short module = MOD_BACKEND;
static void pgendDisable (PGBackend *be);
@ -55,206 +58,6 @@ static const char * pgendSessionGetMode (PGBackend *be);
*/
#define QBUFSIZE 16350
/* hack alert -- calling PQFinish() is quite harsh, since all
* subsequent sql queries will fail. On the other hand, killing
* anything that follows *is* a way of minimizing data corruption
* due to subsequent mishaps ... so anyway, error handling in these
* routines needs to be rethought.
*/
/* ============================================================= */
/* The SEND_QUERY macro sends the sql statement off to the server.
* It performs a minimal check to see that the send succeeded.
*/
#define SEND_QUERY(be,buff,retval) \
{ \
int rc; \
rc = PQsendQuery (be->connection, buff); \
if (!rc) \
{ \
/* hack alert -- we need kinder, gentler error handling */\
PERR("send query failed:\n" \
"\t%s", PQerrorMessage(be->connection)); \
PQfinish (be->connection); \
xaccBackendSetError (&be->be, ERR_SQL_SEND_QUERY_FAILED); \
return retval; \
} \
}
/* --------------------------------------------------------------- */
/* The FINISH_QUERY macro makes sure that the previously sent
* query complete with no errors. It assumes that the query
* is does not produce any results; if it did, those results are
* discarded (only error conditions are checked for).
*/
#define FINISH_QUERY(conn) \
{ \
int i=0; \
PGresult *result; \
/* complete/commit the transaction, check the status */ \
do { \
ExecStatusType status; \
result = PQgetResult((conn)); \
if (!result) break; \
PINFO ("clearing result %d", i); \
status = PQresultStatus(result); \
if (PGRES_COMMAND_OK != status) { \
PERR("finish query failed:\n" \
"\t%s", PQerrorMessage((conn))); \
PQclear(result); \
PQfinish ((conn)); \
xaccBackendSetError (&be->be, ERR_SQL_FINISH_QUERY_FAILED); \
} \
PQclear(result); \
i++; \
} while (result); \
}
/* --------------------------------------------------------------- */
/* The GET_RESULTS macro grabs the result of an pgSQL query off the
* wire, and makes sure that no errors occured. Results are left
* in the result buffer.
*/
#define GET_RESULTS(conn,result) \
{ \
ExecStatusType status; \
result = PQgetResult (conn); \
if (!result) break; \
status = PQresultStatus(result); \
if ((PGRES_COMMAND_OK != status) && \
(PGRES_TUPLES_OK != status)) \
{ \
PERR("failed to get result to query:\n" \
"\t%s", PQerrorMessage((conn))); \
PQclear (result); \
PQfinish (conn); \
xaccBackendSetError (&be->be, ERR_SQL_GET_RESULT_FAILED); \
break; \
} \
}
/* --------------------------------------------------------------- */
/* The IF_ONE_ROW macro counts the number of rows returned by
* a query, reports an error if there is more than one row, and
* conditionally executes a block for the first row.
*/
#define IF_ONE_ROW(result,nrows,loopcounter) \
{ \
int ncols = PQnfields (result); \
nrows += PQntuples (result); \
PINFO ("query result %d has %d rows and %d cols", \
loopcounter, nrows, ncols); \
} \
if (1 < nrows) { \
PERR ("unexpected duplicate records"); \
xaccBackendSetError (&be->be, ERR_SQL_CORRUPT_DB); \
break; \
} else if (1 == nrows)
/* --------------------------------------------------------------- */
/* Some utility macros for comparing values returned from the
* database to values in the engine structs. These macros
* all take three arguments:
* -- sqlname -- input -- the name of the field in the sql table
* -- fun -- input -- a subroutine returning a value
* -- ndiffs -- input/output -- integer, incremented if the
* value ofthe field and the value returned by
* the subroutine differ.
*
* The different macros compare different field types.
*/
#define DB_GET_VAL(str,n) (PQgetvalue (result, n, PQfnumber (result, str)))
/* compare string types. null strings and emty strings are
* considered to be equal */
#define COMP_STR(sqlname,fun,ndiffs) { \
if (null_strcmp (DB_GET_VAL(sqlname,0),fun)) { \
PINFO("mis-match: %s sql='%s', eng='%s'", sqlname, \
DB_GET_VAL (sqlname,0), fun); \
ndiffs++; \
} \
}
/* compare guids */
#define COMP_GUID(sqlname,fun, ndiffs) { \
char guid_str[GUID_ENCODING_LENGTH+1]; \
guid_to_string_buff(fun, guid_str); \
if (null_strcmp (DB_GET_VAL(sqlname,0),guid_str)) { \
PINFO("mis-match: %s sql='%s', eng='%s'", sqlname, \
DB_GET_VAL(sqlname,0), guid_str); \
ndiffs++; \
} \
}
/* comapre one char only */
#define COMP_CHAR(sqlname,fun, ndiffs) { \
if (tolower((DB_GET_VAL(sqlname,0))[0]) != tolower(fun)) { \
PINFO("mis-match: %s sql=%c eng=%c", sqlname, \
tolower((DB_GET_VAL(sqlname,0))[0]), tolower(fun)); \
ndiffs++; \
} \
}
/* Compare dates.
* Assumes the datestring is in ISO-8601 format
* i.e. looks like 1998-07-17 11:00:00.68-05
* hack-alert doesn't compare nano-seconds ..
* this is intentional, its because I suspect
* the sql db round nanoseconds off ...
*/
#define COMP_DATE(sqlname,fun,ndiffs) { \
Timespec eng_time = fun; \
Timespec sql_time = gnc_iso8601_to_timespec_local( \
DB_GET_VAL(sqlname,0)); \
if (eng_time.tv_sec != sql_time.tv_sec) { \
char buff[80]; \
gnc_timespec_to_iso8601_buff(eng_time, buff); \
PINFO("mis-match: %s sql='%s' eng=%s", sqlname, \
DB_GET_VAL(sqlname,0), buff); \
ndiffs++; \
} \
}
/* Compare the date of last modification.
* This is a special date comp to make the m4 macros simpler.
*/
#define COMP_NOW(sqlname,fun,ndiffs) { \
Timespec eng_time = xaccTransRetDateEnteredTS(ptr); \
Timespec sql_time = gnc_iso8601_to_timespec_local( \
DB_GET_VAL(sqlname,0)); \
if (eng_time.tv_sec != sql_time.tv_sec) { \
char buff[80]; \
gnc_timespec_to_iso8601_buff(eng_time, buff); \
PINFO("mis-match: %s sql='%s' eng=%s", sqlname, \
DB_GET_VAL(sqlname,0), buff); \
ndiffs++; \
} \
}
/* Compare long-long integers */
#define COMP_INT64(sqlname,fun,ndiffs) { \
if (atoll (DB_GET_VAL(sqlname,0)) != fun) { \
PINFO("mis-match: %s sql='%s', eng='%lld'", sqlname, \
DB_GET_VAL (sqlname,0), fun); \
ndiffs++; \
} \
}
/* compare 32-bit ints */
#define COMP_INT32(sqlname,fun,ndiffs) { \
if (atol (DB_GET_VAL(sqlname,0)) != fun) { \
PINFO("mis-match: %s sql='%s', eng='%d'", sqlname, \
DB_GET_VAL (sqlname,0), fun); \
ndiffs++; \
} \
}
/* ============================================================= */
/* misc bogus utility routines */
@ -344,7 +147,7 @@ pgendStoreAccountNoLock (PGBackend *be, Account *acct,
const gnc_commodity *com;
if (!be || !acct) return;
if (FALSE == acct->core_dirty) return;
if ((FALSE == do_mark) && (FALSE == acct->core_dirty)) return;
ENTER ("acct=%p, mark=%d", acct, do_mark);
@ -364,7 +167,7 @@ pgendStoreAccountNoLock (PGBackend *be, Account *acct,
/* make sure the account's commodity is in the commodity table */
/* hack alert -- it would be more efficient to do this elsewhere,
* and not here. */
* and not here. Or put a mark on it ... */
com = xaccAccountGetCommodity (acct);
pgendPutOneCommodityOnly (be, (gnc_commodity *) com);
@ -382,11 +185,10 @@ static void
pgendStoreTransactionNoLock (PGBackend *be, Transaction *trans,
gboolean do_mark)
{
GUID nullguid = *(xaccGUIDNULL());
GList *deletelist=NULL, *node;
GList *start, *deletelist=NULL, *node;
PGresult *result;
char * p;
int i, nrows, nsplits;
int i, nrows;
if (!be || !trans) return;
ENTER ("trans=%p, mark=%d", trans, do_mark);
@ -417,7 +219,6 @@ pgendStoreTransactionNoLock (PGBackend *be, Transaction *trans,
{
GUID guid = nullguid;
string_to_guid (DB_GET_VAL ("entryGuid", j), &guid);
/* If the database has splits that the engine doesn't,
* collect 'em up & we'll have to delete em */
if (NULL == xaccLookupEntity (&guid, GNC_ID_SPLIT))
@ -433,41 +234,44 @@ pgendStoreTransactionNoLock (PGBackend *be, Transaction *trans,
/* delete those that don't belong */
p = be->buff; *p = 0;
for (node=deletelist; node; node=node->next)
{
p = be->buff; *p = 0;
p = stpcpy (p, "DELETE FROM gncEntry WHERE entryGuid='");
p = stpcpy (p, node->data);
p = stpcpy (p, "';");
p = stpcpy (p, "';\n");
g_free (node->data);
}
if (p != be->buff)
{
PINFO ("%s", be->buff);
SEND_QUERY (be,be->buff, );
FINISH_QUERY(be->connection);
g_free (node->data);
}
/* Update the rest */
nsplits = xaccTransCountSplits (trans);
start = xaccTransGetSplitList(trans);
if ((nsplits) && !(trans->open & BEING_DESTROYED))
if ((start) && !(trans->open & BEING_DESTROYED))
{
for (i=0; i<nsplits; i++) {
Split * s = xaccTransGetSplit (trans, i);
for (node=start; node; node=node->next)
{
Split * s = node->data;
pgendPutOneSplitOnly (be, s);
}
pgendPutOneTransactionOnly (be, trans);
}
else
{
for (i=0; i<nsplits; i++) {
Split * s = xaccTransGetSplit (trans, i);
p = be->buff; *p = 0;
p = be->buff; *p = 0;
for (node=start; node; node=node->next)
{
Split * s = node->data;
p = stpcpy (p, "DELETE FROM gncEntry WHERE entryGuid='");
p = guid_to_string_buff (xaccSplitGetGUID(s), p);
p = stpcpy (p, "';");
PINFO ("%s\n", be->buff);
SEND_QUERY (be,be->buff, );
FINISH_QUERY(be->connection);
p = stpcpy (p, "';\n");
}
p = be->buff; *p = 0;
p = be->buff;
p = stpcpy (p, "DELETE FROM gncTransaction WHERE transGuid='");
p = guid_to_string_buff (xaccTransGetGUID(trans), p);
p = stpcpy (p, "';");
@ -520,17 +324,17 @@ static void
pgendStoreGroupNoLock (PGBackend *be, AccountGroup *grp,
gboolean do_mark)
{
int i, nacc;
GList *start, *node;
if (!be || !grp) return;
ENTER("grp=%p mark=%d", grp, do_mark);
/* walk the account tree, and store subaccounts */
nacc = xaccGroupGetNumAccounts(grp);
for (i=0; i<nacc; i++) {
start = xaccGroupGetAccountList (grp);
for (node=start; node; node=node->next)
{
AccountGroup *subgrp;
Account *acc = xaccGroupGetAccount(grp, i);
Account *acc = node->data;
pgendStoreAccountNoLock (be, acc, do_mark);
@ -550,6 +354,7 @@ pgendStoreGroup (PGBackend *be, AccountGroup *grp)
if (!be || !grp) return;
/* lock it up so that we store atomically */
/* hack alert ---- we need to lock a bunch of tables, right??!! */
bufp = "BEGIN;";
SEND_QUERY (be,bufp, );
FINISH_QUERY(be->connection);
@ -575,289 +380,6 @@ pgendStoreGroup (PGBackend *be, AccountGroup *grp)
LEAVE(" ");
}
/* ============================================================= */
/* recompute *all* checkpoints for the account */
static void
pgendAccountRecomputeAllCheckpoints (PGBackend *be, const GUID *acct_guid)
{
Timespec this_ts, prev_ts;
GMemChunk *chunk;
GList *node, *checkpoints = NULL;
PGresult *result;
Checkpoint *bp;
char *p;
int i, nrows, nsplits;
Account *acc;
const char *commodity_name;
if (!be) return;
ENTER("be=%p", be);
acc = xaccLookupEntity (acct_guid, GNC_ID_ACCOUNT);
commodity_name = gnc_commodity_get_unique_name (xaccAccountGetCommodity(acc));
chunk = g_mem_chunk_create (Checkpoint, 300, G_ALLOC_ONLY);
/* prevent others from inserting any splits while we recompute
* the checkpoints. (hack alert -verify that this is the correct
* lock) */
p = "BEGIN WORK; "
"LOCK TABLE gncEntry IN SHARE MODE; "
"LOCK TABLE gncCheckpoint IN ACCESS EXCLUSIVE MODE; ";
SEND_QUERY (be,p, );
FINISH_QUERY(be->connection);
/* Blow all the old checkpoints for this account out of the water.
* This should help ensure against accidental corruption.
*/
p = be->buff; *p = 0;
p = stpcpy (p, "DELETE FROM gncCheckpoint WHERE accountGuid='");
p = guid_to_string_buff (acct_guid, p);
p = stpcpy (p, "';");
SEND_QUERY (be,be->buff, );
FINISH_QUERY(be->connection);
/* and now, fetch *all* of the splits in this account */
p = be->buff; *p = 0;
p = stpcpy (p, "SELECT gncEntry.amountNum AS amountNum, "
" gncEntry.reconciled AS reconciled,"
" gncTransaction.date_posted AS date_posted "
"FROM gncEntry, gncTransaction "
"WHERE gncEntry.transGuid = gncTransaction.transGuid "
"AND accountGuid='");
p = guid_to_string_buff (acct_guid, p);
p = stpcpy (p, "' ORDER BY gncTransaction.date_posted ASC;");
SEND_QUERY (be,be->buff, );
/* malloc a new checkpoint, set it to the dawn of AD time ... */
bp = g_chunk_new0 (Checkpoint, chunk);
checkpoints = g_list_prepend (checkpoints, bp);
this_ts = gnc_iso8601_to_timespec_local ("1970-04-15 08:35:46.00");
bp->datetime = this_ts;
bp->account_guid = acct_guid;
bp->commodity = commodity_name;
/* malloc a new checkpoint ... */
nsplits = 0;
bp = g_chunk_new0 (Checkpoint, chunk);
checkpoints = g_list_prepend (checkpoints, bp);
bp->account_guid = acct_guid;
bp->commodity = commodity_name;
/* start adding up balances */
i=0; nrows=0;
do {
GET_RESULTS (be->connection, result);
{
int j, jrows;
int ncols = PQnfields (result);
jrows = PQntuples (result);
nrows += jrows;
PINFO ("query result %d has %d rows and %d cols",
i, nrows, ncols);
for (j=0; j<jrows; j++)
{
gint64 amt;
char recn;
/* lets see if its time to start a new checkpoint */
/* look for splits that occur at least ten seconds apart */
prev_ts = this_ts;
prev_ts.tv_sec += 10;
this_ts = gnc_iso8601_to_timespec_local (DB_GET_VAL("date_posted",j));
if ((MIN_CHECKPOINT_COUNT < nsplits) &&
(timespec_cmp (&prev_ts, &this_ts) < 0))
{
Checkpoint *next_bp;
/* Set checkpoint five seconds back. This is safe,
* because we looked for a 10 second gap above */
this_ts.tv_sec -= 5;
bp->datetime = this_ts;
/* and now, build a new checkpoint */
nsplits = 0;
next_bp = g_chunk_new0 (Checkpoint, chunk);
checkpoints = g_list_prepend (checkpoints, next_bp);
*next_bp = *bp;
bp = next_bp;
bp->account_guid = acct_guid;
bp->commodity = commodity_name;
}
nsplits ++;
/* accumulate balances */
amt = atoll (DB_GET_VAL("amountNum",j));
recn = (DB_GET_VAL("reconciled",j))[0];
bp->balance += amt;
if (NREC != recn)
{
bp->cleared_balance += amt;
}
if (YREC == recn)
{
bp->reconciled_balance += amt;
}
}
}
PQclear (result);
i++;
} while (result);
/* set the timestamp on the final checkpoint,
* 8 seconds past the very last split */
this_ts.tv_sec += 8;
bp->datetime = this_ts;
/* now store the checkpoints */
for (node = checkpoints; node; node = node->next)
{
bp = (Checkpoint *) node->data;
pgendStoreOneCheckpointOnly (be, bp, SQL_INSERT);
}
g_list_free (checkpoints);
g_mem_chunk_destroy (chunk);
p = "COMMIT WORK;";
SEND_QUERY (be,p, );
FINISH_QUERY(be->connection);
}
/* ============================================================= */
/* recompute fresh balance checkpoints for every account */
static void
pgendGroupRecomputeAllCheckpoints (PGBackend *be, AccountGroup *grp)
{
GList *acclist, *node;
acclist = xaccGroupGetSubAccounts(grp);
for (node = acclist; node; node=node->next)
{
Account *acc = (Account *) node->data;
pgendAccountRecomputeAllCheckpoints (be, xaccAccountGetGUID(acc));
}
g_list_free (acclist);
}
/* ============================================================= */
/* get checkpoint value for the account
* We find the checkpoint which matches the account and commodity,
* for the first date immediately preceeding the date.
* Then we fill in the balance fields for the returned query.
*/
static void
pgendAccountGetCheckpoint (PGBackend *be, Checkpoint *chk)
{
PGresult *result;
int i, nrows;
char * p;
if (!be || !chk) return;
ENTER("be=%p", be);
/* create the query we need */
p = be->buff; *p = 0;
p = stpcpy (p, "SELECT balance, cleared_balance, reconciled_balance "
"FROM gncCheckpoint "
"WHERE accountGuid='");
p = guid_to_string_buff (chk->account_guid, p);
p = stpcpy (p, "' AND commodity='");
p = stpcpy (p, chk->commodity);
p = stpcpy (p, "' AND date_xpoint <'");
p = gnc_timespec_to_iso8601_buff (chk->datetime, p);
p = stpcpy (p, "' ORDER BY date_xpoint DESC LIMIT 1;");
SEND_QUERY (be,be->buff, );
i=0; nrows=0;
do {
GET_RESULTS (be->connection, result);
{
int j=0, jrows;
int ncols = PQnfields (result);
jrows = PQntuples (result);
nrows += jrows;
PINFO ("query result %d has %d rows and %d cols",
i, nrows, ncols);
if (1 < nrows)
{
PERR ("excess data");
PQclear (result);
return;
}
chk->balance = atoll(DB_GET_VAL("balance", j));
chk->cleared_balance = atoll(DB_GET_VAL("cleared_balance", j));
chk->reconciled_balance = atoll(DB_GET_VAL("reconciled_balance", j));
}
PQclear (result);
i++;
} while (result);
LEAVE("be=%p", be);
}
/* ============================================================= */
/* get checkpoint value for all accounts */
static void
pgendGroupGetAllCheckpoints (PGBackend *be, AccountGroup*grp)
{
Checkpoint chk;
GList *acclist, *node;
if (!be || !grp) return;
ENTER("be=%p", be);
chk.datetime.tv_sec = time(0);
chk.datetime.tv_nsec = 0;
acclist = xaccGroupGetSubAccounts (grp);
/* loop over all accounts */
for (node=acclist; node; node=node->next)
{
Account *acc;
const gnc_commodity *com;
gint64 deno;
gnc_numeric baln;
gnc_numeric cleared_baln;
gnc_numeric reconciled_baln;
/* setupwhat we will match for */
acc = (Account *) node->data;
com = xaccAccountGetCommodity(acc);
chk.commodity = gnc_commodity_get_unique_name(com);
chk.account_guid = xaccAccountGetGUID (acc);
chk.balance = 0;
chk.cleared_balance = 0;
chk.reconciled_balance = 0;
/* get the checkpoint */
pgendAccountGetCheckpoint (be, &chk);
/* set the account balances */
deno = gnc_commodity_get_fraction (com);
baln = gnc_numeric_create (chk.balance, deno);
cleared_baln = gnc_numeric_create (chk.cleared_balance, deno);
reconciled_baln = gnc_numeric_create (chk.reconciled_balance, deno);
xaccAccountSetStartingBalance (acc, baln,
cleared_baln, reconciled_baln);
}
g_list_free (acclist);
LEAVE("be=%p", be);
}
/* ============================================================= */
static void
@ -939,7 +461,6 @@ static gboolean
pgendCopyTransactionToEngine (PGBackend *be, GUID *trans_guid)
{
const gnc_commodity *modity=NULL;
GUID nullguid = *(xaccGUIDNULL());
char *pbuff;
Transaction *trans;
PGresult *result;
@ -1284,7 +805,6 @@ IsGuidInList (GList *list, GUID *guid)
static void
pgendRunQueryHelper (PGBackend *be, const char *qstring)
{
GUID nullguid = *(xaccGUIDNULL());
PGresult *result;
int i, nrows;
GList *node, *xact_list = NULL;
@ -1444,7 +964,6 @@ pgendGetAllAccounts (PGBackend *be)
AccountGroup *topgrp;
char * bufp;
int i, nrows, iacc;
GUID nullguid = *(xaccGUIDNULL());
ENTER ("be=%p", be);
if (!be) return NULL;
@ -1590,8 +1109,9 @@ pgend_trans_commit_edit (Backend * bend,
Transaction * trans,
Transaction * oldtrans)
{
GList *start, *node;
char * bufp;
int i, ndiffs, nsplits, rollback=0;
int ndiffs, rollback=0;
PGBackend *be = (PGBackend *)bend;
ENTER ("be=%p, trans=%p", be, trans);
@ -1626,9 +1146,10 @@ pgend_trans_commit_edit (Backend * bend,
if (0 < ndiffs) rollback++;
/* be sure to check the old splits as well ... */
nsplits = xaccTransCountSplits (oldtrans);
for (i=0; i<nsplits; i++) {
Split * s = xaccTransGetSplit (oldtrans, i);
start = xaccTransGetSplitList (oldtrans);
for (node=start; node; node=node->next)
{
Split * s = node->data;
ndiffs = pgendCompareOneSplitOnly (be, s);
if (0 < ndiffs) rollback++;
}
@ -2322,6 +1843,9 @@ pgendEnable (PGBackend *be)
static void
pgendInit (PGBackend *be)
{
/* initialize global variable */
nullguid = *(xaccGUIDNULL());
/* access mode */
be->session_mode = MODE_NONE;
be->sessionGuid = NULL;

View File

@ -10,6 +10,9 @@
*/
#ifndef __POSTGRES_BACKEND_H__
#define __POSTGRES_BACKEND_H__
#include <pgsql/libpq-fe.h>
#include "BackendP.h"
@ -82,3 +85,32 @@ typedef struct _checkpoint {
gint64 reconciled_balance;
} Checkpoint;
/* -------------------------------------------------------- */
/* the following prototypes belong in a 'checkpoint.h' file */
void pgendGroupRecomputeAllCheckpoints (PGBackend *be, AccountGroup *grp);
void pgendGroupGetAllCheckpoints (PGBackend *be, AccountGroup*grp);
/* -------------------------------------------------------- */
/* the following protypes belong in a 'autogen.h' file */
void pgendStoreOneAccountOnly (PGBackend *be, Account *ptr,
sqlBuild_QType update);
void pgendStoreOneCheckpointOnly (PGBackend *be, Checkpoint *ptr,
sqlBuild_QType update);
void pgendStoreOneCommodityOnly (PGBackend *be, gnc_commodity *ptr,
sqlBuild_QType update);
void pgendStoreOneSessionOnly (PGBackend *be, void *ptr,
sqlBuild_QType update);
void pgendStoreOneSplitOnly (PGBackend *be, Split *ptr,
sqlBuild_QType update);
void pgendStoreOneTransactionOnly (PGBackend *be, Transaction *ptr,
sqlBuild_QType update);
#endif /* __POSTGRES_BACKEND_H__ */

View File

@ -7,7 +7,7 @@ problems.
postgres install instructions
Postgres Install Instructions
-----------------------------
1) Install postgresql server, client and devel packages.
2) if installed from redhat, then running /etc/rc.d/init.d/postgresql
@ -18,6 +18,32 @@ postgres install instructions
5) as yourself (i.e. your unix login), run 'createdb gnucash'
GnuCash Build Instructions
--------------------------
Same as usual, but you want to specify the flag '--enable-sql' i.e.
./configure --enable-sql
and then 'make'.
How To Use This Thing
---------------------
a) Open your favorite datafile in the usual fashion.
b) Click on 'Save As'
c) enter the following URL instead of a filename in the file picker:
postgres://localhost/some-name-you-pick
The above steps will copy your data into that database. You can
then restart gnucash (or keep working) and type in the same URL
in the file open dialogs. Or try it on the commandline:
/usr/local/bin/gnucash postgres://localhost/whatever
Note that you *must* use 'localhost' for your hostname, and
not some other hostname. This is because anything else requires
an sql username & password, and we don't have the gui dialogs for
that yet.
To Be Done
@ -34,11 +60,14 @@ Core bugs/features that still need work:
-- bug: group sync doesn't pull in newer data from the db ...
(related to the 'save as' above)
-- allow user to enter URL in GUI dialog
-- allow user to enter URL in GUI dialog, get GUI to remember the URL
-- Implement GUI to ask user for username/password to log onto the
server.
-- fix the annoying postgres:,,localhost,asdf file syntax: needs
mods to gnc-book to keep it happy about lock files & such.
To Be Done, Part II
-------------------
@ -47,10 +76,11 @@ This list only affects the multi-user and advanced/optional features.
-- implement account commit edit (actually, the check&rollback part)
(need to check the account version number beofer the commit happens)
-- fix excessive use of account commit by engine
-- use version numbers for accounts, commodities, splits & transactions,
as this will provide a far more efficient 'compare' for
user changes.
-- provide support for more query types in gncquery.c
-- optimize for quantity of SQL traffic -- there's a lot of it,
much of it probably un-needed.
-- Implement logging history in the SQL server. i.e. save the old
copies of stuff in log tables. Make the username part of the

324
src/engine/sql/checkpoint.c Normal file
View File

@ -0,0 +1,324 @@
/*
* FILE:
* checkpoint.c
*
* FUNCTION:
* account balance checkpointing.
*
* HISTORY:
* Copyright (c) 2000, 2001 Linas Vepstas
*
*/
#define _GNU_SOURCE
#include <glib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <pgsql/libpq-fe.h>
#include "Account.h"
#include "AccountP.h"
#include "Backend.h"
#include "BackendP.h"
#include "Group.h"
#include "gnc-commodity.h"
// #include "gnc-engine.h"
#include "gnc-engine-util.h"
// #include "gnc-event.h"
#include "guid.h"
#include "GNCId.h"
#include "GNCIdP.h"
#include "builder.h"
#include "PostgresBackend.h"
#include "putil.h"
static short module = MOD_BACKEND;
/* ============================================================= */
/* recompute *all* checkpoints for the account */
static void
pgendAccountRecomputeAllCheckpoints (PGBackend *be, const GUID *acct_guid)
{
Timespec this_ts, prev_ts;
GMemChunk *chunk;
GList *node, *checkpoints = NULL;
PGresult *result;
Checkpoint *bp;
char *p;
int i, nrows, nsplits;
Account *acc;
const char *commodity_name;
if (!be) return;
ENTER("be=%p", be);
acc = xaccLookupEntity (acct_guid, GNC_ID_ACCOUNT);
commodity_name = gnc_commodity_get_unique_name (xaccAccountGetCommodity(acc));
chunk = g_mem_chunk_create (Checkpoint, 300, G_ALLOC_ONLY);
/* prevent others from inserting any splits while we recompute
* the checkpoints. (hack alert -verify that this is the correct
* lock) */
p = "BEGIN WORK; "
"LOCK TABLE gncEntry IN SHARE MODE; "
"LOCK TABLE gncCheckpoint IN ACCESS EXCLUSIVE MODE; ";
SEND_QUERY (be,p, );
FINISH_QUERY(be->connection);
/* Blow all the old checkpoints for this account out of the water.
* This should help ensure against accidental corruption.
*/
p = be->buff; *p = 0;
p = stpcpy (p, "DELETE FROM gncCheckpoint WHERE accountGuid='");
p = guid_to_string_buff (acct_guid, p);
p = stpcpy (p, "';");
SEND_QUERY (be,be->buff, );
FINISH_QUERY(be->connection);
/* and now, fetch *all* of the splits in this account */
p = be->buff; *p = 0;
p = stpcpy (p, "SELECT gncEntry.amountNum AS amountNum, "
" gncEntry.reconciled AS reconciled,"
" gncTransaction.date_posted AS date_posted "
"FROM gncEntry, gncTransaction "
"WHERE gncEntry.transGuid = gncTransaction.transGuid "
"AND accountGuid='");
p = guid_to_string_buff (acct_guid, p);
p = stpcpy (p, "' ORDER BY gncTransaction.date_posted ASC;");
SEND_QUERY (be,be->buff, );
/* malloc a new checkpoint, set it to the dawn of AD time ... */
bp = g_chunk_new0 (Checkpoint, chunk);
checkpoints = g_list_prepend (checkpoints, bp);
this_ts = gnc_iso8601_to_timespec_local ("1970-04-15 08:35:46.00");
bp->datetime = this_ts;
bp->account_guid = acct_guid;
bp->commodity = commodity_name;
/* malloc a new checkpoint ... */
nsplits = 0;
bp = g_chunk_new0 (Checkpoint, chunk);
checkpoints = g_list_prepend (checkpoints, bp);
bp->account_guid = acct_guid;
bp->commodity = commodity_name;
/* start adding up balances */
i=0; nrows=0;
do {
GET_RESULTS (be->connection, result);
{
int j, jrows;
int ncols = PQnfields (result);
jrows = PQntuples (result);
nrows += jrows;
PINFO ("query result %d has %d rows and %d cols",
i, nrows, ncols);
for (j=0; j<jrows; j++)
{
gint64 amt;
char recn;
/* lets see if its time to start a new checkpoint */
/* look for splits that occur at least ten seconds apart */
prev_ts = this_ts;
prev_ts.tv_sec += 10;
this_ts = gnc_iso8601_to_timespec_local (DB_GET_VAL("date_posted",j));
if ((MIN_CHECKPOINT_COUNT < nsplits) &&
(timespec_cmp (&prev_ts, &this_ts) < 0))
{
Checkpoint *next_bp;
/* Set checkpoint five seconds back. This is safe,
* because we looked for a 10 second gap above */
this_ts.tv_sec -= 5;
bp->datetime = this_ts;
/* and now, build a new checkpoint */
nsplits = 0;
next_bp = g_chunk_new0 (Checkpoint, chunk);
checkpoints = g_list_prepend (checkpoints, next_bp);
*next_bp = *bp;
bp = next_bp;
bp->account_guid = acct_guid;
bp->commodity = commodity_name;
}
nsplits ++;
/* accumulate balances */
amt = atoll (DB_GET_VAL("amountNum",j));
recn = (DB_GET_VAL("reconciled",j))[0];
bp->balance += amt;
if (NREC != recn)
{
bp->cleared_balance += amt;
}
if (YREC == recn)
{
bp->reconciled_balance += amt;
}
}
}
PQclear (result);
i++;
} while (result);
/* set the timestamp on the final checkpoint,
* 8 seconds past the very last split */
this_ts.tv_sec += 8;
bp->datetime = this_ts;
/* now store the checkpoints */
for (node = checkpoints; node; node = node->next)
{
bp = (Checkpoint *) node->data;
pgendStoreOneCheckpointOnly (be, bp, SQL_INSERT);
}
g_list_free (checkpoints);
g_mem_chunk_destroy (chunk);
p = "COMMIT WORK;";
SEND_QUERY (be,p, );
FINISH_QUERY(be->connection);
}
/* ============================================================= */
/* recompute fresh balance checkpoints for every account */
void
pgendGroupRecomputeAllCheckpoints (PGBackend *be, AccountGroup *grp)
{
GList *acclist, *node;
acclist = xaccGroupGetSubAccounts(grp);
for (node = acclist; node; node=node->next)
{
Account *acc = (Account *) node->data;
pgendAccountRecomputeAllCheckpoints (be, xaccAccountGetGUID(acc));
}
g_list_free (acclist);
}
/* ============================================================= */
/* get checkpoint value for the account
* We find the checkpoint which matches the account and commodity,
* for the first date immediately preceeding the date.
* Then we fill in the balance fields for the returned query.
*/
static void
pgendAccountGetCheckpoint (PGBackend *be, Checkpoint *chk)
{
PGresult *result;
int i, nrows;
char * p;
if (!be || !chk) return;
ENTER("be=%p", be);
/* create the query we need */
p = be->buff; *p = 0;
p = stpcpy (p, "SELECT balance, cleared_balance, reconciled_balance "
"FROM gncCheckpoint "
"WHERE accountGuid='");
p = guid_to_string_buff (chk->account_guid, p);
p = stpcpy (p, "' AND commodity='");
p = stpcpy (p, chk->commodity);
p = stpcpy (p, "' AND date_xpoint <'");
p = gnc_timespec_to_iso8601_buff (chk->datetime, p);
p = stpcpy (p, "' ORDER BY date_xpoint DESC LIMIT 1;");
SEND_QUERY (be,be->buff, );
i=0; nrows=0;
do {
GET_RESULTS (be->connection, result);
{
int j=0, jrows;
int ncols = PQnfields (result);
jrows = PQntuples (result);
nrows += jrows;
PINFO ("query result %d has %d rows and %d cols",
i, nrows, ncols);
if (1 < nrows)
{
PERR ("excess data");
PQclear (result);
return;
}
chk->balance = atoll(DB_GET_VAL("balance", j));
chk->cleared_balance = atoll(DB_GET_VAL("cleared_balance", j));
chk->reconciled_balance = atoll(DB_GET_VAL("reconciled_balance", j));
}
PQclear (result);
i++;
} while (result);
LEAVE("be=%p", be);
}
/* ============================================================= */
/* get checkpoint value for all accounts */
void
pgendGroupGetAllCheckpoints (PGBackend *be, AccountGroup*grp)
{
Checkpoint chk;
GList *acclist, *node;
if (!be || !grp) return;
ENTER("be=%p", be);
chk.datetime.tv_sec = time(0);
chk.datetime.tv_nsec = 0;
acclist = xaccGroupGetSubAccounts (grp);
/* loop over all accounts */
for (node=acclist; node; node=node->next)
{
Account *acc;
const gnc_commodity *com;
gint64 deno;
gnc_numeric baln;
gnc_numeric cleared_baln;
gnc_numeric reconciled_baln;
/* setupwhat we will match for */
acc = (Account *) node->data;
com = xaccAccountGetCommodity(acc);
chk.commodity = gnc_commodity_get_unique_name(com);
chk.account_guid = xaccAccountGetGUID (acc);
chk.balance = 0;
chk.cleared_balance = 0;
chk.reconciled_balance = 0;
/* get the checkpoint */
pgendAccountGetCheckpoint (be, &chk);
/* set the account balances */
deno = gnc_commodity_get_fraction (com);
baln = gnc_numeric_create (chk.balance, deno);
cleared_baln = gnc_numeric_create (chk.cleared_balance, deno);
reconciled_baln = gnc_numeric_create (chk.reconciled_balance, deno);
xaccAccountSetStartingBalance (acc, baln,
cleared_baln, reconciled_baln);
}
g_list_free (acclist);
LEAVE("be=%p", be);
}
/* ======================== END OF FILE ======================== */

258
src/engine/sql/putil.h Normal file
View File

@ -0,0 +1,258 @@
/*
* FILE:
* putil.h
*
* FUNCTION:
* Postgres backend utility macros
*
* HISTORY:
* Copyright (c) 2000, 2001 Linas Vepstas
*
*/
#ifndef __P_UTIL_H__
#define __P_UTIL_H__
#include <glib.h>
#include <string.h>
#include <sys/types.h>
#include <pgsql/libpq-fe.h>
#include "Backend.h"
#include "BackendP.h"
#include "gnc-engine-util.h"
#include "guid.h"
#include "GNCId.h"
#include "PostgresBackend.h"
extern GUID nullguid;
/* hack alert -- calling PQFinish() is quite harsh, since all
* subsequent sql queries will fail. On the other hand, killing
* anything that follows *is* a way of minimizing data corruption
* due to subsequent mishaps ... so anyway, error handling in these
* routines needs to be rethought.
*/
/* ============================================================= */
/* The SEND_QUERY macro sends the sql statement off to the server.
* It performs a minimal check to see that the send succeeded.
*/
#define SEND_QUERY(be,buff,retval) \
{ \
int rc; \
rc = PQsendQuery (be->connection, buff); \
if (!rc) \
{ \
/* hack alert -- we need kinder, gentler error handling */\
PERR("send query failed:\n" \
"\t%s", PQerrorMessage(be->connection)); \
PQfinish (be->connection); \
xaccBackendSetError (&be->be, ERR_SQL_SEND_QUERY_FAILED); \
return retval; \
} \
}
/* --------------------------------------------------------------- */
/* The FINISH_QUERY macro makes sure that the previously sent
* query complete with no errors. It assumes that the query
* is does not produce any results; if it did, those results are
* discarded (only error conditions are checked for).
*/
#define FINISH_QUERY(conn) \
{ \
int i=0; \
PGresult *result; \
/* complete/commit the transaction, check the status */ \
do { \
ExecStatusType status; \
result = PQgetResult((conn)); \
if (!result) break; \
PINFO ("clearing result %d", i); \
status = PQresultStatus(result); \
if (PGRES_COMMAND_OK != status) { \
PERR("finish query failed:\n" \
"\t%s", PQerrorMessage((conn))); \
PQclear(result); \
PQfinish ((conn)); \
xaccBackendSetError (&be->be, ERR_SQL_FINISH_QUERY_FAILED); \
} \
PQclear(result); \
i++; \
} while (result); \
}
/* --------------------------------------------------------------- */
/* The GET_RESULTS macro grabs the result of an pgSQL query off the
* wire, and makes sure that no errors occured. Results are left
* in the result buffer.
*/
#define GET_RESULTS(conn,result) \
{ \
ExecStatusType status; \
result = PQgetResult (conn); \
if (!result) break; \
status = PQresultStatus(result); \
if ((PGRES_COMMAND_OK != status) && \
(PGRES_TUPLES_OK != status)) \
{ \
PERR("failed to get result to query:\n" \
"\t%s", PQerrorMessage((conn))); \
PQclear (result); \
PQfinish (conn); \
xaccBackendSetError (&be->be, ERR_SQL_GET_RESULT_FAILED); \
break; \
} \
}
/* --------------------------------------------------------------- */
/* The IF_ONE_ROW macro counts the number of rows returned by
* a query, reports an error if there is more than one row, and
* conditionally executes a block for the first row.
*/
#define IF_ONE_ROW(result,nrows,loopcounter) \
{ \
int ncols = PQnfields (result); \
nrows += PQntuples (result); \
PINFO ("query result %d has %d rows and %d cols", \
loopcounter, nrows, ncols); \
} \
if (1 < nrows) { \
PERR ("unexpected duplicate records"); \
xaccBackendSetError (&be->be, ERR_SQL_CORRUPT_DB); \
break; \
} else if (1 == nrows)
/* --------------------------------------------------------------- */
/* Some utility macros for comparing values returned from the
* database to values in the engine structs. These macros
* all take three arguments:
* -- sqlname -- input -- the name of the field in the sql table
* -- fun -- input -- a subroutine returning a value
* -- ndiffs -- input/output -- integer, incremented if the
* value ofthe field and the value returned by
* the subroutine differ.
*
* The different macros compare different field types.
*/
#define DB_GET_VAL(str,n) (PQgetvalue (result, n, PQfnumber (result, str)))
/* Compare string types. Null strings and empty strings are
* considered to be equal */
#define COMP_STR(sqlname,fun,ndiffs) { \
if (null_strcmp (DB_GET_VAL(sqlname,0),fun)) { \
PINFO("mis-match: %s sql='%s', eng='%s'", sqlname, \
DB_GET_VAL (sqlname,0), fun); \
ndiffs++; \
} \
}
/* Compare commodities. This routine is almost identical to
* COMP_STR, except that a NULL currency from the engine
* is allowed to match any currency in the sql DB. This is
* used to facilitate deletion, where the currency has been
* nulled out .. */
#define COMP_COMMODITY(sqlname,fun,ndiffs) { \
const char *com = fun; \
if (com) { \
if (null_strcmp (DB_GET_VAL(sqlname,0),com)) { \
PINFO("mis-match: %s sql='%s', eng='%s'", sqlname, \
DB_GET_VAL (sqlname,0), fun); \
ndiffs++; \
} \
} \
}
/* Compare guids. A NULL GUID from the engine is considered to
* match any value of a GUID in teh sql database. This is
* equality is used to enable deletion, where the GUID may have
* already been set to NULL in the engine, but not yet in the DB.
*/
#define COMP_GUID(sqlname,fun, ndiffs) { \
char guid_str[GUID_ENCODING_LENGTH+1]; \
const GUID *guid = fun; \
if (!guid_equal (guid, &nullguid)) { \
guid_to_string_buff(guid, guid_str); \
if (null_strcmp (DB_GET_VAL(sqlname,0),guid_str)) { \
PINFO("mis-match: %s sql='%s', eng='%s'", sqlname, \
DB_GET_VAL(sqlname,0), guid_str); \
ndiffs++; \
} \
} \
}
/* Comapre one char only */
#define COMP_CHAR(sqlname,fun, ndiffs) { \
if (tolower((DB_GET_VAL(sqlname,0))[0]) != tolower(fun)) { \
PINFO("mis-match: %s sql=%c eng=%c", sqlname, \
tolower((DB_GET_VAL(sqlname,0))[0]), tolower(fun)); \
ndiffs++; \
} \
}
/* Compare dates.
* Assumes the datestring is in ISO-8601 format
* i.e. looks like 1998-07-17 11:00:00.68-05
* hack-alert doesn't compare nano-seconds ..
* this is intentional, its because I suspect
* the sql db round nanoseconds off ...
*/
#define COMP_DATE(sqlname,fun,ndiffs) { \
Timespec eng_time = fun; \
Timespec sql_time = gnc_iso8601_to_timespec_local( \
DB_GET_VAL(sqlname,0)); \
if (eng_time.tv_sec != sql_time.tv_sec) { \
char buff[80]; \
gnc_timespec_to_iso8601_buff(eng_time, buff); \
PINFO("mis-match: %s sql='%s' eng=%s", sqlname, \
DB_GET_VAL(sqlname,0), buff); \
ndiffs++; \
} \
}
/* Compare the date of last modification.
* This is a special date comp to make the m4 macros simpler.
*/
#define COMP_NOW(sqlname,fun,ndiffs) { \
Timespec eng_time = xaccTransRetDateEnteredTS(ptr); \
Timespec sql_time = gnc_iso8601_to_timespec_local( \
DB_GET_VAL(sqlname,0)); \
if (eng_time.tv_sec != sql_time.tv_sec) { \
char buff[80]; \
gnc_timespec_to_iso8601_buff(eng_time, buff); \
PINFO("mis-match: %s sql='%s' eng=%s", sqlname, \
DB_GET_VAL(sqlname,0), buff); \
ndiffs++; \
} \
}
/* Compare long-long integers */
#define COMP_INT64(sqlname,fun,ndiffs) { \
if (atoll (DB_GET_VAL(sqlname,0)) != fun) { \
PINFO("mis-match: %s sql='%s', eng='%lld'", sqlname, \
DB_GET_VAL (sqlname,0), fun); \
ndiffs++; \
} \
}
/* compare 32-bit ints */
#define COMP_INT32(sqlname,fun,ndiffs) { \
if (atol (DB_GET_VAL(sqlname,0)) != fun) { \
PINFO("mis-match: %s sql='%s', eng='%d'", sqlname, \
DB_GET_VAL (sqlname,0), fun); \
ndiffs++; \
} \
}
#endif /* __P_UTIL_H__ */
/* ======================== END OF FILE ======================== */

View File

@ -41,7 +41,7 @@ define(`split', `gncEntry, Split, Split,
define(`transaction', `gncTransaction, Transaction, Transaction,
num, , char *, xaccTransGetNum(ptr),
description, , char *, xaccTransGetDescription(ptr),
currency, , char *, gnc_commodity_get_unique_name(xaccTransGetCurrency(ptr)),
currency, , commod, gnc_commodity_get_unique_name(xaccTransGetCurrency(ptr)),
date_entered, , now, "NOW",
date_posted, , Timespec, xaccTransRetDatePostedTS(ptr),
transGUID, KEY, GUID *, xaccTransGetGUID(ptr),
@ -97,6 +97,7 @@ define(`sql_setter', `ifelse($2, `KEY',
$2, ,
`ifelse($1, `char *', sqlBuild_Set_Str,
$1, `now', sqlBuild_Set_Str,
$1, `commod', sqlBuild_Set_Str,
$1, `int32', sqlBuild_Set_Int32,
$1, `int64', sqlBuild_Set_Int64,
$1, `GUID *', sqlBuild_Set_GUID,
@ -114,12 +115,15 @@ define(`set_fields', `set_fields_r(firstrec($@))')
/* -------- */
/* macros to compare a query result */
/* the commod type behaves just like a string, except it
* has its one compre function. */
define(`cmp_value', `ifelse($1, `char *', COMP_STR,
$1, `now', COMP_NOW,
$1, `int32', COMP_INT32,
$1, `int64', COMP_INT64,
$1, `GUID *', COMP_GUID,
$1, `commod', COMP_COMMODITY,
$1, `Timespec', COMP_DATE,
$1, `char', COMP_CHAR)')
@ -143,7 +147,7 @@ define(`store_one_only',
* It just pokes the data in
*/
static void
void
pgendStoreOne`'func_name($@)`'Only (PGBackend *be,
xacc_type($@) *ptr,
sqlBuild_QType update)