/********************************************************************\ * PostgresBackend.c -- implements postgres backend * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License* * along with this program; if not, contact: * * * * Free Software Foundation Voice: +1-617-542-5942 * * 59 Temple Place - Suite 330 Fax: +1-617-542-2652 * * Boston, MA 02111-1307, USA gnu@gnu.org * \********************************************************************/ /* * FILE: * PostgresBackend.c * * FUNCTION: * Implements the callbacks for the Postgres backend. * The SINGLE modes should work and are more-or-less feature complete. * The multi-user modes are mostly implemented, and possibly useful. * * HISTORY: * Copyright (c) 2000, 2001 Linas Vepstas * */ #define _GNU_SOURCE #include "config.h" #include #include #include #include #include #include #include #include #include #include "AccountP.h" #include "Backend.h" #include "BackendP.h" #include "Group.h" #include "GroupP.h" #include "gnc-book.h" #include "gnc-commodity.h" #include "gnc-engine.h" #include "gnc-engine-util.h" #include "gnc-event.h" #include "gnc-pricedb.h" #include "gnc-pricedb-p.h" #include "guid.h" #include "GNCId.h" #include "GNCIdP.h" #include "TransactionP.h" #include "builder.h" #include "checkpoint.h" #include "gncquery.h" #include "kvp-sql.h" #include "PostgresBackend.h" #include "putil.h" 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); GUID nullguid; /* 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 there's not much point in getting fancy ... */ #define QBUFSIZE 16350 /* ============================================================= */ /* misc bogus utility routines */ static char * pgendGetHostname (PGBackend *be) { char * p; p = be->buff; *p = 0; if (0 == gethostname (p, QBUFSIZE/3)) { extern int h_errno; struct hostent *hent; hent = gethostbyname (be->buff); if (hent) { strcpy (be->buff, hent->h_name); } else { PERR ("can't get domainname: %s", hstrerror(h_errno)); } } else { *p = 0; PERR ("can't get hostname"); } 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 finds the commodity by parsing a string * of the form NAMESPACE::MNEMONIC */ static gnc_commodity * gnc_string_to_commodity (const char *str) { /* 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; space = g_strdup(str); name = strchr (space, ':'); *name = 0; name += 2; com = gnc_commodity_table_lookup(comtab, space, name); g_free (space); return com; } /* ============================================================= */ /* send the query, process the results */ gpointer pgendGetResults (PGBackend *be, gpointer (*handler) (PGBackend *, PGresult *, int, gpointer), gpointer data) { PGresult *result; int i=0; be->nrows=0; do { GET_RESULTS (be->connection, result); { int j, jrows; int ncols = PQnfields (result); jrows = PQntuples (result); be->nrows += jrows; PINFO ("query result %d has %d rows and %d cols", i, jrows, ncols); for (j=0; jcore_dirty)) return; ENTER ("acct=%p, mark=%d", acct, do_mark); if (do_mark) { /* Check to see if we've processed this account recently. * If so, then return. The goal here is to avoid excess * hits to the database, leading to poor performance. * Note that this marking makes this routine unsafe to use * outside a lock (since we never clear the mark) */ if (xaccAccountGetMark (acct)) return; xaccAccountSetMark (acct, 1); } if (do_check_version) { if (0 < pgendAccountCompareVersion (be, acct)) return; } acct->version ++; /* be sure to update the version !! */ pgendPutOneAccountOnly (be, acct); /* make sure the account's commodity is in the commodity table */ /* XXX hack alert FIXME -- it would be more efficient to do * this elsewhere, and not here. Or put a mark on it ... * See StoreAllPrices for an example of how to do this. */ com = xaccAccountGetCommodity (acct); pgendPutOneCommodityOnly (be, (gnc_commodity *) com); pgendKVPStore (be, &(acct->guid), acct->kvp_data); LEAVE(" "); } /* ============================================================= */ /* The pgendStoreGroup() routine stores the account hierarchy to * the sql database. That is, it stores not oonly the top-level * accounts, but all of thier children too. It also stores the * commodities associated with the accounts. It does *not* store * any of the transactions. * * Note that it checks the version numbers, and only stores * those accounts whose version number is equal or newer than * what's in the DB. * * The NoLock version doesn't lock up the tables. */ static void pgendStoreGroupNoLock (PGBackend *be, AccountGroup *grp, gboolean do_mark, gboolean do_check_version) { GList *start, *node; if (!be || !grp) return; ENTER("grp=%p mark=%d", grp, do_mark); /* walk the account tree, and store subaccounts */ start = xaccGroupGetAccountList (grp); for (node=start; node; node=node->next) { AccountGroup *subgrp; Account *acc = node->data; pgendStoreAccountNoLock (be, acc, do_mark, do_check_version); /* recursively walk to child accounts */ subgrp = xaccAccountGetChildren (acc); if (subgrp) pgendStoreGroupNoLock(be, subgrp, do_mark, do_check_version); } LEAVE(" "); } static void pgendStoreGroup (PGBackend *be, AccountGroup *grp) { char *p; ENTER ("be=%p, grp=%p", be, grp); if (!be || !grp) return; /* lock it up so that we store atomically */ p = "BEGIN;\n" "LOCK TABLE gncAccount IN EXCLUSIVE MODE;\n" "LOCK TABLE gncCommodity IN EXCLUSIVE MODE;\n"; SEND_QUERY (be,p, ); FINISH_QUERY(be->connection); /* Clear the account marks; this is used to avoid visiting * the same account more than once. */ xaccClearMarkDownGr (grp, 0); pgendStoreGroupNoLock (be, grp, TRUE, TRUE); /* reset the write flags again */ xaccClearMarkDownGr (grp, 0); p = "COMMIT;"; SEND_QUERY (be,p, ); FINISH_QUERY(be->connection); LEAVE(" "); } /* ============================================================= */ /* ACCOUNT GETTERS (SETTERS ARE ABOVE) */ /* ============================================================= */ /* ============================================================= */ /* This hack is a work-around for obtaining the account currency. * The sql backend doens't actually store one, so we work around * it here. This routine goes away when the rest of gnucash stops * using account currencies. */ static gpointer get_hack_cb (PGBackend *be, PGresult *result, int j, gpointer data) { Account *acc = (Account *) data; xaccAccountSetCurrency (acc, gnc_string_to_commodity (DB_GET_VAL("currency",j))); return data; } static void pgendGetAccountCurrencyHack (PGBackend *be, Account *acc) { char *p; p = be->buff; *p = 0; p = stpcpy (p, "SELECT gncTransaction.currency FROM " " gncAccount, gncEntry, gncTransaction WHERE " " gncEntry.accountGuid = '"); p = guid_to_string_buff (xaccAccountGetGUID (acc), p); p = stpcpy (p, "' AND " " gncEntry.transGuid = gncTransaction.transGuid AND " " gncTransaction.currency <> gncAccount.commodity " " LIMIT 1;"); SEND_QUERY (be, be->buff, ); pgendGetResults (be, get_hack_cb, acc); } static gpointer get_account_currency_hack_cb (Account *acc, gpointer data) { PGBackend *be = (PGBackend *) data; pgendGetAccountCurrencyHack (be, acc); return NULL; } /* ============================================================= */ /* This routine walks the account group, gets all KVP values */ static gpointer restore_cb (Account *acc, void * cb_data) { PGBackend *be = (PGBackend *) cb_data; acc->kvp_data = pgendKVPFetch (be, &(acc->guid), acc->kvp_data); return NULL; } static void pgendGetAllAccountKVP (PGBackend *be, AccountGroup *grp) { if (!grp) return; xaccGroupForEachAccount (grp, restore_cb, be, TRUE); } /* ============================================================= */ /* This routine restores all commodities in the database. */ static gpointer get_commodities_cb (PGBackend *be, PGresult *result, int j, gpointer data) { gnc_commodity_table *comtab = (gnc_commodity_table *) data; gnc_commodity *com; /* first, lets see if we've already got this one */ com = gnc_commodity_table_lookup(comtab, DB_GET_VAL("namespace",j), DB_GET_VAL("mnemonic",j)); if (com) return comtab; /* no we don't ... restore it */ com = gnc_commodity_new ( 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); return comtab; } static void pgendGetAllCommodities (PGBackend *be) { gnc_commodity_table *comtab; char * p; if (!be) return; ENTER ("be=%p, conn=%p", be, be->connection); comtab = gnc_engine_commodities(); if (!comtab) { PERR ("can't get global commodity table"); return; } /* Get them ALL */ p = "SELECT * FROM gncCommodity;"; SEND_QUERY (be, p, ); pgendGetResults (be, get_commodities_cb, comtab); LEAVE (" "); } /* ============================================================= */ /* The pgendGetAllAccounts() routine restores the account hierarchy * of *all* accounts in the DB. * It implicitly assumes that the database has only one account * hierarchy in it, i.e. any accounts without a parent will be stuffed * into the same top group. */ static gpointer get_account_cb (PGBackend *be, PGresult *result, int j, gpointer data) { AccountGroup *topgrp = (AccountGroup *) data; Account *parent; Account *acc; GUID guid; /* first, lets see if we've already got this one */ PINFO ("account GUID=%s", DB_GET_VAL("accountGUID",j)); guid = nullguid; /* just in case the read fails ... */ string_to_guid (DB_GET_VAL("accountGUID",j), &guid); acc = xaccAccountLookup (&guid); if (!acc) { acc = xaccMallocAccount(); xaccAccountBeginEdit(acc); xaccAccountSetGUID(acc, &guid); } else { xaccAccountBeginEdit(acc); } 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))); xaccAccountSetVersion(acc, atoi(DB_GET_VAL("version",j))); /* try to find the parent account */ PINFO ("parent GUID=%s", DB_GET_VAL("parentGUID",j)); guid = nullguid; /* just in case the read fails ... */ string_to_guid (DB_GET_VAL("parentGUID",j), &guid); if (guid_equal(xaccGUIDNULL(), &guid)) { /* if the parent guid is null, then this * account belongs in the top group */ xaccGroupInsertAccount (topgrp, acc); } else { /* if we haven't restored the parent account, create * an empty holder for it */ parent = xaccAccountLookup (&guid); if (!parent) { parent = xaccMallocAccount(); xaccAccountBeginEdit(parent); xaccAccountSetGUID(parent, &guid); } else { xaccAccountBeginEdit(parent); } xaccAccountInsertSubAccount(parent, acc); xaccAccountCommitEdit(parent); } xaccAccountCommitEdit(acc); return topgrp; } static AccountGroup * pgendGetAllAccounts (PGBackend *be, AccountGroup *topgrp) { char * bufp; ENTER ("be=%p", be); if (!be) return NULL; /* first, make sure commodities table is up to date */ pgendGetAllCommodities (be); if (!topgrp) { topgrp = xaccMallocAccountGroup(); } /* Get them ALL */ bufp = "SELECT * FROM gncAccount;"; SEND_QUERY (be, bufp, NULL); pgendGetResults (be, get_account_cb, topgrp); /* hack alert -- get account currencies */ xaccGroupForEachAccount (topgrp, get_account_currency_hack_cb, be, TRUE); pgendGetAllAccountKVP (be, topgrp); /* 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. */ xaccGroupMarkSaved (topgrp); LEAVE (" "); return topgrp; } /* ============================================================= */ /* ============================================================= */ /* TRANSACTION STUFF */ /* ============================================================= */ /* ============================================================= */ /* The is_trans_empty() routine returns TRUE if this appears to * be a fresh, 'null' transaction. It would be better if somehow * we could get the gui to mark this as a fresh transaction, rather * than having to scan a bunch of fields. But, oh well, this is * a minor quibble in the grand scheme of things. */ static gboolean is_trans_empty (Transaction *trans) { Split *s; if (!trans) return TRUE; if (0 != (xaccTransGetDescription(trans))[0]) return FALSE; if (0 != (xaccTransGetNum(trans))[0]) return FALSE; if (1 != xaccTransCountSplits(trans)) return FALSE; s = xaccTransGetSplit(trans, 0); if (TRUE != gnc_numeric_zero_p(xaccSplitGetShareAmount(s))) return FALSE; if (TRUE != gnc_numeric_zero_p(xaccSplitGetValue(s))) return FALSE; if ('n' != xaccSplitGetReconcile(s)) return FALSE; if (0 != (xaccSplitGetMemo(s))[0]) return FALSE; if (0 != (xaccSplitGetAction(s))[0]) return FALSE; return TRUE; } /* ============================================================= */ /* The pgendStoreTransactionNoLock() routine traverses the transaction * structure and stores/updates it in the database. If checks the * transaction splits as well, updating those. If the database * has splits which the transaction doesn't, those are deleted. * Then any new splits are poked into the database. * * If the do_check_version flag is set, then the database version * is compared to the engine version. If the database version is * newer, then the engine transaction is not stored. * * The pgendStoreTransaction() routine does the same, except that * it locks the tables appropriately. */ static gpointer delete_list_cb (PGBackend *be, PGresult *result, int j, gpointer data) { GList * deletelist = (GList *) data; 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 == xaccSplitLookup (&guid)) { deletelist = g_list_prepend (deletelist, g_strdup(DB_GET_VAL ("entryGuid", j))); } return deletelist; } static void pgendStoreTransactionNoLock (PGBackend *be, Transaction *trans, gboolean do_check_version) { GList *start, *deletelist=NULL, *node; char * p; if (!be || !trans) return; ENTER ("trans=%p", trans); /* don't update the database if the database is newer ... */ if (do_check_version) { if (0 < pgendTransactionCompareVersion (be, trans)) return; } trans->version ++; /* be sure to update the version !! */ /* 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, "';"); SEND_QUERY (be,be->buff, ); deletelist = pgendGetResults (be, delete_list_cb, deletelist); /* delete those splits that don't belong */ p = be->buff; *p = 0; for (node=deletelist; node; node=node->next) { Split *s; GUID guid; string_to_guid ((char *)(node->data), &guid); s = xaccSplitLookup(&guid); pgendStoreAuditSplit (be, s, SQL_DELETE); p = stpcpy (p, "DELETE FROM gncEntry WHERE entryGuid='"); p = stpcpy (p, node->data); p = stpcpy (p, "';\n"); } if (p != be->buff) { PINFO ("%s", be->buff ? be->buff : "(null)"); SEND_QUERY (be,be->buff, ); FINISH_QUERY(be->connection); /* destroy any associated kvp data as well */ for (node=deletelist; node; node=node->next) { pgendKVPDeleteStr (be, (char *)(node->data)); g_free (node->data); } } /* Update the rest */ start = xaccTransGetSplitList(trans); if ((start) && !(trans->do_free)) { for (node=start; node; node=node->next) { Split * s = node->data; pgendPutOneSplitOnly (be, s); pgendKVPStore (be, &(s->guid), s->kvp_data); } pgendPutOneTransactionOnly (be, trans); pgendKVPStore (be, &(trans->guid), trans->kvp_data); } else { p = be->buff; *p = 0; for (node=start; node; node=node->next) { Split * s = node->data; pgendStoreAuditSplit (be, s, SQL_DELETE); p = stpcpy (p, "DELETE FROM gncEntry WHERE entryGuid='"); p = guid_to_string_buff (xaccSplitGetGUID(s), p); p = stpcpy (p, "';\n"); } /* If this trans is marked for deletetion, use the 'orig' values * as the base for recording the audit. This wouldn't be normally * reqquired, except that otherwise one gets a trashed currency * value. */ pgendStoreAuditTransaction (be, trans->orig, SQL_DELETE); p = be->buff; p = stpcpy (p, "DELETE FROM gncTransaction WHERE transGuid='"); p = guid_to_string_buff (xaccTransGetGUID(trans), p); p = stpcpy (p, "';"); PINFO ("%s\n", be->buff ? be->buff : "(null)"); SEND_QUERY (be,be->buff, ); FINISH_QUERY(be->connection); /* destroy any associated kvp data as well */ for (node=start; node; node=node->next) { Split * s = node->data; pgendKVPDelete (be, &(s->guid)); } pgendKVPDelete (be, &(trans->guid)); } LEAVE(" "); } #if 0 /* This routine isn't used anywhere, and probably shouldn't * be, in part because its balance checkpointing algorithm * is wrong. */ 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 */ bufp = "BEGIN;\n" "LOCK TABLE gncTransaction IN EXCLUSIVE MODE;\n" "LOCK TABLE gncEntry IN EXCLUSIVE MODE;\n"; SEND_QUERY (be,bufp, ); FINISH_QUERY(be->connection); pgendStoreTransactionNoLock (be, trans, TRUE); bufp = "COMMIT;"; SEND_QUERY (be,bufp, ); FINISH_QUERY(be->connection); /* If this is the multi-user mode, we need to update the * balances as well. */ if ((MODE_POLL == be->session_mode) || (MODE_EVENT == be->session_mode)) { /* hack alert -- we should also recompute * the checkpoints for any accounts from which splits have * been deleted ... but we don't have these handy here ... * is this is actually kinda wrong ... */ pgendTransactionRecomputeCheckpoints (be, trans); } LEAVE(" "); } #endif /* ============================================================= */ /* The pgendStoreAllTransactions() routine traverses through *all* * transactions in the account group, storing these to the database. * During the store, it checks the transaction version numbers, * and only stores those transactions that were newer in the engine. */ static int trans_traverse_cb (Transaction *trans, void *cb_data) { pgendStoreTransactionNoLock ((PGBackend *) cb_data, trans, TRUE); return 0; } static void pgendStoreAllTransactions (PGBackend *be, AccountGroup *grp) { char *p; ENTER ("be=%p, grp=%p", be, grp); if (!be || !grp) return; /* lock it up so that we store atomically */ p = "BEGIN;\n" "LOCK TABLE gncTransaction IN EXCLUSIVE MODE;\n" "LOCK TABLE gncEntry IN EXCLUSIVE MODE;\n"; SEND_QUERY (be,p, ); FINISH_QUERY(be->connection); /* Recursively walk transactions. Start by reseting the write * flags. We use this to avoid infinite recursion */ xaccGroupBeginStagedTransactionTraversals(grp); xaccGroupStagedTransactionTraversal (grp, 1, trans_traverse_cb, be); p = "COMMIT;"; SEND_QUERY (be,p, ); FINISH_QUERY(be->connection); /* If this is the multi-user mode, we need to update the * balances as well. */ if ((MODE_POLL == be->session_mode) || (MODE_EVENT == be->session_mode)) { pgendGroupRecomputeAllCheckpoints(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 version 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 transaction 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 -1. * * If this routine finds a pre-existing transaction in the engine, * and the version of last modification of this transaction is * equal to or *newer* then what the DB holds, then this routine * returns 0 if equal, and +1 if newer, and does *not* perform any * update. (Note that 0 is returned for various error conditions. * Thus, testing for 0 is a bad idea. This is a hack, and should * probably be fixed. */ static int pgendCopyTransactionToEngine (PGBackend *be, GUID *trans_guid) { char *pbuff; Transaction *trans; PGresult *result; Account *acc, *previous_acc=NULL; gboolean do_set_guid=FALSE; int engine_data_is_newer = 0; int i, j, nrows; int save_state = 1; GList *node, *db_splits=NULL, *engine_splits, *delete_splits=NULL; gnc_commodity *currency = NULL; gint64 trans_frac = 0; ENTER ("be=%p", be); if (!be || !trans_guid) return 0; /* disable callbacks into the backend, and events to GUI */ gnc_engine_suspend_events(); pgendDisable(be); /* first, see if we already have such a transaction */ trans = xaccTransLookup (trans_guid); if (!trans) { trans = xaccMallocTransaction(); do_set_guid=TRUE; engine_data_is_newer = -1; } /* build the sql query to get the transaction */ pbuff = be->buff; pbuff[0] = 0; pbuff = stpcpy (pbuff, "SELECT * FROM gncTransaction WHERE transGuid='"); pbuff = guid_to_string_buff(trans_guid, pbuff); pbuff = stpcpy (pbuff, "';"); SEND_QUERY (be,be->buff, 0); i=0; nrows=0; do { GET_RESULTS (be->connection, result); { int jrows; int ncols = PQnfields (result); jrows = PQntuples (result); nrows += jrows; PINFO ("query result %d has %d rows and %d cols", i, nrows, ncols); j = 0; 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", guid_to_string (trans_guid)); if (jrows != nrows) xaccTransCommitEdit (trans); xaccBackendSetError (&be->be, ERR_BACKEND_DATA_CORRUPT); pgendEnable(be); gnc_engine_resume_events(); return 0; } /* First order of business is to determine whose data is * newer: the engine cache, or the database. If the * database has newer stuff, we update the engine. If the * engine is equal or newer, we do nothing in this routine. * Of course, we know the database has newer data if this * transaction doesn't exist in the engine yet. */ if (!do_set_guid) { gint32 db_version, cache_version; db_version = atoi (DB_GET_VAL("version",j)); cache_version = xaccTransGetVersion (trans); if (db_version == cache_version) { engine_data_is_newer = 0; } else if (db_version < cache_version) { engine_data_is_newer = +1; } else { engine_data_is_newer = -1; } } /* if the DB data is newer, copy it to engine */ if (0 > engine_data_is_newer) { Timespec ts; xaccTransBeginEdit (trans); if (do_set_guid) xaccTransSetGUID (trans, trans_guid); 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_local (DB_GET_VAL("date_entered",j)); xaccTransSetDateEnteredTS (trans, &ts); xaccTransSetVersion (trans, atoi(DB_GET_VAL("version",j))); /* 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. */ currency = gnc_string_to_commodity (DB_GET_VAL("currency",j)); trans_frac = gnc_commodity_get_fraction (currency); #if 0 xaccTransSetCurrency (trans, gnc_string_to_commodity (DB_GET_VAL("currency",j))); #endif } } PQclear (result); i++; } while (result); if (0 == nrows) { /* 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"); xaccBackendSetError (&be->be, ERR_SQL_MISSING_DATA); pgendEnable(be); gnc_engine_resume_events(); return 0; } /* if engine data was newer, we are done */ if (0 <= engine_data_is_newer) { pgendEnable(be); gnc_engine_resume_events(); return engine_data_is_newer; } /* ------------------------------------------------- */ /* If we are here, then the sql database contains data that is * newer than what we have in the engine. And so, below, * we finish the job of yanking data out of the db. */ /* build the sql query the splits */ pbuff = be->buff; pbuff[0] = 0; pbuff = stpcpy (pbuff, "SELECT * FROM gncEntry WHERE transGuid='"); pbuff = guid_to_string_buff(trans_guid, pbuff); pbuff = stpcpy (pbuff, "';"); SEND_QUERY (be,be->buff, 0); 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; jparent) save_state = acc->parent->saved; xaccAccountInsertSplit(acc, s); if (acc->parent) acc->parent->saved = save_state; /* finally tally them up; we use this below to * clean out deleted splits */ db_splits = g_list_prepend (db_splits, s); } } } i++; PQclear (result); } while (result); /* close out dangling edit session */ xaccAccountCommitEdit (previous_acc); /* ------------------------------------------------- */ /* destroy any splits that the engine has that the DB didn't */ i=0; j=0; engine_splits = xaccTransGetSplitList(trans); for (node = engine_splits; node; node=node->next) { /* if not found, mark for deletion */ if (NULL == g_list_find (db_splits, node->data)) { delete_splits = g_list_prepend (delete_splits, node->data); j++; } i++; } PINFO ("%d of %d splits marked for deletion", j, i); /* now, delete them ... */ for (node=delete_splits; node; node=node->next) { xaccSplitDestroy ((Split *) node->data); } g_list_free (delete_splits); g_list_free (db_splits); /* ------------------------------------------------- */ /* restore any kvp data associated with the transaction and splits */ trans->kvp_data = pgendKVPFetch (be, &(trans->guid), trans->kvp_data); engine_splits = xaccTransGetSplitList(trans); for (node = engine_splits; node; node=node->next) { Split *s = node->data; s->kvp_data = pgendKVPFetch (be, &(s->guid), s->kvp_data); } /* ------------------------------------------------- */ /* see note above as to why we do this set here ... */ xaccTransSetCurrency (trans, currency); xaccTransCommitEdit (trans); /* re-enable events to the backend and GUI */ pgendEnable(be); gnc_engine_resume_events(); LEAVE (" "); return -1; } /* ============================================================= */ /* 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. * * THIS IS NOT USED ANYWHERE should probably go away. Although * this kind of a routine could be handy for resyncing after a lost * contact to the backend. Note, however, that it would * mangle balance checkpoints, and these would need to be * recomputed. */ #if 0 static void pgendSyncTransaction (PGBackend *be, GUID *trans_guid) { Transaction *trans; int engine_data_is_newer = 0; 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 (0 < 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 = xaccTransLookup (trans_guid); /* 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); gnc_engine_resume_events(); return; } /* re-enable events to the backend and GUI */ pgendEnable(be); gnc_engine_resume_events(); LEAVE (" "); } #endif /* ============================================================= */ /* QUERY STUFF */ /* ============================================================= */ /* 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 then performs a 'closure' * in order to maintain accurate balances. Warning: this routine * is a bit of a pig, and should be replaced with a better algorithm. * See below. * * The problem that this routine is trying to solve is the need to * to run a query *and* maintain consistent balance checkpoints * within the engine data. As a by-product, it can pull in a vast * amount of sql data into the engine. The steps of teh algorithm * are: * * 1) convert the engine style query to an SQL query string. * 2) run the SQL query to get the splits that satisfy the query * 3) pull the transaction ids out of the matching splits, * 4) fetch the corresponding transactions, put them into the engine. * 5) get the balance checkpoint with the latest date earlier * than the earliest transaction, * 6) get all splits later than the checkpoint start, * 7) go to step 3) until a consistent set of transactions * has been pulled into the engine. * * Note regarding step 4): * 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. * * Note regarding step 6): * During the fill-out up to the checkpoint, new transactions may * pulled in. These splits may link accounts we haven't seen before, * which is why we need to go back to step 3. * * The process may pull in a huge amount of data. * * Oops: mega-bug: if the checkpoints on all accounts don't share * a common set of dates, then the above process will 'walk' until * the start of time, essentially pulling *all* data, and not that * efficiently, either. This is a killer bug with this implementation. * We can work around it by fixing checkpoints in time ... * * There are certainly alternate possible implementations. In one * alternate, 'better' implementation, we don't fill out to to the * checkpoint for all accounts, but only for the one being displayed. * However, doing so would require considerable jiggering in the * engine proper, where we'd have to significantly modify * RecomputeBalance() to do the 'right thing' when it has access to * only some of the splits. Yow. Wait til after gnucash-1.6 for * this tear-up. */ static gpointer query_cb (PGBackend *be, PGresult *result, int j, gpointer data) { GList *node, *xaction_list = (GList *) data; GUID *trans_guid; /* find the transaction this goes into */ trans_guid = xaccGUIDMalloc(); *trans_guid = nullguid; /* just in case the read fails ... */ string_to_guid (DB_GET_VAL("transGUID",j), trans_guid); /* don't put transaction into the list more than once ... */ for (node=xaction_list; node; node=node->next) { if (guid_equal ((GUID *)node->data, trans_guid)) { xaccGUIDFree (trans_guid); return xaction_list; } } xaction_list = g_list_prepend (xaction_list, trans_guid); return xaction_list; } typedef struct acct_earliest { Account *acct; Timespec ts; } AcctEarliest; static int ncalls = 0; static void pgendFillOutToCheckpoint (PGBackend *be, const char *query_string) { GList *node, *anode, *xaction_list= NULL, *acct_list = NULL; ENTER (" "); if (!be) return; ncalls ++; SEND_QUERY (be, query_string, ); xaction_list = pgendGetResults (be, query_cb, xaction_list); if (NULL == xaction_list) return; /* restore the transactions */ for (node=xaction_list; node; node=node->next) { int engine_data_is_newer; GUID *trans_guid = (GUID *)node->data; engine_data_is_newer = pgendCopyTransactionToEngine (be, trans_guid); /* if we restored this transaction from the db, scan over the accounts * it affects and see how far back the data goes. */ if (0 > engine_data_is_newer) { GList *split_list, *snode; Timespec ts; Transaction *trans; trans = xaccTransLookup (trans_guid); ts = xaccTransRetDatePostedTS (trans); split_list = xaccTransGetSplitList (trans); for (snode=split_list; snode; snode=snode->next) { int found = 0; Split *s = (Split *) snode->data; Account *acc = xaccSplitGetAccount (s); /* lets see if we have a record of this account already */ for (anode = acct_list; anode; anode = anode->next) { AcctEarliest * ae = (AcctEarliest *) anode->data; if (ae->acct == acc) { if (0 > timespec_cmp(&ts, &(ae->ts))) { ae->ts = ts; } found = 1; break; } } /* if not found, make note of this account, and the date */ if (0 == found) { AcctEarliest * ae = g_new (AcctEarliest, 1); ae->acct = acc; ae->ts = ts; acct_list = g_list_prepend (acct_list, ae); } } } xaccGUIDFree (trans_guid); } g_list_free(xaction_list); if (NULL == acct_list) return; /* OK, at this point, we have a list of accounts, including the * date of the earliest split in that account. Now, we need to * do two queries: first, get the latest checkpoint that is earlier * than the earliest split. Next, we get *all* of the splits from * that checkpoint onwards. */ for (anode = acct_list; anode; anode = anode->next) { char *p; Timespec start_date; AcctEarliest * ae = (AcctEarliest *) anode->data; start_date = pgendAccountGetBalance (be, ae->acct, ae->ts); p = be->buff; *p = 0; p = stpcpy (p, "SELECT DISTINCT gncEntry.transGuid from gncEntry, gncTransaction WHERE " " gncEntry.transGuid = gncTransaction.transGuid AND accountGuid='"); p = guid_to_string_buff(xaccAccountGetGUID(ae->acct), p); p = stpcpy (p, "' AND gncTransaction.date_posted > '"); p = gnc_timespec_to_iso8601_buff (ae->ts, p); p = stpcpy (p, "';"); pgendFillOutToCheckpoint (be, be->buff); g_free (ae); } g_list_free(acct_list); LEAVE (" "); } static void pgendRunQuery (Backend *bend, Query *q) { PGBackend *be = (PGBackend *)bend; const char * sql_query_string; sqlQuery *sq; GList *node, *anode, *xaction_list= NULL, *acct_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); ncalls = 0; pgendFillOutToCheckpoint (be, sql_query_string); PINFO ("number of calls to fill out=%d", ncalls); sql_Query_destroy(sq); /* the fill-out will dirty a lot of data. That's irrelevent, * mark it all as having been saved. */ xaccGroupMarkSaved (be->topgroup); pgendEnable(be); gnc_engine_resume_events(); LEAVE (" "); } /* ============================================================= */ /* The pgendGetAllTransactions() routine 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) { GList *node, *xaction_list = NULL; gnc_engine_suspend_events(); pgendDisable(be); SEND_QUERY (be, "SELECT transGuid FROM gncTransaction;", ); xaction_list = pgendGetResults (be, query_cb, xaction_list); /* restore the transactions */ for (node=xaction_list; node; node=node->next) { pgendCopyTransactionToEngine (be, (GUID *)node->data); xaccGUIDFree (node->data); } g_list_free(xaction_list); pgendEnable(be); gnc_engine_resume_events(); } /* ============================================================= */ /* ============================================================= */ /* PRICE STUFF */ /* ============================================================= */ /* ============================================================= */ /* store just one price */ static void pgendStorePriceNoLock (PGBackend *be, GNCPrice *pr, gboolean do_check_version) { gnc_commodity *modity; if (do_check_version) { if (0 < pgendPriceCompareVersion (be, pr)) return; } pr->version ++; /* be sure to update the version !! */ /* make sure that we've stored the commodity * and currency before we store the price. */ modity = gnc_price_get_commodity (pr); pgendPutOneCommodityOnly (be, modity); modity = gnc_price_get_currency (pr); pgendPutOneCommodityOnly (be, modity); pgendPutOnePriceOnly (be, pr); } /* ============================================================= */ /* store entire price database */ static gboolean foreach_price_cb (GNCPrice *pr, gpointer bend) { PGBackend *be = (PGBackend *) bend; gnc_commodity *modity; gint16 mark; /* make sure that we've stored the commodity * and currency before we store the price. * We use marks to avoid redundant stores. */ modity = gnc_price_get_commodity (pr); mark = gnc_commodity_get_mark (modity); if (!mark) { pgendPutOneCommodityOnly (be, modity); gnc_commodity_set_mark (modity, 1); } modity = gnc_price_get_currency (pr); mark = gnc_commodity_get_mark (modity); if (!mark) { pgendPutOneCommodityOnly (be, modity); gnc_commodity_set_mark (modity, 1); } pgendPutOnePriceOnly (be, pr); return TRUE; } static gboolean commodity_mark_cb (gnc_commodity *cm, gpointer user_data) { gint32 v = ((gint32) user_data) & 0xffff; gnc_commodity_set_mark (cm, (gint16) v); return TRUE; } static void pgendStorePriceDBNoLock (PGBackend *be, GNCPriceDB *prdb) { gnc_commodity_table *comtab = gnc_engine_commodities(); /* clear the marks on commodities -- we use this to mark * the thing as 'already stored', avoiding redundant stores */ gnc_commodity_table_foreach_commodity (comtab, commodity_mark_cb, 0); gnc_pricedb_foreach_price (prdb, foreach_price_cb, (gpointer) be, FALSE); gnc_commodity_table_foreach_commodity (comtab, commodity_mark_cb, 0); } static void pgendStorePriceDB (PGBackend *be, GNCPriceDB *prdb) { char *p; ENTER ("be=%p, prdb=%p", be, prdb); if (!be || !prdb) return; /* lock it up so that we store atomically */ p = "BEGIN;\n" "LOCK TABLE gncPrice IN EXCLUSIVE MODE;\n"; SEND_QUERY (be,p, ); FINISH_QUERY(be->connection); pgendStorePriceDBNoLock (be, prdb); p = "COMMIT;"; SEND_QUERY (be,p, ); FINISH_QUERY(be->connection); LEAVE(" "); } /* ============================================================= */ /* The pgendGetAllPrices() routine sucks *all* of the * prices out of the database. This is a potential * CPU and memory-burner; its use is not suggested for anything * but single-user mode. */ static gpointer get_price_cb (PGBackend *be, PGresult *result, int j, gpointer data) { GNCPriceDB *prdb = (GNCPriceDB *) data; GNCPrice *pr; gint32 sql_vers, local_vers; Timespec ts; gint64 num, denom; gnc_numeric value; GUID guid = nullguid; int not_found = 0; gnc_commodity * modity; /* first, lets see if we've already got this one */ string_to_guid (DB_GET_VAL ("priceGuid", j), &guid); pr = gnc_price_lookup (&guid); if (!pr) { pr = gnc_price_create(); gnc_price_begin_edit (pr); gnc_price_set_guid (pr, &guid); not_found = 1; } else { gnc_price_ref (pr); gnc_price_begin_edit (pr); not_found = 0; } /* compare versions. Hack alert -- Not sure how to handle failures */ sql_vers = atoi (DB_GET_VAL("version",j)); local_vers = gnc_price_get_version(pr); if (sql_vers < local_vers) { PERR ("local price version is higher than db !!! local=%d sql=%d", local_vers, sql_vers); gnc_price_commit_edit (pr); gnc_price_unref (pr); return prdb; } gnc_price_set_version (pr, sql_vers); modity = gnc_string_to_commodity (DB_GET_VAL("commodity",j)); gnc_price_set_commodity (pr, modity); modity = gnc_string_to_commodity (DB_GET_VAL("currency",j)); gnc_price_set_currency (pr, modity); ts = gnc_iso8601_to_timespec_local (DB_GET_VAL("time",j)); gnc_price_set_time (pr, ts); gnc_price_set_source (pr, DB_GET_VAL("source",j)); gnc_price_set_type (pr, DB_GET_VAL("type",j)); num = atoll (DB_GET_VAL("valueNum", j)); denom = atoll (DB_GET_VAL("valueDenom", j)); value = gnc_numeric_create (num, denom); gnc_price_set_value (pr, value); if (not_found) gnc_pricedb_add_price(prdb, pr); gnc_price_commit_edit (pr); gnc_price_unref (pr); return prdb; } static GNCPriceDB * pgendGetAllPrices (PGBackend *be, GNCPriceDB *prdb) { char * p; if (!be) return NULL; ENTER ("be=%p, conn=%p", be, be->connection); if (!prdb) { prdb = gnc_pricedb_create(); } /* first, make sure commodities table is up to date */ pgendGetAllCommodities (be); /* Get them ALL */ p = "SELECT * FROM gncPrice;"; SEND_QUERY (be, p, prdb); pgendGetResults (be, get_price_cb, prdb); LEAVE (" "); return prdb; } /* ============================================================= */ static void pgendPriceLookup (Backend *bend, GNCPriceLookup *look) { PGBackend *be = (PGBackend *)bend; char * p; ENTER ("be=%p, lookup=%p", be, look); if (!be || !look) return; /* special case the two-way search in terms of more basic primitives */ if (LOOKUP_NEAREST_IN_TIME == look->type) { look->type = LOOKUP_LATEST_BEFORE; pgendPriceLookup (bend, look); look->type = LOOKUP_EARLIEST_AFTER; pgendPriceLookup (bend, look); return; } /* don't send events to GUI, don't accept callbacks to backend */ gnc_engine_suspend_events(); pgendDisable(be); /* set up the common part of the query */ p = be->buff; *p = 0; p = stpcpy (p, "SELECT * FROM gncPrice" " WHERE commodity='"); p = stpcpy (p, gnc_commodity_get_unique_name(look->commodity)); p = stpcpy (p, "' AND currency='"); p = stpcpy (p, gnc_commodity_get_unique_name(look->currency)); p = stpcpy (p, "' "); switch (look->type) { case LOOKUP_LATEST: p = stpcpy (p, "ORDER BY time DESC LIMIT 1;"); break; case LOOKUP_ALL: /* Get all prices for this commodity and currency */ p = stpcpy (p, ";"); break; case LOOKUP_AT_TIME: p = stpcpy (p, "AND time='"); p = gnc_timespec_to_iso8601_buff (look->date, p); p = stpcpy (p, "';"); break; case LOOKUP_NEAREST_IN_TIME: PERR ("this can't possibly happen but it did!!!"); p = stpcpy (p, ";"); break; case LOOKUP_LATEST_BEFORE: p = stpcpy (p, "AND time <= '"); p = gnc_timespec_to_iso8601_buff (look->date, p); p = stpcpy (p, "' ORDER BY time DESC LIMIT 1;"); break; case LOOKUP_EARLIEST_AFTER: p = stpcpy (p, "AND time >= '"); p = gnc_timespec_to_iso8601_buff (look->date, p); p = stpcpy (p, "' ORDER BY time ASC LIMIT 1;"); break; default: PERR ("unknown lookup type %d", look->type); /* re-enable events */ pgendEnable(be); gnc_engine_resume_events(); return; } SEND_QUERY (be, be->buff, ); pgendGetResults (be, get_price_cb, look->prdb); /* insertion into the price db will mark it dirty; * but it really isn't at this point. */ gnc_pricedb_mark_clean (look->prdb); /* re-enable events */ pgendEnable(be); gnc_engine_resume_events(); } /* ============================================================= */ /* ============================================================= */ /* HIGHER LEVEL ROUTINES AND BACKEND PROPER */ /* ============================================================= */ /* ============================================================= */ static int pgend_account_commit_edit (Backend * bend, Account * acct) { AccountGroup *parent; char *p; PGBackend *be = (PGBackend *)bend; ENTER ("be=%p, acct=%p", be, acct); if (!be || !acct) return 1; /* hack alert hardcode literal */ if (FALSE == acct->core_dirty) { parent = xaccAccountGetParent(acct); if (parent) parent->saved = 1; return 0; } /* lock it up so that we query and store atomically */ /* its not at all clear to me that this isn't rife with deadlocks. */ p = "BEGIN;\n" "LOCK TABLE gncAccount IN EXCLUSIVE MODE;\n" "LOCK TABLE gncCommodity IN EXCLUSIVE MODE;\n"; SEND_QUERY (be,p, 555); FINISH_QUERY(be->connection); /* check to see that the engine version is equal or newer than * whats in the database. It its not, then some other user has * made changes, and we must roll back. */ if (0 < pgendAccountCompareVersion (be, acct)) { acct->do_free = FALSE; p = "ROLLBACK;"; SEND_QUERY (be,p,444); FINISH_QUERY(be->connection); /* hack alert -- we should restore the account data from the * sql back end at this point ! !!! */ PWARN(" account data in engine is newer\n" " account must be rolled back. This function\n" " is not completely implemented !! \n"); LEAVE ("rolled back"); return 445; } acct->version ++; /* be sure to update the version !! */ if (acct->do_free) { const GUID *guid = xaccAccountGetGUID(acct); pgendStoreAuditAccount (be, acct, SQL_DELETE); pgendKVPDelete (be, guid); p = be->buff; *p = 0; p = stpcpy (p, "DELETE FROM gncAccount WHERE accountGuid='"); p = guid_to_string_buff (guid, p); p = stpcpy (p, "';"); SEND_QUERY (be,be->buff, 444); FINISH_QUERY(be->connection); } else { pgendStoreAccountNoLock (be, acct, FALSE, FALSE); } p = "COMMIT;"; SEND_QUERY (be,p,336); FINISH_QUERY(be->connection); /* Mark this up so that we don't get that annoying gui dialog * about having to save to file. unfortunately,however, this * is too liberal, and could screw up synchronization if we've lost * contact with the back end at some point. So hack alert -- fix * this. */ parent = xaccAccountGetParent(acct); if (parent) parent->saved = 1; LEAVE ("commited"); return 0; } /* ============================================================= */ static int pgend_trans_commit_edit (Backend * bend, Transaction * trans, Transaction * oldtrans) { char * bufp; int rollback=0; PGBackend *be = (PGBackend *)bend; ENTER ("be=%p, trans=%p", be, trans); if (!be || !trans) return 1; /* hack alert hardcode literal */ /* lock it up so that we query and store atomically */ bufp = "BEGIN;\n" "LOCK TABLE gncTransaction IN EXCLUSIVE MODE;\n" "LOCK TABLE gncEntry IN EXCLUSIVE MODE;\n"; SEND_QUERY (be,bufp, 555); FINISH_QUERY(be->connection); /* Check to see if this is a 'new' transaction, or not. * The hallmark of a 'new' transaction is that all the * fields are empty. If its new, then we just go ahead * and commit. If its old, then we need some consistency * checks. */ if (FALSE == is_trans_empty (oldtrans)) { /* See if the database is in the state that we last left it in. * Basically, the database should contain the 'old transaction'. * If it doesn't, then someone else has modified this transaction, * and thus, any further action on our part would be unsafe. It * is recommended that this be spit back at the GUI, and let a * human decide what to do next. * * We could directly compare all of the data ... but instead, * its more efficient to just compare the version number. */ #ifdef COMPARE_ALL_TRANSACTION_DATA { int ndiffs; GList *start, *node; ndiffs = pgendCompareOneTransactionOnly (be, oldtrans); if (0 < ndiffs) rollback++; /* be sure to check the old splits as well ... */ start = xaccTransGetSplitList (oldtrans); for (node=start; node; node=node->next) { Split * s = node->data; ndiffs = pgendCompareOneSplitOnly (be, s); if (0 < ndiffs) rollback++; } } #else if (0 < pgendTransactionCompareVersion (be, oldtrans)) rollback ++; #endif if (rollback) { bufp = "ROLLBACK;"; SEND_QUERY (be,bufp,444); FINISH_QUERY(be->connection); PINFO ("old tranasction didn't match DB, edit rolled back)\n"); return 666; /* hack alert */ } } /* if we are here, we are good to go */ pgendStoreTransactionNoLock (be, trans, FALSE); bufp = "COMMIT;"; SEND_QUERY (be,bufp,334); FINISH_QUERY(be->connection); /* If this is the multi-user mode, we need to update the * balances as well. */ if ((MODE_POLL == be->session_mode) || (MODE_EVENT == be->session_mode)) { GList *node; /* loop over the old accounts, as they used to be. */ for (node = xaccTransGetSplitList(trans->orig); node; node=node->next) { Split *s = (Split *) node->data; Account *acc = xaccSplitGetAccount (s); pgendAccountRecomputeOneCheckpoint (be, acc, trans->orig->date_posted); } /* set checkpoints for the new accounts */ pgendTransactionRecomputeCheckpoints (be, trans); } /* hack alert -- the following code will get rid of that annoying * message from the GUI about saving one's data. However, it doesn't * do the right thing if the connection to the backend was ever lost. * what should happen is the user should get a chance to * resynchronize thier data with the backend, before quiting out. */ { Split * s = xaccTransGetSplit (trans, 0); Account *acc = xaccSplitGetAccount (s); AccountGroup *top = xaccGetAccountRoot (acc); xaccGroupMarkSaved (top); } LEAVE ("commited"); return 0; } /* ============================================================= */ static int pgend_price_begin_edit (Backend * bend, GNCPrice *pr) { if (pr && pr->db && pr->db->dirty) { PERR ("price db is unexpectedly dirty"); } return 0; } static int pgend_price_commit_edit (Backend * bend, GNCPrice *pr) { char * bufp; PGBackend *be = (PGBackend *)bend; ENTER ("be=%p, price=%p", be, pr); if (!be || !pr) return 1; /* hack alert hardcode literal */ /* lock it up so that we query and store atomically */ bufp = "BEGIN;\n" "LOCK TABLE gncPrice IN EXCLUSIVE MODE;\n"; SEND_QUERY (be,bufp, 555); FINISH_QUERY(be->connection); /* check to see that the engine version is equal or newer than * whats in the database. It its not, then some other user has * made changes, and we must roll back. */ if (0 < pgendPriceCompareVersion (be, pr)) { pr->do_free = FALSE; bufp = "ROLLBACK;"; SEND_QUERY (be,bufp,444); FINISH_QUERY(be->connection); /* hack alert -- we should restore the price data from the * sql back end at this point ! !!! */ PWARN(" price data in engine is newer\n" " price must be rolled back. This function\n" " is not completely implemented !! \n"); LEAVE ("rolled back"); return 445; } pr->version ++; /* be sure to update the version !! */ if (pr->do_free) { pgendStoreAuditPrice (be, pr, SQL_DELETE); bufp = be->buff; *bufp = 0; bufp = stpcpy (bufp, "DELETE FROM gncPrice WHERE priceGuid='"); bufp = guid_to_string_buff (gnc_price_get_guid(pr), bufp); bufp = stpcpy (bufp, "';"); PINFO ("%s\n", be->buff ? be->buff : "(null)"); SEND_QUERY (be,be->buff, 444); FINISH_QUERY(be->connection); } else { pgendStorePriceNoLock (be, pr, FALSE); } bufp = "COMMIT;"; SEND_QUERY (be,bufp,335); FINISH_QUERY(be->connection); if (pr->db) pr->db->dirty = FALSE; LEAVE ("commited"); return 0; } /* ============================================================= */ /* hack alert -- the sane-ness of this algorithm should be reviewed. * I can't vouch that there aren't any subtle issues or race conditions * lurking in this. Anyway, with that introduction: * * The pgendSync() routine 'synchronizes' the accounts & commodities * cached in the engine to those in the database. It does this first * by writing out all of the accounts and transactions, from the * top-group down, and then re-reading from the database. This * write-then-read cycle has the effect of merging the engine data * into the sql database. Note that version checking is done during * the writing: only accounts and transactions that are 'newer' in * the engine are written out. Then during the read cycle, anything * in the DB that is newer than what's in the engine is sucked back * into the engine. * * There are three scenarios to contemplate with the update with * this 'sync' operation: * * 1) Database merge: the user has two substantialy similar copies * of the same data; the first copy was read into the engine earlier, * and now, in this routine, it is being written into the second. * Because the merge uses version numbers, this merge should be * 'safe' in that only the newer copy of any account or transaction * is merged. But this 'safety' can break down, in certain cases; * see below. * 1a) Same situation as above, except the 'first' copy is a file * that resulted because the user was kicked off-line (off-network) * and saved the data to a file. Now, coming back on-line, they * are merging the file data back into the central store. * * This merge is *not* safe when two different users made a change * to the same account or transaction. This routine does not check * for such conflicts or report them. Hack alert: this is a bug that * should be fixed. */ static void pgendSync (Backend *bend, AccountGroup *grp) { PGBackend *be = (PGBackend *)bend; ENTER ("be=%p, grp=%p", be, grp); /* store the account group hierarchy, and then all transactions */ pgendStoreGroup (be, grp); pgendStoreAllTransactions (be, grp); /* don't send events to GUI, don't accept callbacks to backend */ gnc_engine_suspend_events(); pgendDisable(be); pgendKVPInit(be); pgendGetAllAccounts (be, grp); if ((MODE_SINGLE_FILE != be->session_mode) && (MODE_SINGLE_UPDATE != be->session_mode)) { Timespec ts = gnc_iso8601_to_timespec_local (CK_AFTER_LAST_DATE); pgendGroupGetAllBalances (be, grp, ts); } else { /* in single user mode, read all the transactions */ pgendGetAllTransactions (be, grp); } /* re-enable events */ pgendEnable(be); gnc_engine_resume_events(); LEAVE(" "); } /* ============================================================= */ /* The pgendSyncSingleFile() routine syncs the engine and database. * 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 in this mode, 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. * * The use of this routine/this mode is 'depricated'. * Its handy for testing, sanity-checking, and as a failsafe, * but its use shouldn't be encouraged. */ static void pgendSyncSingleFile (Backend *bend, AccountGroup *grp) { char *p; PGBackend *be = (PGBackend *)bend; ENTER ("be=%p, grp=%p", be, grp); p = "BEGIN;\n" "LOCK TABLE gncAccount IN EXCLUSIVE MODE;\n" "LOCK TABLE gncCommodity IN EXCLUSIVE MODE;\n" "LOCK TABLE gncTransaction IN EXCLUSIVE MODE;\n" "LOCK TABLE gncEntry IN EXCLUSIVE MODE;\n" "DELETE FROM gncEntry;\n" "DELETE FROM gncTransaction;\n" "DELETE FROM gncAccount;\n" "DELETE FROM gncCommodity;\n"; SEND_QUERY (be,p, ); FINISH_QUERY(be->connection); /* Store accounts and commodities */ xaccClearMarkDownGr (grp, 0); pgendStoreGroupNoLock (be, grp, TRUE, TRUE); xaccClearMarkDownGr (grp, 0); /* Recursively walk transactions. Start by reseting the write * flags. We use this to avoid infinite recursion */ xaccGroupBeginStagedTransactionTraversals(grp); xaccGroupStagedTransactionTraversal (grp, 1, trans_traverse_cb, be); p = "COMMIT;"; SEND_QUERY (be,p, ); FINISH_QUERY(be->connection); LEAVE(" "); } /* ============================================================= */ /* Please read the commend for pgendSync to truly understand * how this routine works. Its somewhat subtle. */ static void pgendSyncPriceDB (Backend *bend, GNCPriceDB *prdb) { PGBackend *be = (PGBackend *)bend; ENTER ("be=%p, prdb=%p", be, prdb); pgendStorePriceDB (be, prdb); /* don't send events to GUI, don't accept callbacks to backend */ gnc_engine_suspend_events(); pgendDisable(be); pgendGetAllPrices (be, prdb); /* re-enable events */ pgendEnable(be); gnc_engine_resume_events(); LEAVE(" "); } /* ============================================================= */ /* The pgendSyncPriceSingleFile() routine syncs the prices in the * engine with the database. * 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 in this mode, there is * no other way of finding out that a price was deleted. * i.e. there's no other way to delete. * So start with a clean slate. * * The use of this routine/this mode is 'depricated'. * Its handy for testing, sanity-checking, and as a failsafe, * but its use shouldn't be encouraged. */ static void pgendSyncPriceDBSingleFile (Backend *bend, GNCPriceDB *prdb) { char *p; PGBackend *be = (PGBackend *)bend; ENTER ("be=%p, prdb=%p", be, prdb); p = "BEGIN;\n" "LOCK TABLE gncPrice IN EXCLUSIVE MODE;\n" "DELETE FROM gncPrice;\n"; SEND_QUERY (be,p, ); FINISH_QUERY(be->connection); /* Store accounts and commodities */ pgendStorePriceDBNoLock (be, prdb); p = "COMMIT;"; SEND_QUERY (be,p, ); FINISH_QUERY(be->connection); LEAVE(" "); } /* ============================================================= */ 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"; } /* ============================================================= */ /* Instead of loading the book, just set the lock error */ static AccountGroup * pgend_book_load_single_lockerr (Backend *bend) { PGBackend *be = (PGBackend *)bend; if (!be) return NULL; xaccBackendSetError (&be->be, ERR_BACKEND_LOCKED); return NULL; } /* ============================================================= */ /* The get_session_cb() routine can determine whether we can start * a session of the desired type. * The logic used is as follows: * -- if there is any (other) 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 gpointer get_session_cb (PGBackend *be, PGresult *result, int j, gpointer data) { char *lock_holder = (char *) data; char *mode = DB_GET_VAL("session_mode", j); if ((MODE_SINGLE_FILE == be->session_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 ? username : "(null)", hostname ? hostname : "(null)", gecos ? gecos : "(null)", mode ? mode : "(null)", datestr ? datestr : "(null)"); PWARN ("The above messages should be handled by the GUI\n"); if (lock_holder) return lock_holder; lock_holder = g_strdup (DB_GET_VAL("sessionGUID",j)); } return lock_holder; } static gboolean pgendSessionCanStart (PGBackend *be, int break_lock) { gboolean retval = TRUE; char *p, *lock_holder; 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); lock_holder = pgendGetResults (be, get_session_cb, NULL); if (lock_holder) retval = FALSE; /* If just one other user has a lock, then will go ahead and * break the lock... If the user approved. I don't like this * but that's what the GUI is set up to do ... */ PINFO ("break_lock=%d nrows=%d lock_holder=%s\n", break_lock, be->nrows, lock_holder ? lock_holder : "(null)"); if (break_lock && (1==be->nrows) && lock_holder) { p = be->buff; *p=0; p = stpcpy (p, "UPDATE gncSession SET time_off='NOW' " "WHERE sessionGuid='"); p = stpcpy (p, lock_holder); p = stpcpy (p, "';"); SEND_QUERY (be,be->buff, retval); FINISH_QUERY(be->connection); retval = TRUE; } if (lock_holder) g_free (lock_holder); LEAVE (" "); return retval; } /* ============================================================= */ /* The pgendSessionValidate() routine determines whether a valid * session could be obtained. It checks to see if: * 1) Database appers to have gnucash data in it * 2) the session table can be locked and updated to start * a session. The update is handled as an atomic test-n-set. * Return TRUE if we have a session. */ static gpointer is_gnucash_cb (PGBackend *be, PGresult *result, int j, gpointer data) { if (TRUE == (gboolean) data) return (gpointer) TRUE; if (0 == strcmp ("gncsession", (DB_GET_VAL ("tablename", j)))) return (gpointer) TRUE; return FALSE; } static gboolean pgendSessionValidate (PGBackend *be, int break_lock) { gboolean retval = FALSE; char *p; ENTER(" "); if (MODE_NONE == be->session_mode) return FALSE; /* check to see if this database actually contains * GnuCash data... */ p = "SELECT * FROM pg_tables; "; SEND_QUERY (be,p, FALSE); retval = (gboolean) pgendGetResults (be, is_gnucash_cb, (gpointer) FALSE); if (FALSE == retval) { xaccBackendSetError (&be->be, ERR_BACKEND_DATA_CORRUPT); 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, break_lock)) { /* This error should be treated just like the * file-lock error from the GUI perspective: * (The GUI allows users to break the lock, if desired). */ be->be.book_load = pgend_book_load_single_lockerr; xaccBackendSetError (&be->be, ERR_BACKEND_LOCKED); 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; } /* ============================================================= */ /* The pgendSessionEnd() routine will log the end of session in * the session table of 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; } /* ============================================================= */ /* The pgend_session_end() routine is the main entrypoint into * this backend for terminating a session. It logs the * end of the session into the gncsession table, disconnects * from the database, and finally frees all malloced memory. */ static void pgend_session_end (Backend *bend) { int i; PGBackend *be = (PGBackend *)bend; if (!be) return; ENTER("be=%p", be); /* mode-specific shutdowns */ switch (be->session_mode) { case MODE_SINGLE_FILE: case MODE_SINGLE_UPDATE: /* although the book may be open in 'single-user' mode right now, * it might be opened in multi-user mode next time. Thus, update * the account balance checkpoints just in case. */ pgendGroupRecomputeAllCheckpoints (be, be->topgroup); break; case MODE_POLL: break; case MODE_EVENT: break; default: PERR ("bad mode specified"); break; } /* prevent further callbacks into backend */ pgendDisable(be); be->be.book_begin = NULL; be->be.book_end = NULL; /* note the logoff time in the session directory */ pgendSessionEnd (be); /* disconnect from the backend */ if(be->connection) PQfinish (be->connection); be->connection = 0; if (be->dbName) { g_free(be->dbName); be->dbName = NULL; } 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; /* free the path strings */ for (i=0; i< be->path_cache_size; i++) { if ((be->path_cache)[i]) g_free ((be->path_cache)[i]); (be->path_cache)[i] = NULL; } g_free (be->path_cache); be->path_cache = NULL; be->path_cache_size = 0; be->ipath_max = 0; LEAVE("be=%p", be); } /* ============================================================= */ /* The pgend_book_load_poll() routine loads account info from * the database into the engine. Its to be used only for * the poll & event style load, where only the accounts, * and never the transactions, need to be loaded. */ static AccountGroup * pgend_book_load_poll (Backend *bend) { Timespec ts = gnc_iso8601_to_timespec_local (CK_AFTER_LAST_DATE); AccountGroup *grp; PGBackend *be = (PGBackend *)bend; if (!be) return NULL; /* don't send events to GUI, don't accept callbacks to backend */ gnc_engine_suspend_events(); pgendDisable(be); pgendKVPInit(be); grp = pgendGetAllAccounts (be, NULL); pgendGroupGetAllBalances (be, grp, ts); /* re-enable events */ pgendEnable(be); gnc_engine_resume_events(); be->topgroup = grp; return grp; } /* ============================================================= */ /* The pgend_price_load_poll() routine creates the pricedb, but * doesn't actually put any prices in it. These are polled on * an as-needed basis. */ static GNCPriceDB * pgend_price_load_poll (Backend *bend) { GNCPriceDB *prdb; PGBackend *be = (PGBackend *)bend; if (!be) return NULL; /* don't send events to GUI */ gnc_engine_suspend_events(); prdb = gnc_pricedb_create(); /* re-enable events */ gnc_engine_resume_events(); return prdb; } /* ============================================================= */ /* The pgend_book_load_single() routine loads the engine with * data from the database. Used only in single-user mode, * it loads account *and* transaction data. Single-user * mode doesn't require balance checkpoingts, to these are * not handled. */ 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 callbacks to backend */ gnc_engine_suspend_events(); pgendDisable(be); pgendKVPInit(be); grp = pgendGetAllAccounts (be, NULL); pgendGetAllTransactions (be, grp); /* re-enable events */ pgendEnable(be); gnc_engine_resume_events(); be->topgroup = grp; return grp; } /* ============================================================= */ /* The pgend_price_load_single() routine loads the engine with * price data from the database. */ static GNCPriceDB * pgend_price_load_single (Backend *bend) { GNCPriceDB *prdb; PGBackend *be = (PGBackend *)bend; if (!be) return NULL; /* don't send events to GUI, don't accept callbacks to backend */ gnc_engine_suspend_events(); pgendDisable(be); prdb = pgendGetAllPrices (be, NULL); /* re-enable events */ pgendEnable(be); gnc_engine_resume_events(); return prdb; } /* ============================================================= */ /* The pgend_session_begin() routine implements the main entrypoint * into the SQL backend code. * * 1) It parses the URL to find the database, username, password, etc. * 2) It makes the first contact to the database, and tries to * initiate a user session. * 3) It creates the GnuCash tables for the first time, if these * need to be created. * 4) It logs the user session in the database (gncsession table). * 5) loads data from the database into the engine. */ static void pgend_session_begin (GNCBook *sess, const char * sessionid, gboolean ignore_lock, gboolean create_new_db) { int really_do_create = 0; int rc; PGBackend *be; char *url, *start, *end; char *password=NULL; char *pg_options=NULL; char *pg_tty=NULL; char *bufp; if (!sess) return; be = (PGBackend *) xaccGNCBookGetBackend (sess); ENTER("be=%p, sessionid=%s", be, sessionid ? sessionid : "(null)"); /* close any dangling sessions from before; reinitialize */ pgend_session_end ((Backend *) be); pgendInit (be); /* Parse the sessionid for the hostname, port number and db name. * The expected URL format is * postgres://some.host.com/db_name * postgres://some.host.com:portno/db_name * postgres://localhost/db_name * postgres://localhost:nnn/db_name * * Also parse urls of the form * postgres://some.host.com/db_name?pgkey=pgval&pgkey=pgval * e.g. * postgres://some.host.com/db_name?user=r00t&pass=3733t&mode=multi-user */ if (strncmp (sessionid, "postgres://", 11)) { xaccBackendSetError (&be->be, ERR_BACKEND_BAD_URL); return; } url = g_strdup(sessionid); start = url + 11; end = strchr (start, ':'); if (end) { /* if colon found, then extract port number */ *end = 0x0; be->hostname = g_strdup (start); start = end+1; end = strchr (start, '/'); if (!end) { g_free(url); return; } *end = 0; be->portno = g_strdup (start); } else { end = strchr (start, '/'); if (!end) { g_free(url); return; } *end = 0; be->hostname = g_strdup (start); } start = end+1; if (0x0 == *start) { xaccBackendSetError (&be->be, ERR_BACKEND_BAD_URL); g_free(url); return; } /* dbname is the last thing before the url-encoded data */ end = strchr (start, '?'); if (end) *end = 0; be->dbName = g_strdup (start); /* loop and parse url-encoded data */ while (end) { start = end+1; end = strchr (start, '&'); if (end) *end = 0; /* mode keyword */ if (0 == strncasecmp (start, "mode=", 5)) { start += 5; if (0 == strcasecmp (start, "single-file")) { be->session_mode = MODE_SINGLE_FILE; } else if (0 == strcasecmp (start, "single-update")) { be->session_mode = MODE_SINGLE_UPDATE; } else if (0 == strcasecmp (start, "multi-user")) { be->session_mode = MODE_POLL; } else if (0 == strcasecmp (start, "multi-user-event")) { be->session_mode = MODE_EVENT; } else { PWARN ("the following message should be shown in a gui"); PWARN ("unknown mode %s, will use single-update mode", start ? start : "(null)"); be->session_mode = MODE_SINGLE_UPDATE; } } else /* username and password */ if ((0 == strncasecmp (start, "username=", 9)) || (0 == strncasecmp (start, "user=", 5)) || (0 == strncasecmp (start, "login=", 6))) { start = strchr (start, '=') +1; be->username = g_strdup (start); } else if ((0 == strncasecmp (start, "password=", 9)) || (0 == strncasecmp (start, "passwd=", 7)) || (0 == strncasecmp (start, "pass=", 5)) || (0 == strncasecmp (start, "pwd=", 4))) { start = strchr (start, '=') +1; password = start; if (0 == strcmp (password, "''")) password = ""; } else /* postgres-specific options and debug tty */ if (0 == strncasecmp (start, "options=", 8)) { start = strchr (start, '=') +1; pg_options = start; } else if (0 == strncasecmp (start, "tty=", 4)) { start = strchr (start, '=') +1; pg_tty = start; } else /* ignore other postgres-specific keywords */ if ((0 == strncasecmp (start, "host=", 5)) || (0 == strncasecmp (start, "port=", 5)) || (0 == strncasecmp (start, "dbname=", 7)) || (0 == strncasecmp (start, "authtype=", 9))) { PWARN ("the following message should be shown in a gui"); PWARN ("ignoring the postgres keyword %s", start ? start : "(null)"); } else { PWARN ("the following message should be shown in a gui"); PWARN ("unknown keyword %s, ignoring", start ? start : "(null)"); } } /* handle localhost as a special case */ if (!safe_strcmp("localhost", be->hostname)) { g_free (be->hostname); be->hostname = NULL; } #ifdef NEW_LOGIN /* not fully implemented */ /* Login algorithm. First, we connect to a default, existing * database. (Hopefully it allows any username and password * to connect. We have a problem we don't know how to recover * from if we can't connect to this.) We then query pg_database * to see if the desired dabase exists. (We have a problem if * the dbadmin has set permissions to prevent this query.) * If the user-named db exists, then we connect to it, otherwise * we create it before connecting. */ be->connection = PQsetdbLogin (be->hostname, be->portno, pg_options, /* trace/debug options */ pg_tty, /* file or tty for debug output */ "template1", be->username, /* login */ password); /* pwd */ /* check the connection status */ if (CONNECTION_BAD == PQstatus(be->connection)) { PWARN("Can't connect to default database 'template1':\n" "\t%s", PQerrorMessage(be->connection)); PQfinish (be->connection); /* Well, maybe the user-requested database exists, and we * can connect to that ... */ be->connection = PQsetdbLogin (be->hostname, be->portno, pg_options, /* trace/debug options */ pg_tty, /* file or tty for debug output */ be->dbName, be->username, /* login */ password); /* pwd */ /* check the connection status */ if (CONNECTION_BAD == PQstatus(be->connection)) { PWARN("Connection to database '%s' failed:\n" "\t%s", be->dbName ? be->dbName : "(null)", PQerrorMessage(be->connection)); PQfinish (be->connection); be->connection = NULL; /* OK, this part is convoluted. * I wish that postgres returned usable error codes. * Alas, it does not, so we just bomb out. */ xaccBackendSetError (&be->be, ERR_BACKEND_CANT_CONNECT); return; } } else { /* if we are here, then we're connected to 'template1'. * Look for entry in the system table pgdatabase i.e. * SELECT datname FROM pg_database; this should tell us * if it exists already, or if it needs to be created. */ PERR ("not implemented"); } #else /* Old login algorithm. We try to connect to the database that * the user requested. If it fails, we get a fatal message from * postgres. (Porblem: we don't really know why there was a fatal * error, there may be many reasons. This is the fundamental * problem with this approach.) If the connect failed, then we * create teh database, and try again. */ be->connection = PQsetdbLogin (be->hostname, be->portno, pg_options, /* trace/debug options */ pg_tty, /* file or tty for debug output */ be->dbName, be->username, /* login */ password); /* pwd */ /* check the connection status */ if (CONNECTION_BAD == PQstatus(be->connection)) { PWARN("Connection to database '%s' failed:\n" "\t%s", be->dbName ? be->dbName : "(null)", PQerrorMessage(be->connection)); PQfinish (be->connection); be->connection = NULL; /* OK, this part is convoluted. * I wish that postgres returned usable error codes. * Alas, it does not, so we guess the true error. * If the host is 'localhost', and we couldn't connect, * then we assume that its because the database doesn't * exist (although this might also happen if the database * existed, but the user supplied a bad username/password) */ if (NULL == be->hostname) { if (create_new_db) { really_do_create = TRUE; } else { xaccBackendSetError (&be->be, ERR_BACKEND_NO_SUCH_DB); return; } } else { xaccBackendSetError (&be->be, ERR_BACKEND_CANT_CONNECT); return; } } if (really_do_create) { char * p; be->connection = PQsetdbLogin (be->hostname, be->portno, pg_options, /* trace/debug options */ pg_tty, /* file or tty for debug output */ "template1", be->username, /* login */ password); /* pwd */ /* check the connection status */ if (CONNECTION_BAD == PQstatus(be->connection)) { PERR("Can't connect to database 'template1':\n" "\t%s", PQerrorMessage(be->connection)); PQfinish (be->connection); be->connection = NULL; xaccBackendSetError (&be->be, ERR_BACKEND_CANT_CONNECT); return; } /* create the database */ p = be->buff; *p =0; p = stpcpy (p, "CREATE DATABASE "); p = stpcpy (p, be->dbName); p = stpcpy (p, ";"); SEND_QUERY (be,be->buff, ); FINISH_QUERY(be->connection); PQfinish (be->connection); /* now connect to the newly created database */ be->connection = PQsetdbLogin (be->hostname, be->portno, pg_options, /* trace/debug options */ pg_tty, /* file or tty for debug output */ be->dbName, be->username, /* login */ password); /* pwd */ /* check the connection status */ if (CONNECTION_BAD == PQstatus(be->connection)) { PERR("Can't connect to the newly created database '%s':\n" "\t%s", be->dbName ? be->dbName : "(null)", PQerrorMessage(be->connection)); PQfinish (be->connection); be->connection = NULL; xaccBackendSetError (&be->be, ERR_BACKEND_CANT_CONNECT); return; } /* Finally, create all the tables and indexes. * We do this in pieces, so as not to exceed the max length * for postgres queries (which is 8192). */ SEND_QUERY (be,table_create_str, ); FINISH_QUERY(be->connection); SEND_QUERY (be,table_audit_str, ); FINISH_QUERY(be->connection); SEND_QUERY (be,sql_functions_str, ); FINISH_QUERY(be->connection); } #endif /* free url only after login completed */ g_free(url); // DEBUGCMD (PQtrace(be->connection, stderr)); /* set the datestyle to something we can parse */ bufp = "SET DATESTYLE='ISO';"; SEND_QUERY (be,bufp, ); FINISH_QUERY(be->connection); /* OK, lets see if we can get a valid session */ rc = pgendSessionValidate (be, ignore_lock); /* set up pointers for appropriate behaviour */ if (rc) { switch (be->session_mode) { case MODE_SINGLE_FILE: pgendEnable(be); be->be.book_load = pgend_book_load_single; be->be.price_load = pgend_price_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.price_begin_edit = NULL; be->be.price_commit_edit = NULL; be->be.run_query = NULL; be->be.price_lookup = NULL; be->be.sync = pgendSyncSingleFile; be->be.sync_price = pgendSyncPriceDBSingleFile; PWARN ("MODE_SINGLE_FILE is final beta -- \n" "we've fixed all known bugs but that doesn't mean\n" "there aren't any! We think its safe to use.\n"); break; case MODE_SINGLE_UPDATE: pgendEnable(be); be->be.book_load = pgend_book_load_single; be->be.price_load = pgend_price_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.price_begin_edit = pgend_price_begin_edit; be->be.price_commit_edit = pgend_price_commit_edit; be->be.run_query = NULL; be->be.price_lookup = NULL; be->be.sync = pgendSync; be->be.sync_price = pgendSyncPriceDB; PWARN ("MODE_SINGLE_UPDATE is final beta -- \n" "we've fixed all known bugs but that doesn't mean\n" "there aren't any! We think its safe to use.\n"); break; case MODE_POLL: pgendEnable(be); be->be.book_load = pgend_book_load_poll; be->be.price_load = pgend_price_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.price_begin_edit = pgend_price_begin_edit; be->be.price_commit_edit = pgend_price_commit_edit; be->be.run_query = pgendRunQuery; be->be.price_lookup = pgendPriceLookup; // be->be.sync = pgendSync; be->be.sync = NULL; be->be.sync_price = pgendSyncPriceDB; PWARN ("MODE_POLL is alpha -- \n" "there are a few unfixed bugs, but maybe this mode is usable.\n" "It might still corrupt your data, we're not sure yet.\n"); break; case MODE_EVENT: PERR ("MODE_EVENT is unimplemented"); break; default: PERR ("bad mode specified"); break; } } LEAVE("be=%p, sessionid=%s", be, sessionid ? sessionid : "(null)"); } /* ============================================================= */ static void pgendDisable (PGBackend *be) { if (0 > be->nest_count) { PERR ("too many nested enables"); } be->nest_count ++; PINFO("nest count=%d", be->nest_count); 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.price_begin_edit = be->be.price_begin_edit; be->snr.price_commit_edit = be->be.price_commit_edit; be->snr.run_query = be->be.run_query; be->snr.price_lookup = be->be.price_lookup; be->snr.sync = be->be.sync; be->snr.sync_price = be->be.sync_price; 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.price_begin_edit = NULL; be->be.price_commit_edit = NULL; be->be.run_query = NULL; be->be.price_lookup = NULL; be->be.sync = NULL; be->be.sync_price = NULL; } /* ============================================================= */ static void pgendEnable (PGBackend *be) { if (0 >= be->nest_count) { PERR ("too many nested disables"); } be->nest_count --; PINFO("nest count=%d", be->nest_count); if (be->nest_count) return; /* 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.price_begin_edit = be->snr.price_begin_edit; be->be.price_commit_edit = be->snr.price_commit_edit; be->be.run_query = be->snr.run_query; be->be.price_lookup = be->snr.price_lookup; be->be.sync = be->snr.sync; be->be.sync_price = be->snr.sync_price; } /* ============================================================= */ /* The pgendInit() routine initializes the backend private * structures, mallocs any needed memory, etc. */ static void pgendInit (PGBackend *be) { int i; /* initialize global variable */ nullguid = *(xaccGUIDNULL()); /* access mode */ be->session_mode = MODE_SINGLE_UPDATE; be->sessionGuid = NULL; /* generic backend handlers */ be->be.book_begin = pgend_session_begin; be->be.book_load = NULL; be->be.price_load = NULL; be->be.book_end = pgend_session_end; 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.price_begin_edit = NULL; be->be.price_commit_edit = NULL; be->be.run_query = NULL; be->be.price_lookup = NULL; be->be.sync = NULL; be->be.sync_price = NULL; be->be.events_pending = NULL; be->be.process_events = NULL; be->nest_count = 0; pgendDisable(be); be->be.last_err = ERR_BACKEND_NO_ERR; /* postgres specific data */ be->hostname = NULL; be->portno = NULL; be->dbName = NULL; be->username = NULL; be->connection = NULL; be->builder = sqlBuilder_new(); be->buff = g_malloc (QBUFSIZE); be->bufflen = QBUFSIZE; be->nrows = 0; #define INIT_CACHE_SZ 1000 be->path_cache = (char **) g_malloc (INIT_CACHE_SZ * sizeof(char *)); be->path_cache_size = INIT_CACHE_SZ; for (i=0; i< be->path_cache_size; i++) { (be->path_cache)[i] = NULL; } be->ipath_max = 0; be->topgroup = NULL; } /* ============================================================= */ Backend * pgendNew (void) { PGBackend *be; be = g_new0 (PGBackend, 1); pgendInit (be); return (Backend *) be; } /* ======================== END OF FILE ======================== */