diff --git a/src/engine/sql/PostgresBackend.c b/src/engine/sql/PostgresBackend.c index 67d7a8a207..0a424df7b4 100644 --- a/src/engine/sql/PostgresBackend.c +++ b/src/engine/sql/PostgresBackend.c @@ -14,8 +14,11 @@ #define _GNU_SOURCE #include +#include #include #include +#include +#include #include @@ -40,6 +43,16 @@ static short module = MOD_BACKEND; static void pgendDisable (PGBackend *be); static void pgendEnable (PGBackend *be); +static void pgendInit (PGBackend *be); + +static const char * pgendSessionGetMode (PGBackend *be); + +/* hack alert -- this is the query buffer size, it can be overflowed. + * Ideally, its dynamically resized. On the other hand, Postgres + * rejects queries longer than 8192 bytes,(according to the + * documentation) so theres not much point in getting fancy ... + */ +#define QBUFSIZE 16350 /* ============================================================= */ /* The SEND_QUERY macro sends the sql statement off to the server. @@ -56,6 +69,7 @@ static void pgendEnable (PGBackend *be); PERR("send query failed:\n" \ "\t%s", PQerrorMessage(be->connection)); \ PQfinish (be->connection); \ + xaccBackendSetError (&be->be, ERR_SQL_SEND_QUERY_FAILED); \ return retval; \ } \ } @@ -79,9 +93,11 @@ static void pgendEnable (PGBackend *be); PINFO ("clearing result %d", i); \ status = PQresultStatus(result); \ if (PGRES_COMMAND_OK != status) { \ - PERR("bad 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++; \ @@ -102,10 +118,12 @@ static void pgendEnable (PGBackend *be); if ((PGRES_COMMAND_OK != status) && \ (PGRES_TUPLES_OK != status)) \ { \ - PERR ("failed to get result to query"); \ + PERR("failed to get result to query:\n" \ + "\t%s", PQerrorMessage((conn))); \ PQclear (result); \ /* hack alert need gentler, kinder error recovery */ \ PQfinish (conn); \ + xaccBackendSetError (&be->be, ERR_SQL_GET_RESULT_FAILED); \ break; \ } \ } @@ -125,116 +143,176 @@ static void pgendEnable (PGBackend *be); } \ 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. + * 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 GET_DB_VAL(str,n) (PQgetvalue (result, n, PQfnumber (result, str))) +#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 (GET_DB_VAL(sqlname,0),fun)) { \ - PINFO("%s sql='%s', eng='%s'", sqlname, \ - GET_DB_VAL (sqlname,0), fun); \ + 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) { \ - const char *tmp = guid_to_string(fun); \ - if (null_strcmp (GET_DB_VAL(sqlname,0),tmp)) { \ - PINFO("%s sql='%s', eng='%s'", sqlname, \ - GET_DB_VAL(sqlname,0), tmp); \ + 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++; \ } \ - free ((char *) tmp); \ } /* comapre one char only */ #define COMP_CHAR(sqlname,fun, ndiffs) { \ - if (tolower((GET_DB_VAL(sqlname,0))[0]) != tolower(fun)) { \ - PINFO("%s sql=%c eng=%c", sqlname, \ - tolower((GET_DB_VAL(sqlname,0))[0]), tolower(fun)); \ + 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++; \ } \ } -/* assumes the datestring is in ISO-8601 format +/* 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 .. - * that's becuase I suspect the sql db round nanoseconds off ... + * 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( \ - GET_DB_VAL(sqlname,0)); \ + Timespec sql_time = gnc_iso8601_to_timespec_local( \ + DB_GET_VAL(sqlname,0)); \ if (eng_time.tv_sec != sql_time.tv_sec) { \ - time_t tmp = eng_time.tv_sec; \ - PINFO("%s sql='%s' eng=%s", sqlname, \ - GET_DB_VAL(sqlname,0), ctime(&tmp)); \ + 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++; \ } \ } -/* a very special date comp */ +/* 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( \ - GET_DB_VAL(sqlname,0)); \ + Timespec sql_time = gnc_iso8601_to_timespec_local( \ + DB_GET_VAL(sqlname,0)); \ if (eng_time.tv_sec != sql_time.tv_sec) { \ - time_t tmp = eng_time.tv_sec; \ - PINFO("%s sql='%s' eng=%s", sqlname, \ - GET_DB_VAL(sqlname,0), ctime(&tmp)); \ + 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 (GET_DB_VAL(sqlname,0)) != fun) { \ - PINFO("%s sql='%s', eng='%lld'", sqlname, \ - GET_DB_VAL (sqlname,0), fun); \ + 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 (GET_DB_VAL(sqlname,0)) != fun) { \ - PINFO("%s sql='%s', eng='%d'", sqlname, \ - GET_DB_VAL (sqlname,0), fun); \ + 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 */ -#include "tmp.c" +static char * +pgendGetHostname (PGBackend *be) +{ + char * p; + + p = be->buff; + *p = 0; + if (0 == gethostname (p, QBUFSIZE/3)) + { + p += strlen (p); + p = stpcpy (p, "."); + } + getdomainname (p, QBUFSIZE/3); + return be->buff; +} + +static char * +pgendGetUsername (PGBackend *be) +{ + uid_t uid = getuid(); + struct passwd *pw = getpwuid (uid); + if (pw) return (pw->pw_name); + return NULL; +} + +static char * +pgendGetUserGecos (PGBackend *be) +{ + uid_t uid = getuid(); + struct passwd *pw = getpwuid (uid); + if (pw) return (pw->pw_gecos); + return NULL; +} /* ============================================================= */ -/* This routine updates the commodity structure if needed, and/or - * stores it the first time if it hasn't yet been stored. +/* This routine finds the commodity by parsing a string + * of the form NAMESPACE::MNEMONIC */ -static void -pgendStoreCommodityNoLock (PGBackend *be, const gnc_commodity *com) +static const gnc_commodity * +gnc_string_to_commodity (const char *str) { - gnc_commodity *commie = (gnc_commodity *) com; - int ndiffs; - if (!be || !com) return; + /* hop through a couple of hoops for the commodity */ + /* it would be nice to simplify this ... */ + gnc_commodity_table *comtab = gnc_engine_commodities(); + gnc_commodity *com; + char *space, *name; - ndiffs = pgendCompareOnegnc_commodityOnly (be, commie); + space = g_strdup(str); + name = strchr (space, ':'); + *name = 0; + name += 2; - /* update commodity if there are differences ... */ - if (0ndiffs) pgendStoreOnegnc_commodityOnly (be, commie, SQL_INSERT); - - LEAVE(" "); + com = gnc_commodity_table_lookup(comtab, space, name); + g_free (space); + return com; } +/* ============================================================= */ +/* include the auto-generated code */ + +#include "autogen.c" + /* ============================================================= */ /* This routine updates the account structure if needed, and/or * stores it the first time if it hasn't yet been stored. @@ -248,7 +326,6 @@ pgendStoreAccountNoLock (PGBackend *be, Account *acct, gboolean do_mark) { const gnc_commodity *com; - int ndiffs; if (!be || !acct) return; ENTER ("acct=%p, mark=%d", acct, do_mark); @@ -265,16 +342,13 @@ pgendStoreAccountNoLock (PGBackend *be, Account *acct, xaccAccountSetMark (acct, 1); } - ndiffs = pgendCompareOneAccountOnly (be, acct); - - /* update account if there are differences ... */ - if (0ndiffs) pgendStoreOneAccountOnly (be, acct, SQL_INSERT); + pgendPutOneAccountOnly (be, 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. */ com = xaccAccountGetCommodity (acct); - pgendStoreCommodityNoLock (be, com); + pgendPutOneCommodityOnly (be, (gnc_commodity *) com); LEAVE(" "); } @@ -290,57 +364,123 @@ static void pgendStoreTransactionNoLock (PGBackend *be, Transaction *trans, gboolean do_mark) { - int i, ndiffs, nsplits; + GUID nullguid = *(xaccGUIDNULL()); + GList *deletelist=NULL, *node; + PGresult *result; + char * p; + int i, nrows, nsplits; if (!be || !trans) return; ENTER ("trans=%p, mark=%d", trans, do_mark); - ndiffs = pgendCompareOneTransactionOnly (be, trans); - /* update transaction if there are differences ... */ - if (0ndiffs) pgendStoreOneTransactionOnly (be, trans, SQL_INSERT); + /* first, we need to see which splits are in the database + * since what is there may not match what we have cached in + * the engine. */ + p = be->buff; *p = 0; + p = stpcpy (p, "SELECT entryGuid FROM gncEntry WHERE transGuid='"); + p = guid_to_string_buff(xaccTransGetGUID(trans), p); + p = stpcpy (p, "';"); - /* walk over the list of splits */ - nsplits = xaccTransCountSplits (trans); - for (i=0; ibuff, ); - ndiffs = pgendCompareOneSplitOnly (be, s); - /* update split if there are differences ... */ - if (0ndiffs) pgendStoreOneSplitOnly (be, s, SQL_INSERT); + 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); - /* check to see if the account that this split references is in - * storage; if not, add it */ - pgendStoreAccountNoLock (be, acct, do_mark); + for (j=0; jnext) + { + p = be->buff; *p = 0; + p = stpcpy (p, "DELETE FROM gncEntry WHERE entryGuid='"); + p = stpcpy (p, node->data); + p = stpcpy (p, "';"); + SEND_QUERY (be,be->buff, ); + FINISH_QUERY(be->connection); + g_free (node->data); } + + /* Update the rest */ + nsplits = xaccTransCountSplits (trans); + + if ((nsplits) && !(trans->open & BEING_DESTROYED)) + { + for (i=0; ibuff; *p = 0; + 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 = be->buff; *p = 0; + p = stpcpy (p, "DELETE FROM gncTransaction WHERE transGuid='"); + p = guid_to_string_buff (xaccTransGetGUID(trans), p); + p = stpcpy (p, "';"); + PINFO ("%s\n", be->buff); + SEND_QUERY (be,be->buff, ); + FINISH_QUERY(be->connection); + } + LEAVE(" "); } static void pgendStoreTransaction (PGBackend *be, Transaction *trans) { + char * bufp; if (!be || !trans) return; ENTER ("be=%p, trans=%p", be, trans); /* lock it up so that we store atomically */ - snprintf (be->buff, be->bufflen, "BEGIN;" - "LOCK TABLE gncTransaction IN EXCLUSIVE MODE; " - "LOCK TABLE gncEntry IN EXCLUSIVE MODE; " - "LOCK TABLE gncAccount IN EXCLUSIVE MODE; " - "LOCK TABLE gncCommodity IN EXCLUSIVE MODE; " - ); - SEND_QUERY (be,be->buff, ); + bufp = "BEGIN;" + "LOCK TABLE gncTransaction IN EXCLUSIVE MODE; " + "LOCK TABLE gncEntry IN EXCLUSIVE MODE; " + "LOCK TABLE gncAccount IN EXCLUSIVE MODE; " + "LOCK TABLE gncCommodity IN EXCLUSIVE MODE; "; + SEND_QUERY (be,bufp, ); FINISH_QUERY(be->connection); pgendStoreTransactionNoLock (be, trans, FALSE); - snprintf (be->buff, be->bufflen, "COMMIT;"); - SEND_QUERY (be,be->buff, ); + bufp = "COMMIT;"; + SEND_QUERY (be,bufp, ); FINISH_QUERY(be->connection); LEAVE(" "); } @@ -387,12 +527,13 @@ pgendStoreGroupNoLock (PGBackend *be, AccountGroup *grp, static void pgendStoreGroup (PGBackend *be, AccountGroup *grp) { + char *bufp; ENTER ("be=%p, grp=%p", be, grp); if (!be || !grp) return; /* lock it up so that we store atomically */ - snprintf (be->buff, be->bufflen, "BEGIN;"); - SEND_QUERY (be,be->buff, ); + bufp = "BEGIN;"; + SEND_QUERY (be,bufp, ); FINISH_QUERY(be->connection); /* Clear the account marks; useful later to avoid recurision @@ -410,12 +551,297 @@ pgendStoreGroup (PGBackend *be, AccountGroup *grp) /* reset the write flags again */ xaccClearMarkDownGr (grp, 0); - snprintf (be->buff, be->bufflen, "COMMIT;"); - SEND_QUERY (be,be->buff, ); + bufp = "COMMIT;"; + SEND_QUERY (be,bufp, ); FINISH_QUERY(be->connection); 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; jdatetime = 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 pgendSync (Backend *bend, AccountGroup *grp) { @@ -423,29 +849,81 @@ pgendSync (Backend *bend, AccountGroup *grp) ENTER ("be=%p, grp=%p", be, grp); /* hack alert -- this is *not* the correct implementation - * of what they synchronize function is supposed to do. + * of what the synchronize function is supposed to do. * This is a sick placeholder. */ pgendStoreGroup (be, grp); + + if ((MODE_SINGLE_FILE != be->session_mode) && + (MODE_SINGLE_UPDATE != be->session_mode)) + { + /* Maybe this should be part of store group ?? */ + pgendGroupRecomputeAllCheckpoints (be, grp); + } + LEAVE(" "); } /* ============================================================= */ -/* This routine returns the update Transaction structure - * associated with the GUID. Data is pulled out of the database, - * the versions are compared, and updates made, if needed. - * The splits are handled as well ... - * - * hack alert unfinished, incomplete - */ static void -pgendSyncTransaction (PGBackend *be, GUID *trans_guid) +pgendSyncSingleFile (Backend *bend, AccountGroup *grp) { + char *p; + PGBackend *be = (PGBackend *)bend; + ENTER ("be=%p, grp=%p", be, grp); + + /* In single file mode, we treat 'sync' as 'file save'. + * We start by deleting *everything*, and then writing + * everything out. This is rather nasty, ugly and dangerous, + * but that's the nature of single-file mode. Note: we + * have to delete everything because there is no other way + * of finding out that an account, transaction or split + * was deleted. i.e. there's no other way to delete. So + * start with a clean slate. + */ + + p = "DELETE FROM gncEntry; " + "DELETE FROM gncTransaction; " + "DELETE FROM gncAccount; " + "DELETE FROM gncCommodity; "; + SEND_QUERY (be,p, ); + FINISH_QUERY(be->connection); + + pgendStoreGroup (be, grp); + + LEAVE(" "); +} + +/* ============================================================= */ +/* + * The pgendCopyTransactionToEngine() routine 'copies' data out of + * the SQL database and into the engine, for the indicated + * Transaction GUID. It starts by looking for an existing + * transaction in the engine with such a GUID. If found, then + * it compares the date of last update to what's in the sql DB. + * If the engine data is older, or the engine doesn't yet have + * this transaction, then the full update happens. The full + * update sets up the stransaction structure, all of the splits + * in the transaction, and makes sure that all of the splits + * are in the proper accounts. If the pre-existing tranasaction + * in the engine has more splits than what's in the DB, then these + * are pruned so that the structure exactly matches what's in the + * DB. This routine then returns FALSE. + * + * If this routine finds a pre-existing transaction in the engine, + * and the date of last modification of this transaction is + * *newer* then what the DB holds, then this routine returns + * TRUE, and does *not* perform any update. + */ + +static gboolean +pgendCopyTransactionToEngine (PGBackend *be, GUID *trans_guid) +{ + const gnc_commodity *modity=NULL; GUID nullguid = *(xaccGUIDNULL()); - char qbuff[120], *pbuff; + char *pbuff; Transaction *trans; - char trans_guid_str[GUID_ENCODING_LENGTH+1]; PGresult *result; Account *acc, *previous_acc=NULL; gboolean do_set_guid=FALSE; @@ -454,7 +932,7 @@ pgendSyncTransaction (PGBackend *be, GUID *trans_guid) GList *node, *db_splits=NULL, *engine_splits, *delete_splits=NULL; ENTER ("be=%p", be); - if (!be || !trans_guid) return; + if (!be || !trans_guid) return FALSE; /* disable callbacks into the backend, and events to GUI */ gnc_engine_suspend_events(); @@ -470,15 +948,14 @@ pgendSyncTransaction (PGBackend *be, GUID *trans_guid) } /* build the sql query to get the transaction */ - guid_to_string_buff(trans_guid, trans_guid_str); - pbuff = qbuff; + pbuff = be->buff; pbuff[0] = 0; pbuff = stpcpy (pbuff, "SELECT * FROM gncTransaction WHERE transGuid='"); - pbuff = stpcpy (pbuff, trans_guid_str); + pbuff = guid_to_string_buff(trans_guid, pbuff); pbuff = stpcpy (pbuff, "';"); - SEND_QUERY (be,qbuff, ); + SEND_QUERY (be,be->buff, FALSE); i=0; nrows=0; do { GET_RESULTS (be->connection, result); @@ -493,11 +970,14 @@ pgendSyncTransaction (PGBackend *be, GUID *trans_guid) if (1 < nrows) { + /* since the guid is primary key, this error is totally + * and completely impossible, theoretically ... */ PERR ("!!!!!!!!!!!SQL database is corrupt!!!!!!!\n" "too many transactions with GUID=%s\n", - trans_guid_str); + guid_to_string (trans_guid)); if (jrows != nrows) xaccTransCommitEdit (trans); - return; + xaccBackendSetError (&be->be, ERR_SQL_CORRUPT_DB); + return FALSE; } /* First order of business is to determine whose data is @@ -513,7 +993,7 @@ pgendSyncTransaction (PGBackend *be, GUID *trans_guid) if (!do_set_guid) { Timespec db_ts, cache_ts; - db_ts = gnc_iso8601_to_timespec (GET_DB_VAL("date_entered",j)); + db_ts = gnc_iso8601_to_timespec_local (DB_GET_VAL("date_entered",j)); cache_ts = xaccTransRetDateEnteredTS (trans); if (0 < timespec_cmp (&db_ts, &cache_ts)) { engine_data_is_newer = TRUE; @@ -528,26 +1008,23 @@ pgendSyncTransaction (PGBackend *be, GUID *trans_guid) Timespec ts; xaccTransBeginEdit (trans); if (do_set_guid) xaccTransSetGUID (trans, trans_guid); - xaccTransSetNum (trans, GET_DB_VAL("num",j)); - xaccTransSetDescription (trans, GET_DB_VAL("description",j)); - ts = gnc_iso8601_to_timespec (GET_DB_VAL("date_posted",j)); + xaccTransSetNum (trans, DB_GET_VAL("num",j)); + xaccTransSetDescription (trans, DB_GET_VAL("description",j)); + ts = gnc_iso8601_to_timespec_local (DB_GET_VAL("date_posted",j)); xaccTransSetDatePostedTS (trans, &ts); - ts = gnc_iso8601_to_timespec (GET_DB_VAL("date_entered",j)); + ts = gnc_iso8601_to_timespec_local (DB_GET_VAL("date_entered",j)); xaccTransSetDateEnteredTS (trans, &ts); - } - else - { - /* XXX hack alert -- fixme */ - PERR ("Data in the local cache is newer than the data in\n" - "\tthe database. Thus, the local data will be sent\n" - "\tto the database. This mode of operation is\n" - "\tguerenteed to clobber other user's updates\n"); - /* basically, we should use the pgend_commit_transaction - * routine instead, and in fact, 'StoreTransaction' - * pretty much shouldn't be allowed to exist in this - * framework */ - pgendStoreTransaction (be, trans); + /* hack alert -- don't set the transaction currency until + * after all splits are restored. This hack is used to set + * the reporting currency in an account. This hack will be + * obsolete when reporting currencies are removed from the + * account. */ + modity = gnc_string_to_commodity (DB_GET_VAL("currency",j)); +#if 0 + xaccTransSetCurrency (trans, + gnc_string_to_commodity (DB_GET_VAL("currency",j))); +#endif } } PQclear (result); @@ -559,21 +1036,22 @@ pgendSyncTransaction (PGBackend *be, GUID *trans_guid) /* hack alert -- not sure how to handle this case; we'll just * punt for now ... */ PERR ("no such transaction in the database. This is unexpected ...\n"); - return; + xaccBackendSetError (&be->be, ERR_SQL_MISSING_DATA); + return FALSE; } - /* if engine data was newer, we should be done at this point */ - if (TRUE == engine_data_is_newer) return; + /* if engine data was newer, we are done */ + if (TRUE == engine_data_is_newer) return TRUE; /* build the sql query the splits */ - pbuff = qbuff; + pbuff = be->buff; pbuff[0] = 0; pbuff = stpcpy (pbuff, "SELECT * FROM gncEntry WHERE transGuid='"); - pbuff = stpcpy (pbuff, trans_guid_str); + pbuff = guid_to_string_buff(trans_guid, pbuff); pbuff = stpcpy (pbuff, "';"); - SEND_QUERY (be,qbuff, ); + SEND_QUERY (be,be->buff, FALSE); i=0; nrows=0; do { GET_RESULTS (be->connection, result); @@ -595,9 +1073,9 @@ pgendSyncTransaction (PGBackend *be, GUID *trans_guid) /* --------------------------------------------- */ /* first, lets see if we've already got this one */ - PINFO ("split GUID=%s", GET_DB_VAL("entryGUID",j)); + PINFO ("split GUID=%s", DB_GET_VAL("entryGUID",j)); guid = nullguid; /* just in case the read fails ... */ - string_to_guid (GET_DB_VAL("entryGUID",j), &guid); + string_to_guid (DB_GET_VAL("entryGUID",j), &guid); s = (Split *) xaccLookupEntity (&guid, GNC_ID_SPLIT); if (!s) { @@ -607,29 +1085,29 @@ pgendSyncTransaction (PGBackend *be, GUID *trans_guid) /* next, restore some split data */ /* hack alert - not all split fields handled */ - xaccSplitSetMemo(s, GET_DB_VAL("memo",j)); - xaccSplitSetAction(s, GET_DB_VAL("action",j)); - ts = gnc_iso8601_to_timespec (GET_DB_VAL("date_reconciled",j)); + xaccSplitSetMemo(s, DB_GET_VAL("memo",j)); + xaccSplitSetAction(s, DB_GET_VAL("action",j)); + ts = gnc_iso8601_to_timespec_local (DB_GET_VAL("date_reconciled",j)); xaccSplitSetDateReconciledTS (s, &ts); - num = atoll (GET_DB_VAL("amountNum", j)); - denom = atoll (GET_DB_VAL("amountDenom", j)); + num = atoll (DB_GET_VAL("amountNum", j)); + denom = atoll (DB_GET_VAL("amountDenom", j)); amount = gnc_numeric_create (num, denom); xaccSplitSetShareAmount (s, amount); - num = atoll (GET_DB_VAL("valueNum", j)); - denom = atoll (GET_DB_VAL("valueDenom", j)); + num = atoll (DB_GET_VAL("valueNum", j)); + denom = atoll (DB_GET_VAL("valueDenom", j)); value = gnc_numeric_create (num, denom); xaccSplitSetValue (s, value); - xaccSplitSetReconcile (s, (GET_DB_VAL("reconciled", j))[0]); + xaccSplitSetReconcile (s, (DB_GET_VAL("reconciled", j))[0]); xaccTransAppendSplit (trans, s); /* --------------------------------------------- */ /* next, find the account that this split goes into */ guid = nullguid; /* just in case the read fails ... */ - string_to_guid (GET_DB_VAL("accountGUID",j), &guid); + string_to_guid (DB_GET_VAL("accountGUID",j), &guid); acc = (Account *) xaccLookupEntity (&guid, GNC_ID_ACCOUNT); if (!acc) { @@ -681,9 +1159,181 @@ pgendSyncTransaction (PGBackend *be, GUID *trans_guid) g_list_free (delete_splits); g_list_free (db_splits); + /* see note about as to why we do this set here ... */ + xaccTransSetCurrency (trans, modity); + xaccTransCommitEdit (trans); - /* reneable events to the backend and GUI */ + /* re-enable events to the backend and GUI */ + pgendEnable(be); + gnc_engine_resume_events(); + + LEAVE (" "); + return FALSE; +} + +/* ============================================================= */ +/* This routine 'synchronizes' the Transaction structure + * associated with the GUID. Data is pulled out of the database, + * the versions are compared, and updates made, if needed. + * The splits are handled as well ... + * + * hack alert unfinished, incomplete + * hack alert -- philosophically speaking, not clear that this is the + * right metaphor. Its OK to poke date into the engine, but writing + * data out to the database should make use of versioning, and this + * routine doesn't. + */ + +static void +pgendSyncTransaction (PGBackend *be, GUID *trans_guid) +{ + Transaction *trans; + gboolean engine_data_is_newer = FALSE; + + ENTER ("be=%p", be); + if (!be || !trans_guid) return; + + /* disable callbacks into the backend, and events to GUI */ + gnc_engine_suspend_events(); + pgendDisable(be); + + engine_data_is_newer = pgendCopyTransactionToEngine (be, trans_guid); + + /* if engine data was newer, we save to the db. */ + if (TRUE == engine_data_is_newer) + { + /* XXX hack alert -- fixme */ + PERR ("Data in the local cache is newer than the data in\n" + "\tthe database. Thus, the local data will be sent\n" + "\tto the database. This mode of operation is\n" + "\tguarenteed to clobber other user's updates.\n"); + + trans = (Transaction *) xaccLookupEntity (trans_guid, GNC_ID_TRANS); + + /* hack alert -- basically, we should use the pgend_commit_transaction + * routine instead, and in fact, 'StoreTransaction' + * pretty much shouldn't be allowed to exist in this + * framework */ + pgendStoreTransaction (be, trans); + return; + } + + /* re-enable events to the backend and GUI */ + pgendEnable(be); + gnc_engine_resume_events(); + + LEAVE (" "); +} + +/* ============================================================= */ +/* The pgendRunQuery() routine performs a search on the SQL database for + * all of the splits that correspond to gnc-style query, and then + * integrates them into the engine cache. It does this in several steps: + * + * 1) convert the engine style query to SQL. + * 2) run the SQL query to get the splits that satisfy the query + * 3) pull the transaction ids out of the split, and + * 4) 'synchronize' the transactions. + * + * That is, we only ever pull complete transactions out of the + * engine, and never dangling splits. This helps make sure that + * the splits always balance in a transaction; it also allows + * the ledger to operate in 'journal' mode. + * + * The pgendRunQueryHelper() routine does most of the dirty work. + * It takes as an argument an sql command that must be of the + * form "SELECT * FROM gncEntry [...]" + */ + +static gboolean +IsGuidInList (GList *list, GUID *guid) +{ + GList *node; + for (node=list; node; node=node->next) + { + if (guid_equal ((GUID *)node->data, guid)) return TRUE; + } + return FALSE; +} + +static void +pgendRunQueryHelper (PGBackend *be, const char *qstring) +{ + GUID nullguid = *(xaccGUIDNULL()); + PGresult *result; + int i, nrows; + GList *node, *xact_list = NULL; + + ENTER ("string=%s\n", qstring); + + SEND_QUERY (be, qstring, ); + + 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; jnext) + { + pgendCopyTransactionToEngine (be, (GUID *)node->data); + xaccGUIDFree (node->data); + } + g_list_free(xact_list); + + LEAVE (" "); +} + +static void +pgendRunQuery (Backend *bend, Query *q) +{ + PGBackend *be = (PGBackend *)bend; + const char * sql_query_string; + sqlQuery *sq; + + ENTER (" "); + if (!be || !q) return; + + gnc_engine_suspend_events(); + pgendDisable(be); + + /* first thing we do is convert the gnc-engine query into + * an sql string. */ + sq = sqlQuery_new(); + sql_query_string = sqlQuery_build (sq, q); + + pgendRunQueryHelper (be, sql_query_string); + + sql_Query_destroy(sq); + pgendEnable(be); gnc_engine_resume_events(); @@ -699,7 +1349,7 @@ pgendGetAllCommodities (PGBackend *be) { gnc_commodity_table *comtab = gnc_engine_commodities(); PGresult *result; - char * buff; + char * bufp; int i, nrows; ENTER ("be=%p", be); @@ -711,8 +1361,8 @@ pgendGetAllCommodities (PGBackend *be) } /* Get them ALL */ - buff = "SELECT * FROM gncCommodity;"; - SEND_QUERY (be, buff, ); + bufp = "SELECT * FROM gncCommodity;"; + SEND_QUERY (be, bufp, ); i=0; nrows=0; do { @@ -731,16 +1381,16 @@ pgendGetAllCommodities (PGBackend *be) /* first, lets see if we've already got this one */ com = gnc_commodity_table_lookup(comtab, - GET_DB_VAL("namespace",j), GET_DB_VAL("mnemonic",j)); + DB_GET_VAL("namespace",j), DB_GET_VAL("mnemonic",j)); if (com) continue; /* no we don't ... restore it */ com = gnc_commodity_new ( - GET_DB_VAL("fullname",j), - GET_DB_VAL("namespace",j), - GET_DB_VAL("mnemonic",j), - GET_DB_VAL("code",j), - atoi(GET_DB_VAL("fraction",j))); + DB_GET_VAL("fullname",j), + DB_GET_VAL("namespace",j), + DB_GET_VAL("mnemonic",j), + DB_GET_VAL("code",j), + atoi(DB_GET_VAL("fraction",j))); gnc_commodity_table_insert (comtab, com); } @@ -766,10 +1416,9 @@ pgendGetAllCommodities (PGBackend *be) static AccountGroup * pgendGetAllAccounts (PGBackend *be) { - gnc_commodity_table *comtab = gnc_engine_commodities(); PGresult *result; AccountGroup *topgrp; - char * buff; + char * bufp; int i, nrows, iacc; GUID nullguid = *(xaccGUIDNULL()); @@ -780,8 +1429,8 @@ pgendGetAllAccounts (PGBackend *be) pgendGetAllCommodities (be); /* Get them ALL */ - buff = "SELECT * FROM gncAccount;"; - SEND_QUERY (be, buff, NULL); + bufp = "SELECT * FROM gncAccount;"; + SEND_QUERY (be, bufp, NULL); i=0; nrows=0; iacc=0; topgrp = xaccMallocAccountGroup(); @@ -803,9 +1452,9 @@ pgendGetAllAccounts (PGBackend *be) GUID guid; /* first, lets see if we've already got this one */ - PINFO ("account GUID=%s", GET_DB_VAL("accountGUID",j)); + PINFO ("account GUID=%s", DB_GET_VAL("accountGUID",j)); guid = nullguid; /* just in case the read fails ... */ - string_to_guid (GET_DB_VAL("accountGUID",j), &guid); + string_to_guid (DB_GET_VAL("accountGUID",j), &guid); acc = (Account *) xaccLookupEntity (&guid, GNC_ID_ACCOUNT); if (!acc) { @@ -814,35 +1463,17 @@ pgendGetAllAccounts (PGBackend *be) xaccAccountSetGUID(acc, &guid); } - xaccAccountSetName(acc, GET_DB_VAL("accountName",j)); - xaccAccountSetDescription(acc, GET_DB_VAL("description",j)); - xaccAccountSetCode(acc, GET_DB_VAL("accountCode",j)); - xaccAccountSetType(acc, xaccAccountStringToEnum(GET_DB_VAL("type",j))); - - /* hop through a couple of hoops for the commodity */ - /* it would be nice to simplify this ... */ - { - gnc_commodity *com; - char *str, *name; - - str = g_strdup(GET_DB_VAL("commodity",j)); - name = strchr (str, ':'); - *name = 0; - name += 2; - - com = gnc_commodity_table_lookup(comtab, str, name); -PINFO ("found %p for %s-%s", com, str, name); -PINFO ("found %p is for %s", com, -gnc_commodity_get_unique_name(com)); - - xaccAccountSetCommodity(acc, com); - g_free (str); - } + xaccAccountSetName(acc, DB_GET_VAL("accountName",j)); + xaccAccountSetDescription(acc, DB_GET_VAL("description",j)); + xaccAccountSetCode(acc, DB_GET_VAL("accountCode",j)); + xaccAccountSetType(acc, xaccAccountStringToEnum(DB_GET_VAL("type",j))); + xaccAccountSetCommodity(acc, + gnc_string_to_commodity (DB_GET_VAL("commodity",j))); /* try to find the parent account */ - PINFO ("parent GUID=%s", GET_DB_VAL("parentGUID",j)); + PINFO ("parent GUID=%s", DB_GET_VAL("parentGUID",j)); guid = nullguid; /* just in case the read fails ... */ - string_to_guid (GET_DB_VAL("parentGUID",j), &guid); + string_to_guid (DB_GET_VAL("parentGUID",j), &guid); if (guid_equal(xaccGUIDNULL(), &guid)) { /* if the parent guid is null, then this @@ -862,6 +1493,7 @@ gnc_commodity_get_unique_name(com)); } xaccAccountInsertSubAccount(parent, acc); } + xaccAccountCommitEdit(acc); } } @@ -870,7 +1502,6 @@ gnc_commodity_get_unique_name(com)); i++; } while (result); - /* Mark the newly read group as saved, since the act of putting * it together will have caused it to be marked up as not-saved. */ @@ -880,6 +1511,29 @@ gnc_commodity_get_unique_name(com)); return topgrp; } +/* ============================================================= */ +/* Like the title suggests, this one sucks *all* of the + * transactions out of the database. This is a potential + * CPU and memory-burner; its use is not suggested for anything + * but single-user mode. + * + * To add injury to insult, this routine fetches in a rather + * inefficient manner, in particular, the account query. + */ + +static void +pgendGetAllTransactions (PGBackend *be, AccountGroup *grp) +{ + + gnc_engine_suspend_events(); + pgendDisable(be); + + pgendRunQueryHelper (be, "SELECT * FROM gncEntry;"); + + pgendEnable(be); + gnc_engine_resume_events(); +} + /* ============================================================= */ /* return TRUE if this appears to be a fresh, 'null' transaction */ /* it would be better is somehow we could get the gui to mark this @@ -912,6 +1566,7 @@ pgend_trans_commit_edit (Backend * bend, Transaction * trans, Transaction * oldtrans) { + char * bufp; int i, ndiffs, nsplits, rollback=0; PGBackend *be = (PGBackend *)bend; @@ -920,14 +1575,12 @@ pgend_trans_commit_edit (Backend * bend, /* lock it up so that we query and store atomically */ /* its not at all clear to me that this isn't rife with deadlocks. */ - snprintf (be->buff, be->bufflen, - "BEGIN; " - "LOCK TABLE gncTransaction IN EXCLUSIVE MODE; " - "LOCK TABLE gncEntry IN EXCLUSIVE MODE; " - "LOCK TABLE gncAccount IN EXCLUSIVE MODE; " - "LOCK TABLE gncCommodity IN EXCLUSIVE MODE; " - ); - SEND_QUERY (be,be->buff, 555); + bufp = "BEGIN; " + "LOCK TABLE gncTransaction IN EXCLUSIVE MODE; " + "LOCK TABLE gncEntry IN EXCLUSIVE MODE; " + "LOCK TABLE gncAccount IN EXCLUSIVE MODE; " + "LOCK TABLE gncCommodity IN EXCLUSIVE MODE; "; + SEND_QUERY (be,bufp, 555); FINISH_QUERY(be->connection); /* Check to see if this is a 'new' transaction, or not. @@ -957,11 +1610,13 @@ pgend_trans_commit_edit (Backend * bend, } if (rollback) { - snprintf (be->buff, be->bufflen, "ROLLBACK;"); - SEND_QUERY (be,be->buff,444); + bufp = "ROLLBACK;"; + SEND_QUERY (be,bufp,444); FINISH_QUERY(be->connection); - LEAVE ("rolled back"); + PWARN ("Some other user changed this transaction. Please\n" + "refresh your GUI, type in your changes and try again.\n" + "(old tranasction didn't match DB, edit rolled back)\n"); return 666; /* hack alert */ } } @@ -969,8 +1624,8 @@ pgend_trans_commit_edit (Backend * bend, /* if we are here, we are good to go */ pgendStoreTransactionNoLock (be, trans, FALSE); - snprintf (be->buff, be->bufflen, "COMMIT;"); - SEND_QUERY (be,be->buff,333); + bufp = "COMMIT;"; + SEND_QUERY (be,bufp,333); FINISH_QUERY(be->connection); /* hack alert -- the following code will get rid of that annoying @@ -996,6 +1651,7 @@ static int pgend_account_commit_edit (Backend * bend, Account * acct) { + char * bufp; PGBackend *be = (PGBackend *)bend; ENTER ("be=%p, acct=%p", be, acct); @@ -1003,12 +1659,11 @@ pgend_account_commit_edit (Backend * bend, /* lock it up so that we query and store atomically */ /* its not at all clear to me that this isn't rife with deadlocks. */ - snprintf (be->buff, be->bufflen, - "BEGIN; " - "LOCK TABLE gncAccount IN EXCLUSIVE MODE; " - "LOCK TABLE gncCommodity IN EXCLUSIVE MODE; " - ); - SEND_QUERY (be,be->buff, 555); + bufp = "BEGIN; " + "LOCK TABLE gncAccount IN EXCLUSIVE MODE; " + "LOCK TABLE gncCommodity IN EXCLUSIVE MODE; "; + + SEND_QUERY (be,bufp, 555); FINISH_QUERY(be->connection); /* hack alert -- we aren't comparing old to new, @@ -1016,8 +1671,8 @@ pgend_account_commit_edit (Backend * bend, * we're clobbering someone elses changes. */ pgendStoreAccountNoLock (be, acct, FALSE); - snprintf (be->buff, be->bufflen, "COMMIT;"); - SEND_QUERY (be,be->buff,333); + bufp = "COMMIT;"; + SEND_QUERY (be,bufp,333); FINISH_QUERY(be->connection); /* Mark this up so that we don't get that annoying gui dialog @@ -1032,6 +1687,167 @@ pgend_account_commit_edit (Backend * bend, /* ============================================================= */ +static const char * +pgendSessionGetMode (PGBackend *be) +{ + switch (be->session_mode) + { + case MODE_SINGLE_FILE: + return "SINGLE-FILE"; + case MODE_SINGLE_UPDATE: + return "SINGLE-UPDATE"; + case MODE_POLL: + return "POLL"; + case MODE_EVENT: + return "EVENT"; + default: + } + return "ERROR"; +} + +/* ============================================================= */ +/* Determine whether we can start a session of the desired type. + * The logic used is as follows: + * -- if there is any session at all, and we want single + * (exclusive) access, then fail. + * -- if we want any kind of session, and there is a single + * (exclusive) session going, then fail. + * -- otherwise, suceed. + * Return TRUE if we can get a session. + * + * This routine does not lock, but may be used inside a + * test-n-set atomic operation. + */ + +static gboolean +pgendSessionCanStart (PGBackend *be) +{ + gboolean retval = TRUE; + PGresult *result; + int i, nrows; + char *p; + + ENTER (" "); + /* Find out if there are any open sessions. + * If 'time_off' is infinity, then user hasn't logged off yet */ + p = "SELECT * FROM gncSession " + "WHERE time_off='INFINITY';"; + SEND_QUERY (be,p, FALSE); + + 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; jsession_mode) || + (MODE_SINGLE_UPDATE == be->session_mode) || + (0 == strcasecmp (mode, "SINGLE-FILE")) || + (0 == strcasecmp (mode, "SINGLE-UPDATE"))) + { + char * hostname = DB_GET_VAL("hostname", j); + char * username = DB_GET_VAL("login_name",j); + char * gecos = DB_GET_VAL("gecos",j); + char * datestr = DB_GET_VAL("time_on", j); + + PWARN ("This database is already opened by \n" + "\t%s@%s (%s) in mode %s on %s \n", + username, hostname, gecos, mode, datestr); + + retval = FALSE; + } + } + } + PQclear (result); + i++; + } while (result); + + LEAVE (" "); + return retval; +} + + +/* ============================================================= */ +/* Determine whether a valid session could be obtained. + * Return TRUE if we have a session + * This routine is implemented attomically as a test-n-set. + */ + +static gboolean +pgendSessionValidate (PGBackend *be) +{ + gboolean retval = FALSE; + char *p; + ENTER(" "); + + if (MODE_NONE == be->session_mode) return FALSE; + + /* Lock it up so that we test-n-set atomically + * i.e. we want to avoid a race condition when testing + * for the single-user session. + */ + p = "BEGIN;" + "LOCK TABLE gncSession IN EXCLUSIVE MODE; "; + SEND_QUERY (be,p, FALSE); + FINISH_QUERY(be->connection); + + /* check to see if we can start a session of the desired type. */ + if (FALSE == pgendSessionCanStart (be)) + { + /* this error should be treated just like the + * file-lock error from the GUI perspective */ + xaccBackendSetError (&be->be, ERR_SQL_BUSY); + retval = FALSE; + } else { + + /* make note of the session. */ + be->sessionGuid = xaccGUIDMalloc(); + guid_new (be->sessionGuid); + pgendStoreOneSessionOnly (be, (void *)-1, SQL_INSERT); + retval = TRUE; + } + + p = "COMMIT;"; + SEND_QUERY (be,p, FALSE); + FINISH_QUERY(be->connection); + + LEAVE(" "); + return retval; +} + +/* ============================================================= */ +/* log end of session in the database. */ + +static void +pgendSessionEnd (PGBackend *be) +{ + char *p; + + if (!be->sessionGuid) return; + + p = be->buff; *p=0; + p = stpcpy (p, "UPDATE gncSession SET time_off='NOW' " + "WHERE sessionGuid='"); + p = guid_to_string_buff (be->sessionGuid, p); + p = stpcpy (p, "';"); + + SEND_QUERY (be,be->buff, ); + FINISH_QUERY(be->connection); + + xaccGUIDFree (be->sessionGuid); be->sessionGuid = NULL; +} + +/* ============================================================= */ + static void pgend_session_end (Backend *bend) { @@ -1043,6 +1859,9 @@ pgend_session_end (Backend *bend) /* prevent further callbacks into backend */ pgendDisable(be); + /* note the logoff time in the session directory */ + pgendSessionEnd (be); + /* disconnect from the backend */ if(be->connection) PQfinish (be->connection); be->connection = 0; @@ -1051,25 +1870,80 @@ pgend_session_end (Backend *bend) if (be->portno) { g_free(be->portno); be->portno = NULL; } if (be->hostname) { g_free(be->hostname); be->hostname = NULL; } + sqlBuilder_destroy (be->builder); be->builder = NULL; + g_free (be->buff); be->buff = NULL; + LEAVE("be=%p", be); } +/* ============================================================= */ +/* the poll & event style load only loads accounts, never the + * transactions. */ + +static AccountGroup * +pgend_book_load_poll (Backend *bend) +{ + AccountGroup *grp; + PGBackend *be = (PGBackend *)bend; + if (!be) return NULL; + + /* don't send events to GUI, don't accept callaback to backend */ + gnc_engine_suspend_events(); + pgendDisable(be); + + grp = pgendGetAllAccounts (be); + pgendGroupGetAllCheckpoints (be, grp); + + /* re-enable events */ + pgendEnable(be); + gnc_engine_resume_events(); + + return grp; +} + +/* ============================================================= */ +/* The single-user mode loads all transactions. Doesn't bother + * with checkpoints */ + +static AccountGroup * +pgend_book_load_single (Backend *bend) +{ + AccountGroup *grp; + PGBackend *be = (PGBackend *)bend; + if (!be) return NULL; + + /* don't send events to GUI, don't accept callaback to backend */ + gnc_engine_suspend_events(); + pgendDisable(be); + + grp = pgendGetAllAccounts (be); + pgendGetAllTransactions (be, grp); + + /* re-enable events */ + pgendEnable(be); + gnc_engine_resume_events(); + + return grp; +} + /* ============================================================= */ static void pgend_session_begin (GNCBook *sess, const char * sessionid) { + int rc; PGBackend *be; char *url, *start, *end; + char * bufp; if (!sess) return; be = (PGBackend *) xaccGNCBookGetBackend (sess); ENTER("be=%p, sessionid=%s", be, sessionid); - /* close any dangling sessions from before */ + /* close any dangling sessions from before; reinitialize */ pgend_session_end ((Backend *) be); - pgendEnable(be); /* re-enable after session end */ + pgendInit (be); /* connect to a bogus database ... */ /* Parse the sessionid for the hostname, port number and db name. @@ -1138,124 +2012,81 @@ pgend_session_begin (GNCBook *sess, const char * sessionid) "\t%s", be->dbName, PQerrorMessage(be->connection)); PQfinish (be->connection); + xaccBackendSetError (&be->be, ERR_SQL_CANT_CONNECT); return; } // DEBUGCMD (PQtrace(be->connection, stderr)); /* set the datestyle to something we can parse */ - snprintf (be->buff, be->bufflen, "SET DATESTYLE='ISO';"); - SEND_QUERY (be,be->buff, ); + bufp = "SET DATESTYLE='ISO';"; + SEND_QUERY (be,bufp, ); FINISH_QUERY(be->connection); - LEAVE("be=%p, sessionid=%s", be, sessionid); -} + /* OK, lets see if we can get a valid session */ + /* hack alert -- we hard-code the access mode here, + * but it should be user-adjustable. */ + be->session_mode = MODE_SINGLE_UPDATE; + rc = pgendSessionValidate (be); -/* ============================================================= */ - -static AccountGroup * -pgend_book_load (Backend *bend) -{ - AccountGroup *grp; - PGBackend *be = (PGBackend *)bend; - if (!be) return NULL; - - /* don't send events to GUI, don't accept callaback to backend */ - gnc_engine_suspend_events(); - pgendDisable(be); - - grp = pgendGetAllAccounts (be); - - /* re-enable events */ - pgendEnable(be); - gnc_engine_resume_events(); - - return grp; -} - -/* ============================================================= */ -/* This routine performs a search on the SQL database for all of - * the splits that correspond to gnc-style query, and then integrates - * them into the engine cache. It does this in several steps: - * - * 1) convert the engine style query to SQL. - * 2) run the SQL query to get the splits that satisfy the query - * 3) pull the transaction ids out of the split, and - * 4) 'synchronize' the transactions. - * - * That is, we only ever pull complete transactions out of the - * engine, and never dangling splits. This helps make sure that - * the splits always balance in a transaction; it also allows - * the ledger to operate in 'journal' mode. - * - */ - -static void -pgendRunQuery (Backend *bend, Query *q) -{ - PGBackend *be = (PGBackend *)bend; - GUID nullguid = *(xaccGUIDNULL()); - const char * sql_query_string; - sqlQuery *sq; - PGresult *result; - int i, nrows; - GList *node, *xact_list = NULL; - - ENTER (" "); - if (!be || !q) return; - - gnc_engine_suspend_events(); - pgendDisable(be); - - /* first thing we do is convert the gnc-engine query into - * an sql string. */ - sq = sqlQuery_new(); - sql_query_string = sqlQuery_build (sq, q); - PINFO ("string=%s\n", sql_query_string); - - SEND_QUERY (be,sql_query_string, ); - - sql_Query_destroy(sq); - - 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; jnext) + /* set up pointers for appropriate behaviour */ + /* In single mode, we load all transactions right away. + * and we never have to query the database. + */ + if (rc) { - pgendSyncTransaction (be, (GUID *)node->data); - xaccGUIDFree (node->data); + switch (be->session_mode) + { + case MODE_SINGLE_FILE: + pgendEnable(be); + be->be.book_load = pgend_book_load_single; + be->be.account_begin_edit = NULL; + be->be.account_commit_edit = NULL; + be->be.trans_begin_edit = NULL; + be->be.trans_commit_edit = NULL; + be->be.trans_rollback_edit = NULL; + be->be.run_query = NULL; + be->be.sync = pgendSyncSingleFile; + PWARN ("MODE_SINGLE_FILE is experimental"); + break; + + case MODE_SINGLE_UPDATE: + pgendEnable(be); + be->be.book_load = pgend_book_load_single; + be->be.account_begin_edit = NULL; + be->be.account_commit_edit = pgend_account_commit_edit; + be->be.trans_begin_edit = NULL; + be->be.trans_commit_edit = pgend_trans_commit_edit; + be->be.trans_rollback_edit = NULL; + be->be.run_query = NULL; + be->be.sync = pgendSync; + PWARN ("MODE_SINGLE_UPDATE is experimental"); + break; + + case MODE_POLL: + pgendEnable(be); + be->be.book_load = pgend_book_load_poll; + be->be.account_begin_edit = NULL; + be->be.account_commit_edit = pgend_account_commit_edit; + be->be.trans_begin_edit = NULL; + be->be.trans_commit_edit = pgend_trans_commit_edit; + be->be.trans_rollback_edit = NULL; + be->be.run_query = pgendRunQuery; + be->be.sync = pgendSync; + PWARN ("MODE_EVENT is experimental"); + break; + + case MODE_EVENT: + PERR ("MODE_EVENT is unimplemented"); + break; + + default: + PERR ("bad mode specified"); + break; + } } - g_list_free(xact_list); - pgendEnable(be); - gnc_engine_resume_events(); - - LEAVE (" "); + LEAVE("be=%p, sessionid=%s", be, sessionid); } /* ============================================================= */ @@ -1269,13 +2100,24 @@ pgendDisable (PGBackend *be) } be->nest_count ++; PINFO("nest count=%d", be->nest_count); - be->be.account_begin_edit = NULL; + if (1 < be->nest_count) return; + + /* save hooks */ + be->snr.account_begin_edit = be->be.account_begin_edit; + be->snr.account_commit_edit = be->be.account_commit_edit; + be->snr.trans_begin_edit = be->be.trans_begin_edit; + be->snr.trans_commit_edit = be->be.trans_commit_edit; + be->snr.trans_rollback_edit = be->be.trans_rollback_edit; + be->snr.run_query = be->be.run_query; + be->snr.sync = be->be.sync; + + be->be.account_begin_edit = NULL; be->be.account_commit_edit = NULL; - be->be.trans_begin_edit = NULL; - be->be.trans_commit_edit = NULL; + be->be.trans_begin_edit = NULL; + be->be.trans_commit_edit = NULL; be->be.trans_rollback_edit = NULL; - be->be.run_query = NULL; - be->be.sync = NULL; + be->be.run_query = NULL; + be->be.sync = NULL; } /* ============================================================= */ @@ -1290,38 +2132,42 @@ pgendEnable (PGBackend *be) be->nest_count --; PINFO("nest count=%d", be->nest_count); if (be->nest_count) return; - be->be.account_begin_edit = NULL; - be->be.account_commit_edit = pgend_account_commit_edit; - be->be.trans_begin_edit = NULL; - be->be.trans_commit_edit = pgend_trans_commit_edit; - be->be.trans_rollback_edit = NULL; - be->be.run_query = pgendRunQuery; - be->be.sync = pgendSync; + + /* restore hooks */ + be->be.account_begin_edit = be->snr.account_begin_edit; + be->be.account_commit_edit = be->snr.account_commit_edit; + be->be.trans_begin_edit = be->snr.trans_begin_edit; + be->be.trans_commit_edit = be->snr.trans_commit_edit; + be->be.trans_rollback_edit = be->snr.trans_rollback_edit; + be->be.run_query = be->snr.run_query; + be->be.sync = be->snr.sync; + } /* ============================================================= */ -/* hack alert -- this is the query buffer, it can be overflowed. - * we need to make it dynamic sized. On the other hand, Postgres - * rejects queries longer than 8192 bytes,(according to the - * documentation) so... - */ -#define QBUFSIZE 16350 - -Backend * -pgendNew (void) +static void +pgendInit (PGBackend *be) { - PGBackend *be; - - be = (PGBackend *) g_malloc (sizeof (PGBackend)); + /* access mode */ + be->session_mode = MODE_NONE; + be->sessionGuid = NULL; /* generic backend handlers */ be->be.book_begin = pgend_session_begin; - be->be.book_load = pgend_book_load; + be->be.book_load = NULL; be->be.book_end = pgend_session_end; - be->nest_count = 1; - pgendEnable(be); + be->be.account_begin_edit = NULL; + be->be.account_commit_edit = NULL; + be->be.trans_begin_edit = NULL; + be->be.trans_commit_edit = NULL; + be->be.trans_rollback_edit = NULL; + be->be.run_query = NULL; + be->be.sync = NULL; + + be->nest_count = 0; + pgendDisable(be); be->be.last_err = ERR_BACKEND_NO_ERR; @@ -1335,7 +2181,16 @@ pgendNew (void) be->buff = g_malloc (QBUFSIZE); be->bufflen = QBUFSIZE; +} +/* ============================================================= */ + +Backend * +pgendNew (void) +{ + PGBackend *be; + be = (PGBackend *) g_malloc (sizeof (PGBackend)); + pgendInit (be); return (Backend *) be; } diff --git a/src/engine/sql/PostgresBackend.h b/src/engine/sql/PostgresBackend.h index dd2fc28042..9e70de3992 100644 --- a/src/engine/sql/PostgresBackend.h +++ b/src/engine/sql/PostgresBackend.h @@ -1,8 +1,12 @@ /* + * FILE: * PostgresBackend.h * + * FUNCTION: * Implements the callbacks for the postgres backend. - * + * + * HISTORY: + * Copyright (c) 2000, 2001 Linas Vepstas */ @@ -11,9 +15,24 @@ typedef struct _pgend PGBackend; +typedef enum { + MODE_NONE = 0, + MODE_SINGLE_FILE =1, + MODE_SINGLE_UPDATE, + MODE_POLL, + MODE_EVENT +} AccessMode; + struct _pgend { Backend be; + /* snr is used only for temporarily saving hook values */ + Backend snr; + + /* session mode */ + AccessMode session_mode; + GUID *sessionGuid; + /* sql query compiler */ sqlBuilder *builder; @@ -36,3 +55,30 @@ struct _pgend { */ Backend * pgendNew (void); + +/* -------------------------------------------------------- */ +/* The balance checkpoint structure is used to store partial, + * running balances. The balances are correct for the checkpoint + * date shown. The commodity indicates what commodity the + * balances are valued in (they need not be in the same + * commodity as the account) + */ + +/* the MIN_CHECKPOINT_COUNT value is the number of splits that + * each checkpoint will handle, on avergage. 30 seems like a good + * number. The number of splits in a checkpoint will vary; + * checkpoints can onmly occur in between entry dates, so a + * bunch of entries with the same date will go into the same + * checkpoint (and there might be an arbitrarily large number of these) + */ +#define MIN_CHECKPOINT_COUNT 3 + +typedef struct _checkpoint { + const GUID *account_guid; + const char * commodity; + Timespec datetime; + gint64 balance; + gint64 cleared_balance; + gint64 reconciled_balance; +} Checkpoint; + diff --git a/src/engine/sql/README b/src/engine/sql/README index 76a4edccc7..6c71780cfb 100644 --- a/src/engine/sql/README +++ b/src/engine/sql/README @@ -18,7 +18,7 @@ A) run the script gnc-init.sh B) edit gnc-book.c and #define SQLHACK C) edit src/Makefile.am and add engine/sql/libgnc_postgres.la and -lpq to LDADD -D) cd src/engine/sql; m4 table.m4 > tmp.c; make +D) cd src/engine/sql; m4 table.m4 > autogen.c; make E) cd ../../..; make; make install @@ -36,6 +36,7 @@ To Bo Done ---------- -- Implement acount and transaction deletion. Deleting an account or transaction doesn't remove it from the database. + (done with split delete) -- Implement logging history in the sql server. i.e. save the old copies of stuff in log tables. Make the username part of the @@ -44,6 +45,9 @@ To Bo Done -- Implement gui to ask user for username/password to log onto the server. +-- let all attached client receive update events via sql LISTEN/NOTIFY + events. + -- Implement various advanced database features, such as checking the user's permission to view/edit account by account ... (hmmm this done by the dbadmin... using sql commands... which means if user @@ -72,10 +76,9 @@ To Bo Done -- finish implementing pgendAccountGroupSync -- store account balances in database. This will be tricky ... - -- need to write stored proceedures to recalc split balances - -- need to modify engine and maybe gui + -- split query gets hard ... -- urlencode all strings (escape single-quote mark) --- check multiuser operation +-- review multiuser operation for correctness -- allow user to enter url in gui dialog -- handle kvp frames -- implement account commit edit (actually, the check&rollback part) @@ -89,5 +92,7 @@ To Bo Done them out. But right now, this is brroken. In particular, the use of xaccGrouparkSaved screws up some status bits ... +-- review & match up against docs at + http://www.lupercalia.net/gnc-db/ diff --git a/src/engine/sql/builder.c b/src/engine/sql/builder.c index cf7f585410..328aefc26f 100644 --- a/src/engine/sql/builder.c +++ b/src/engine/sql/builder.c @@ -19,6 +19,7 @@ #include #include +#include "date.h" #include "builder.h" #include "gnc-engine-util.h" @@ -182,10 +183,9 @@ void sqlBuild_Set_GUID (sqlBuilder *b, const char *tag, const GUID *val) { if (val) { - char *guid; - guid = guid_to_string(val); - sqlBuild_Set_Str (b, tag, guid); - g_free (guid); + char guid_str[GUID_ENCODING_LENGTH+1]; + guid_to_string_buff(val, guid_str); + sqlBuild_Set_Str (b, tag, guid_str); } else { /* if a SELECT statement is being built, then val may be null */ sqlBuild_Set_Str (b, tag, ""); @@ -197,28 +197,8 @@ sqlBuild_Set_GUID (sqlBuilder *b, const char *tag, const GUID *val) void sqlBuild_Set_Date (sqlBuilder *b, const char *tag, Timespec ts) { - int tz_hour, tz_min; - char cyn; char buf[512]; - time_t tmp; - struct tm parsed; - tmp = ts.tv_sec; - localtime_r(&tmp, &parsed); - - tz_hour = timezone/3600; - tz_min = (timezone - 3600*tz_hour)/60; - if (0>tz_min) { tz_min +=60; tz_hour --; } - - /* we also have to print the sign by hand, to work around a bug - * in the glibc 2.1.3 printf (where %+02d fails to zero-pad) - */ - cyn = '+'; - if (0>tz_hour) { cyn = '-'; tz_hour = -tz_hour; } - - snprintf (buf, 512, "%4d-%02d-%02d %02d:%02d:%02d.%06ld %c%02d%02d", - parsed.tm_year+1900, parsed.tm_mon+1, parsed.tm_mday, - parsed.tm_hour, parsed.tm_min, parsed.tm_sec, - ts.tv_nsec/1000, cyn, tz_hour, tz_min); + gnc_timespec_to_iso8601_buff (ts, buf); sqlBuild_Set_Str (b, tag, buf); } @@ -306,10 +286,9 @@ sqlBuild_Where_Str (sqlBuilder *b, const char *tag, const char *val) void sqlBuild_Where_GUID (sqlBuilder *b, const char *tag, const GUID *val) { - char *guid; - guid = guid_to_string(val); - sqlBuild_Where_Str (b, tag, guid); - g_free (guid); + char guid_str[GUID_ENCODING_LENGTH+1]; + guid_to_string_buff(val, guid_str); + sqlBuild_Where_Str (b, tag, guid_str); } /* ================================================ */ diff --git a/src/engine/sql/design.txt b/src/engine/sql/design.txt new file mode 100644 index 0000000000..0099f28776 --- /dev/null +++ b/src/engine/sql/design.txt @@ -0,0 +1,55 @@ + +SQL Tables +---------- +These mostly parallel the data structures in the gnc engine. +See gnc-init.txt for more info. + + +Session Table, Session Modes +---------------------------- +There are three basic modes for accessing the database: +"Single User", "Polled Multi-User" and "Event-Driven Multi-User" +The session table in the database indicates which mode the database is +functioning. + +-- "Single User File Mode" -- + Only one user can have access to the database at a time. The + database is used as a glorified file: engine data is 'saved' to + the database only when the user selects 'save' from the GUI + dialog. This mode exists because it is easy to implement, easy + to debug, and has reasonable demands on the database for small + datasets. + + This mode is guarenteed to clobber any sort of changes made by + other users (in the same way that multiple file writers clobber + each other). Thus, this mode is mutually exclusive of any other, + and te engine prevents more than one concurrent user. + + +-- "Single User Update Mode" -- + Similar to the "Single User File Mode", except that updates + are stored in the DB as they are made (thus eliminating the + need for a big 'save' at the end of a session). This mode is + more robust in that there is minimal/no data loss in the event + of a crash. + +-- "Multi-User Polled" -- + Multiple users are assumed, Gnucash polls the database to detect + changes in the data. Partially Implemented. + + +-- "Multi-User Events" -- + Gnucash uses the SQL LISTEN/NOTIFY async message delivery routines. + Not implemented. + + +Session Design Notes +-------------------- +The pgendSyncSingleFile() subroutine performs the equivalent of 'file +save'. Note that it does this by deleting the entire contents of the +database, and then writing out the entire contents of the engine. It +works this way (needs to work this way) in order to make sure that +deleted transactions,etc. are really deleted from the database. This +is because in this mode, the backend never finds out about deletions. +If you want incremental deletion, then use the 'Single Update' mode. + diff --git a/src/engine/sql/gnc-init.sql b/src/engine/sql/gnc-init.sql index 1f84965418..fe99ee989e 100644 --- a/src/engine/sql/gnc-init.sql +++ b/src/engine/sql/gnc-init.sql @@ -1,17 +1,35 @@ --- these tables roughly mirror the c structs in --- TransactionP.h, AccountP.h --- these tables are hand-built, but maybe they should be --- autobuilt with the m4 macros ... +-- FILE: +-- gnc-init.sql +-- +-- FUNCTION: +-- Define the tables needed to initialize a new GnuCash database +-- +-- These tables roughly mirror the c structs in +-- TransactionP.h, AccountP.h, gnc-commodity.c +-- Please refer to the C files to get the right level of documentation. +-- +-- These tables are specifically designed for the +-- postgres database server, but are hopefull relatively portable. +-- +-- These tables are hand-built, but maybe they should be +-- auto-built with the m4 macros ... +-- +-- HISTORY: +-- Copyright (C) 2000, 2001 Linas Vepstas +-- -- Commodity structure +-- Store currency, security types. Namespace includes +-- ISO4217 for currencies, NASDAQ, AMEX, NYSE, EUREX for +-- stocks. See the C documentation for details. DROP TABLE gncCommodity; CREATE TABLE gncCommodity ( commodity TEXT PRIMARY KEY, fullname TEXT, - namespace TEXT, - mnemonic TEXT, + namespace TEXT NOT NULL, + mnemonic TEXT NOT NULL, code TEXT, fraction INT DEFAULT '100' ); @@ -20,20 +38,18 @@ CREATE TABLE gncCommodity ( -- guid. There is no supports for Groups in this schema. -- (there seems to be no strong need to have groups in the DB.) -- --- hack alert -- add the kvp frames, the currency tables, - -- the current balances, etc +-- hack alert -- add kvp frames, DROP TABLE gncAccount; CREATE TABLE gncAccount ( accountGuid CHAR(32) PRIMARY KEY, - parentGuid CHAR(32), --- childrenGuid CHAR(32), - accountName TEXT DEFAULT 'xoxo', + parentGuid CHAR(32) NOT NULL, + accountName TEXT NOT NULL CHECK (accountName <> ''), accountCode TEXT, description TEXT, notes TEXT, - type TEXT, - commodity TEXT + type TEXT NOT NULL, + commodity TEXT NOT NULL CHECK (commodity <>'') ); -- CREATE INDEX gncAccount_pg_idx ON gncAccount (parentGuid); @@ -48,54 +64,71 @@ CREATE TABLE gncTransaction ( date_posted DATETIME, num TEXT, description TEXT, - currency TEXT + currency TEXT NOT NULL CHECK (currency <> '') ); +CREATE INDEX gncTransaction_posted_idx ON gncTransaction (date_posted); + -- a gncEntry is what we call 'Split' elsewhere in the engine +-- Here, we call it a 'journal entry' DROP TABLE gncEntry; CREATE TABLE gncEntry ( entryGuid CHAR(32) PRIMARY KEY, - accountGuid CHAR(32), - transGuid CHAR(32), + accountGuid CHAR(32) NOT NULL, + transGuid CHAR(32) NOT NULL, memo TEXT, action TEXT, reconciled CHAR DEFAULT 'n', date_reconciled DATETIME, amountNum INT8 DEFAULT '0', - amountDenom INT8 DEFAULT '100', + amountDenom INT4 DEFAULT '100', valueNum INT8 DEFAULT '0', - valueDenom INT8 DEFAULT '100' + valueDenom INT4 DEFAULT '100' ); CREATE INDEX gncEntry_acc_idx ON gncEntry (accountGuid); CREATE INDEX gncEntry_trn_idx ON gncEntry (transGuid); --- populate with some bogus data --- INSERT INTO gncAccount (accountGuid, parentGuid, accountName, type, description) VALUES --- ('9101752f77d6615dcdc0fffe24f0de24', --- '00000000000000000000000000000000', --- 'Swipe Trading Account', --- 'STOCK', --- 'Swipe Brokers Margin Account'); --- INSERT INTO gncAccount (accountGuid, parentGuid, accountName, type, description) VALUES --- ('0d7c1819693c85c16d5556b37f6caf9d', --- '00000000000000000000000000000000', --- 'Stock Dividends & Distributions', --- 'BANK', --- 'Stock Dividends & Distributions'); --- --- INSERT INTO gncTransaction (transGuid, date_entered, --- date_posted, num, description) VALUES --- ('2ebc806e72c17bdc3c2c4e964b82eff8', --- '1998-07-01 11:00:00.345678 -0500', --- '1998-07-02 11:00:00.678945 -0531', --- '101aaa', --- 'Interest at 3.5%'); --- --- INSERT INTO gncEntry (entryGuid, memo, reconciled, amountNum,valueNum) VALUES --- ('d56a1146e414a30d6f2e251af2075f71', --- 'this is a split memo', --- 'Y', --- 700000, --- 700000); +-- The checkpoint table provides balance information +-- The balance is provided in the indicated currency; +-- this allows the potential of maintaining balance information +-- in multiple currencies. +-- (e.g. report stock account balances in shares of stock, +-- and in dollars) + +DROP TABLE gncCheckpoint; +CREATE TABLE gncCheckpoint ( + accountGuid CHAR(32) NOT NULL, + date_xpoint DATETIME NOT NULL, + commodity TEXT NOT NULL CHECK (commodity <>''), + balance INT8 DEFAULT '0', + cleared_balance INT8 DEFAULT '0', + reconciled_balance INT8 DEFAULT '0', + + PRIMARY KEY (accountGuid, date_xpoint, commodity) +); + + +-- The session directory serves several purposes. First and formost, +-- it notes the database access type. There are three modes: +-- o "Single User" -- Only one user can have access to the database +-- at a time. +-- o "Multi-User Polled" -- multiple users +-- o "Muilti-User Event Driven" +-- See Design.txt for more info. +-- Note that a client can lie about its identity, sign-on time, etc. +-- so these records aren't really sufficient for a true audit. + +DROP TABLE gncSession; +CREATE TABLE gncSession ( + sessionGuid CHAR(32) PRIMARY KEY, + session_mode CHAR(16) NOT NULL, + hostname TEXT, + login_name TEXT, + gecos TEXT, + time_on DATETIME NOT NULL, + time_off DATETIME NOT NULL DEFAULT 'INFINITY' +); + + diff --git a/src/engine/sql/gncquery.c b/src/engine/sql/gncquery.c index f42d5f8816..22877d0a6d 100644 --- a/src/engine/sql/gncquery.c +++ b/src/engine/sql/gncquery.c @@ -108,7 +108,6 @@ sqlQuery_build (sqlQuery*sq, Query *q) { int got_more = 0; GList *acct; - char guid_str[GUID_ENCODING_LENGTH+1]; PINFO("term is PR_ACCOUNT"); @@ -119,9 +118,8 @@ sqlQuery_build (sqlQuery*sq, Query *q) if (got_more) sq->pq = stpcpy(sq->pq, " AND "); got_more = 1; - guid_to_string_buff ((GUID*) acct->data, guid_str); sq->pq = stpcpy(sq->pq, "accountguid='"); - sq->pq = stpcpy(sq->pq, guid_str); + sq->pq = guid_to_string_buff ((GUID*) acct->data, sq->pq); sq->pq = stpcpy(sq->pq, "'"); } break; diff --git a/src/engine/sql/table.m4 b/src/engine/sql/table.m4 index 10a4fea174..a9f2ca78f0 100644 --- a/src/engine/sql/table.m4 +++ b/src/engine/sql/table.m4 @@ -5,7 +5,7 @@ changecom(`/*', `*/') /* data dictionary for the gnucash tables */ /* sql table description and manipulation macros */ -define(`account', `gncAccount, Account, +define(`account', `gncAccount, Account, Account, accountName, , char *, xaccAccountGetName(ptr), accountCode, , char *, xaccAccountGetCode(ptr), description, , char *, xaccAccountGetDescription(ptr), @@ -17,7 +17,7 @@ define(`account', `gncAccount, Account, ') -define(`split', `gncEntry, Split, +define(`split', `gncEntry, Split, Split, accountGUID, , GUID *, xaccAccountGetGUID(xaccSplitGetAccount(ptr)), transGUID, , GUID *, xaccTransGetGUID(xaccSplitGetParent(ptr)), memo, , char *, xaccSplitGetMemo(ptr), @@ -38,7 +38,7 @@ define(`split', `gncEntry, Split, /* date_entered, , Timespec, xaccTransRetDateEnteredTS(ptr), */ /* as one might have guessed */ -define(`transaction', `gncTransaction, Transaction, +define(`transaction', `gncTransaction, Transaction, Transaction, num, , char *, xaccTransGetNum(ptr), description, , char *, xaccTransGetDescription(ptr), currency, , char *, gnc_commodity_get_unique_name(xaccTransGetCurrency(ptr)), @@ -48,7 +48,7 @@ define(`transaction', `gncTransaction, Transaction, ') -define(`modity', `gncCommodity, gnc_commodity, +define(`modity', `gncCommodity, Commodity, gnc_commodity, namespace, , char *, gnc_commodity_get_namespace(ptr), fullname, , char *, gnc_commodity_get_fullname(ptr), mnemonic, , char *, gnc_commodity_get_mnemonic(ptr), @@ -58,12 +58,33 @@ define(`modity', `gncCommodity, gnc_commodity, ') +define(`checkpoint', `gncCheckpoint, Checkpoint, Checkpoint, + balance, , int64, ptr->balance, + cleared_balance, , int64, ptr->cleared_balance, + reconciled_balance, , int64, ptr->reconciled_balance, + date_xpoint, , Timespec, ptr->datetime, + commodity, , char *, ptr->commodity, + accountGuid, , GUID *, ptr->account_guid, + ') + + +define(`session', `gncSession, Session, void, + session_mode, , char *, pgendSessionGetMode(be), + hostname, , char *, pgendGetHostname(be), + login_name, , char *, pgendGetUsername(be), + gecos, , char *, pgendGetUserGecos(be), + time_on, , now, "NOW", + time_off, , now, "INFINITY", + sessionGUID, KEY, GUID *, be->sessionGuid, + ') + /* ------------------------------------------------------- */ /* symbolic names for the table accessors */ define(`tablename', $1) -define(`xacc_type', $2) +define(`func_name', $2) +define(`xacc_type', $3) -define(`firstrec', `shift(shift($@))') +define(`firstrec', `shift(shift(shift($@)))') define(`nextrec', `shift(shift(shift(shift($@))))') /* -------- */ @@ -123,7 +144,7 @@ define(`store_one_only', */ static void -pgendStoreOne`'xacc_type($@)`'Only (PGBackend *be, +pgendStoreOne`'func_name($@)`'Only (PGBackend *be, xacc_type($@) *ptr, sqlBuild_QType update) { @@ -155,7 +176,7 @@ define(`compare_one_only', */ static int -pgendCompareOne`'xacc_type($@)`'Only (PGBackend *be, +pgendCompareOne`'func_name($@)`'Only (PGBackend *be, xacc_type($@) *ptr) { const char *buf; @@ -192,14 +213,44 @@ pgendCompareOne`'xacc_type($@)`'Only (PGBackend *be, ') +define(`put_one_only', +` +/* ------------------------------------------------------ */ +/* This routine inserts or updates, as appropriate + * It does not do any traversals, it does not lock. + * It just updates. + */ + +static void +pgendPutOne`'func_name($@)`'Only (PGBackend *be, + xacc_type($@) *ptr) +{ + int ndiffs; + ndiffs = pgendCompareOne`'func_name($@)`'Only (be, ptr); + + /* update the record if there are differences ... */ + if (0ndiffs) pgendStoreOne`'func_name($@)`'Only (be, ptr, SQL_INSERT); +} + +') + /* ------------------------------------------------------- */ divert store_one_only(account) -store_one_only(transaction) -store_one_only(split) +store_one_only(checkpoint) store_one_only(modity) +store_one_only(session) +store_one_only(split) +store_one_only(transaction) compare_one_only(account) -compare_one_only(transaction) -compare_one_only(split) compare_one_only(modity) +compare_one_only(split) +compare_one_only(transaction) + +put_one_only(account) +put_one_only(modity) +put_one_only(split) +put_one_only(transaction)