mirror of
https://github.com/Gnucash/gnucash.git
synced 2025-02-25 18:55:30 -06:00
git-svn-id: svn+ssh://svn.gnucash.org/repo/gnucash/branches/gnucash-gnome2-dev@9638 57a11ea4-9604-0410-9ed3-97b8803252fd
2548 lines
75 KiB
C
2548 lines
75 KiB
C
/********************************************************************\
|
|
* PostgresBackend.c -- implements postgres backend - main file *
|
|
* Copyright (c) 2000, 2001, 2002 Linas Vepstas <linas@linas.org> *
|
|
* *
|
|
* 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 *
|
|
\********************************************************************/
|
|
|
|
|
|
#define _GNU_SOURCE
|
|
|
|
#include "config.h"
|
|
|
|
#include <ctype.h>
|
|
#include <glib.h>
|
|
#include <netdb.h>
|
|
#include <pwd.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/types.h>
|
|
#include <unistd.h>
|
|
#if HAVE_LANGINFO_CODESET
|
|
#include <langinfo.h>
|
|
#endif
|
|
#include <libpq-fe.h>
|
|
|
|
#include "AccountP.h"
|
|
#include "Group.h"
|
|
#include "GroupP.h"
|
|
#include "gnc-commodity.h"
|
|
#include "gnc-engine.h"
|
|
#include "gnc-engine-util.h"
|
|
#include "gnc-event.h"
|
|
#include "gnc-event-p.h"
|
|
#include "gnc-pricedb.h"
|
|
#include "gnc-pricedb-p.h"
|
|
#include "guid.h"
|
|
#include "qofbackend.h"
|
|
#include "qofbackend-p.h"
|
|
#include "qofid.h"
|
|
#include "qofid-p.h"
|
|
#include "qofbook.h"
|
|
#include "qofbook-p.h"
|
|
#include "TransactionP.h"
|
|
|
|
#include "account.h"
|
|
#include "book.h"
|
|
#include "builder.h"
|
|
#include "checkpoint.h"
|
|
#include "events.h"
|
|
#include "gncquery.h"
|
|
#include "kvp-sql.h"
|
|
#include "messages.h"
|
|
#include "PostgresBackend.h"
|
|
#include "price.h"
|
|
#include "txn.h"
|
|
#include "txnmass.h"
|
|
#include "upgrade.h"
|
|
|
|
static short module = MOD_BACKEND;
|
|
|
|
#include "putil.h"
|
|
|
|
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;
|
|
}
|
|
|
|
/* ============================================================= */
|
|
|
|
Account *
|
|
pgendAccountLookup (PGBackend *be, const GUID *acct_guid)
|
|
{
|
|
GList *node;
|
|
Account * acc = NULL;
|
|
|
|
ENTER("guid = %s", guid_to_string(acct_guid));
|
|
for (node=be->blist; node; node=node->next)
|
|
{
|
|
QofBook *book = node->data;
|
|
acc = xaccAccountLookup (acct_guid, book);
|
|
if (acc) { LEAVE("acc = %p", acc); return acc; }
|
|
}
|
|
|
|
LEAVE("acc = (null)");
|
|
return NULL;
|
|
}
|
|
|
|
Transaction *
|
|
pgendTransLookup (PGBackend *be, const GUID *txn_guid)
|
|
{
|
|
GList *node;
|
|
Transaction * txn = NULL;
|
|
|
|
ENTER("guid = %s", guid_to_string(txn_guid));
|
|
for (node=be->blist; node; node=node->next)
|
|
{
|
|
QofBook *book = node->data;
|
|
txn = xaccTransLookup (txn_guid, book);
|
|
if (txn) { LEAVE("txt = %p", txn); return txn; }
|
|
}
|
|
|
|
LEAVE("txn = (null");
|
|
return NULL;
|
|
}
|
|
|
|
Split *
|
|
pgendSplitLookup (PGBackend *be, const GUID *split_guid)
|
|
{
|
|
GList *node;
|
|
Split * split = NULL;
|
|
|
|
ENTER("guid = %s", guid_to_string(split_guid));
|
|
for (node=be->blist; node; node=node->next)
|
|
{
|
|
QofBook *book = node->data;
|
|
split = xaccSplitLookup (split_guid, book);
|
|
if (split) { LEAVE("split = %p", split); return split; }
|
|
}
|
|
|
|
LEAVE("split = (null)");
|
|
return NULL;
|
|
}
|
|
|
|
struct _iter {
|
|
const GUID *guid;
|
|
QofEntity *ent;
|
|
};
|
|
|
|
static void
|
|
cforeach (QofCollection *col, gpointer data)
|
|
{
|
|
struct _iter *iter = data;
|
|
if (iter->ent) return;
|
|
iter->ent = qof_collection_lookup_entity (col, iter->guid);
|
|
}
|
|
|
|
QofIdType
|
|
pgendGUIDType (PGBackend *be, const GUID *guid)
|
|
{
|
|
GList *node;
|
|
struct _iter iter;
|
|
|
|
iter.guid = guid;
|
|
iter.ent = NULL;
|
|
|
|
ENTER("guid = %s", guid_to_string(guid));
|
|
for (node=be->blist; node; node=node->next)
|
|
{
|
|
QofBook *book = node->data;
|
|
qof_book_foreach_collection (book, cforeach, &iter);
|
|
if (iter.ent)
|
|
{
|
|
LEAVE("tip = %s", iter.ent->e_type);
|
|
return iter.ent->e_type;
|
|
}
|
|
}
|
|
|
|
LEAVE("tip = NULL");
|
|
return GNC_ID_NONE;
|
|
}
|
|
|
|
/* ============================================================= */
|
|
|
|
QofBook *
|
|
pgendGetBook(PGBackend *pbe) {
|
|
QofBook *book;
|
|
|
|
ENTER(" ");
|
|
book = qof_session_get_book(pbe->session);
|
|
|
|
LEAVE("book = %p", book);
|
|
return book;
|
|
}
|
|
|
|
static void
|
|
pgend_set_book (PGBackend *be, QofBook *book)
|
|
{
|
|
GList *node;
|
|
be->book = book;
|
|
|
|
for (node=be->blist; node; node=node->next)
|
|
{
|
|
if (book == node->data) return;
|
|
}
|
|
|
|
be->blist = g_list_prepend (be->blist, book);
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* This routine finds the commodity by parsing a string
|
|
* of the form NAMESPACE::MNEMONIC
|
|
*/
|
|
|
|
gnc_commodity *
|
|
gnc_string_to_commodity (const char *str, QofBook *book)
|
|
{
|
|
gnc_commodity_table *comtab;
|
|
gnc_commodity *com;
|
|
char *space, *name;
|
|
|
|
comtab = gnc_book_get_commodity_table (book);
|
|
|
|
space = g_strdup(str);
|
|
name = strchr (space, ':');
|
|
|
|
if (!name)
|
|
{
|
|
PERR ("bad commodity string: %s", str ? str : "(null)");
|
|
g_free (space);
|
|
return NULL;
|
|
}
|
|
|
|
*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; j<jrows; j++)
|
|
{
|
|
data = handler (be, result, j, data);
|
|
}
|
|
}
|
|
i++;
|
|
PQclear (result);
|
|
} while (result);
|
|
|
|
return data;
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* version number callback for pgendGetResults */
|
|
|
|
static gpointer
|
|
get_version_cb (PGBackend *be, PGresult *result, int j, gpointer data)
|
|
{
|
|
int version = atoi(DB_GET_VAL ("version", j));
|
|
int incoming = GPOINTER_TO_INT (data);
|
|
if (version < incoming) version = incoming;
|
|
return GINT_TO_POINTER (version);
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* include the auto-generated code */
|
|
|
|
#include "base-autogen.h"
|
|
#include "base-autogen.c"
|
|
|
|
static const char *table_audit_str =
|
|
#include "table-audit.c"
|
|
;
|
|
|
|
static const char *table_create_str =
|
|
#include "table-create.c"
|
|
;
|
|
|
|
static const char *table_version_str =
|
|
#include "table-version.c"
|
|
;
|
|
|
|
static const char *sql_functions_str =
|
|
#include "functions.c"
|
|
;
|
|
|
|
#if 0
|
|
static const char *table_drop_str =
|
|
#include "table-drop.c"
|
|
;
|
|
#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 the 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.
|
|
*/
|
|
|
|
typedef struct
|
|
{
|
|
Transaction * trans;
|
|
char * commodity_string;
|
|
} TransResolveInfo;
|
|
|
|
typedef struct
|
|
{
|
|
GList * xaction_list;
|
|
GList * resolve_list;
|
|
} QueryData;
|
|
|
|
static gpointer
|
|
query_cb (PGBackend *be, PGresult *result, int j, gpointer data)
|
|
{
|
|
QueryData *qd = data;
|
|
GUID trans_guid;
|
|
Transaction *trans;
|
|
gnc_commodity *currency;
|
|
Timespec ts;
|
|
|
|
/* find the transaction this goes into */
|
|
trans_guid = nullguid; /* just in case the read fails ... */
|
|
string_to_guid (DB_GET_VAL("transGUID",j), &trans_guid);
|
|
|
|
/* use markers to avoid redundant traversals of transactions we've
|
|
* already checked recently. */
|
|
trans = pgendTransLookup (be, &trans_guid);
|
|
if (NULL != trans)
|
|
{
|
|
if (0 != trans->marker)
|
|
{
|
|
return qd;
|
|
}
|
|
else
|
|
{
|
|
gint32 db_version, cache_version;
|
|
db_version = atoi (DB_GET_VAL("version",j));
|
|
cache_version = xaccTransGetVersion (trans);
|
|
if (db_version <= cache_version) {
|
|
return qd;
|
|
}
|
|
xaccTransBeginEdit (trans);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
trans = xaccMallocTransaction(pgendGetBook(be));
|
|
xaccTransBeginEdit (trans);
|
|
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)));
|
|
trans->idata = atoi(DB_GET_VAL("iguid",j));
|
|
|
|
currency = gnc_string_to_commodity (DB_GET_VAL("currency",j),
|
|
pgendGetBook(be));
|
|
if (currency)
|
|
xaccTransSetCurrency (trans, currency);
|
|
else
|
|
{
|
|
TransResolveInfo * ri = g_new0 (TransResolveInfo, 1);
|
|
|
|
ri->trans = trans;
|
|
ri->commodity_string = g_strdup (DB_GET_VAL("currency",j));
|
|
|
|
qd->resolve_list = g_list_prepend (qd->resolve_list, ri);
|
|
}
|
|
|
|
trans->marker = 1;
|
|
qd->xaction_list = g_list_prepend (qd->xaction_list, trans);
|
|
|
|
return qd;
|
|
}
|
|
|
|
typedef struct acct_earliest
|
|
{
|
|
Account *acct;
|
|
Timespec ts;
|
|
} AcctEarliest;
|
|
|
|
static int ncalls = 0;
|
|
|
|
static void
|
|
pgendFillOutToCheckpoint (PGBackend *be, const char *query_string)
|
|
{
|
|
int call_count = ncalls;
|
|
int nact=0;
|
|
QueryData qd;
|
|
GList *node, *anode, *acct_list = NULL;
|
|
|
|
qd.xaction_list = NULL;
|
|
qd.resolve_list = NULL;
|
|
|
|
ENTER (" ");
|
|
if (!be) return;
|
|
|
|
if (0 == ncalls) {
|
|
START_CLOCK (9, "starting at level 0");
|
|
}
|
|
else
|
|
{
|
|
REPORT_CLOCK (9, "call count %d", call_count);
|
|
}
|
|
ncalls ++;
|
|
|
|
SEND_QUERY (be, query_string, );
|
|
pgendGetResults (be, query_cb, &qd);
|
|
REPORT_CLOCK (9, "fetched results at call %d", call_count);
|
|
|
|
for (node = qd.resolve_list; node; node = node->next)
|
|
{
|
|
TransResolveInfo * ri = node->data;
|
|
gnc_commodity * commodity;
|
|
|
|
pgendGetCommodity (be, ri->commodity_string);
|
|
commodity = gnc_string_to_commodity (ri->commodity_string,
|
|
pgendGetBook(be));
|
|
|
|
if (commodity)
|
|
{
|
|
xaccTransSetCurrency (ri->trans, commodity);
|
|
}
|
|
else
|
|
{
|
|
PERR ("Can't find commodity %s", ri->commodity_string);
|
|
}
|
|
|
|
g_free (ri->commodity_string);
|
|
ri->commodity_string = NULL;
|
|
|
|
g_free (ri);
|
|
node->data = NULL;
|
|
}
|
|
g_list_free (qd.resolve_list);
|
|
qd.resolve_list = NULL;
|
|
|
|
/* restore the splits for these transactions */
|
|
for (node = qd.xaction_list; node; node = node->next)
|
|
{
|
|
Transaction *trans = (Transaction *) node->data;
|
|
|
|
pgendCopySplitsToEngine (be, trans);
|
|
}
|
|
|
|
/* restore any kvp data associated with the transaction and splits */
|
|
for (node = qd.xaction_list; node; node = node->next)
|
|
{
|
|
Transaction *trans = (Transaction *) node->data;
|
|
GList *engine_splits, *snode;
|
|
|
|
trans->inst.kvp_data = pgendKVPFetch (be, trans->idata, trans->inst.kvp_data);
|
|
|
|
engine_splits = xaccTransGetSplitList(trans);
|
|
for (snode = engine_splits; snode; snode=snode->next)
|
|
{
|
|
Split *s = snode->data;
|
|
s->kvp_data = pgendKVPFetch (be, s->idata, s->kvp_data);
|
|
}
|
|
|
|
xaccTransCommitEdit (trans);
|
|
}
|
|
|
|
/* run the fill-out algorithm */
|
|
for (node = qd.xaction_list; node; node = node->next)
|
|
{
|
|
Transaction *trans = (Transaction *) node->data;
|
|
GList *split_list, *snode;
|
|
Timespec ts;
|
|
|
|
ts = xaccTransRetDatePostedTS (trans);
|
|
|
|
/* Back off by a second to disambiguate time.
|
|
* This is safe, because the fill-out will recurse
|
|
* if something got into this one-second gap. */
|
|
ts.tv_sec --;
|
|
split_list = xaccTransGetSplitList (trans);
|
|
for (snode=split_list; snode; snode=snode->next)
|
|
{
|
|
int found = 0;
|
|
Split *s = (Split *) snode->data;
|
|
Account *acc = xaccSplitGetAccount (s);
|
|
GList *splits;
|
|
|
|
/* make sure the earliest split is first */
|
|
xaccAccountSortSplits (acc, TRUE);
|
|
|
|
splits = xaccAccountGetSplitList (acc);
|
|
|
|
/* See if we already have a split (acc_split)
|
|
* earlier than this transaction. Then either:
|
|
*
|
|
* 1) The acc_split was pulled in by the same
|
|
* query which brought in the current split
|
|
* and we will get to acc_split later.
|
|
* 2) The acc_split was loaded already and
|
|
* the account starting balance is correct
|
|
* up to that point.
|
|
*
|
|
* Either way, we can ignore the current split
|
|
* for the purposes of checkpointing.
|
|
*/
|
|
if (splits)
|
|
{
|
|
Split *acc_split = splits->data;
|
|
Transaction *t = xaccSplitGetParent (acc_split);
|
|
Timespec ts_2 = xaccTransRetDatePostedTS (t);
|
|
|
|
if (timespec_cmp (&ts_2, &ts) < 0)
|
|
continue;
|
|
}
|
|
|
|
/* 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);
|
|
}
|
|
}
|
|
}
|
|
g_list_free (qd.xaction_list);
|
|
qd.xaction_list = NULL;
|
|
|
|
REPORT_CLOCK (9, "done gathering at call %d", call_count);
|
|
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 running balances to that point,
|
|
* and then all of the splits from that date onwards.
|
|
*/
|
|
nact = 0;
|
|
for (anode = acct_list; anode; anode = anode->next)
|
|
{
|
|
char *p;
|
|
AcctEarliest * ae = (AcctEarliest *) anode->data;
|
|
pgendAccountGetBalance (be, ae->acct, ae->ts);
|
|
|
|
/* n.b. date_posted compare must be strictly greater than, since the
|
|
* GetBalance goes to less-then-or-equal-to because of the BETWEEN
|
|
* that appears in the gncSubTotalBalance sql function. */
|
|
p = be->buff; *p = 0;
|
|
p = stpcpy (p,
|
|
"SELECT DISTINCT gncTransaction.* "
|
|
"FROM gncSplit, gncTransaction WHERE "
|
|
"gncSplit.transGuid = gncTransaction.transGuid AND "
|
|
"gncSplit.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);
|
|
nact ++;
|
|
}
|
|
g_list_free(acct_list);
|
|
|
|
REPORT_CLOCK (9, "done w/ fillout at call %d, handled %d accounts",
|
|
call_count, nact);
|
|
LEAVE (" ");
|
|
}
|
|
|
|
static gpointer
|
|
pgendCompileQuery (QofBackend *bend, Query *q)
|
|
{
|
|
return q;
|
|
}
|
|
|
|
static void
|
|
pgendFreeQuery (QofBackend *bend, gpointer q)
|
|
{
|
|
}
|
|
|
|
static void
|
|
pgendRunQuery (QofBackend *bend, gpointer q_p)
|
|
{
|
|
PGBackend *be = (PGBackend *)bend;
|
|
Query *q = (Query*) q_p;
|
|
const char * sql_query_string;
|
|
AccountGroup *topgroup;
|
|
sqlQuery *sq;
|
|
|
|
ENTER ("be=%p, qry=%p", be, q);
|
|
if (!be || !q) { LEAVE("(null) args"); return; }
|
|
be->version_check = (guint32) time(0);
|
|
|
|
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);
|
|
|
|
topgroup = gnc_book_get_group (pgendGetBook(be));
|
|
|
|
/* stage transactions, save some postgres overhead */
|
|
xaccGroupBeginStagedTransactionTraversals (topgroup);
|
|
|
|
/* We will be doing a bulk insertion of transactions below.
|
|
* We can gain a tremendous performance improvement,
|
|
* for example, a factor of 10x when querying 3000 transactions,
|
|
* by opening all accounts for editing before we start, and
|
|
* closing them all only after we're done. This is because
|
|
* an account must be open for editing in order to insert a split,
|
|
* and when the commit is made, the splits are sorted in date order.
|
|
* If we're sloppy, then there's an ordering for every insertion.
|
|
* By defering the Commit, we defer the sort, and thus save gobs.
|
|
* Of course, this hurts 'shallow' queries some, but I beleive
|
|
* by not very much.
|
|
*/
|
|
ncalls = 0;
|
|
xaccAccountGroupBeginEdit(topgroup);
|
|
pgendFillOutToCheckpoint (be, sql_query_string);
|
|
xaccAccountGroupCommitEdit(topgroup);
|
|
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 (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.
|
|
*
|
|
* NB. This routine has been obsoleted by the more efficient
|
|
* pgendGetMassTransactions(). We keep it around here ...
|
|
* for a rainy day...
|
|
*/
|
|
|
|
#if 0
|
|
static gpointer
|
|
get_all_trans_cb (PGBackend *be, PGresult *result, int j, gpointer data)
|
|
{
|
|
GList *xaction_list = (GList *) data;
|
|
GUID *trans_guid;
|
|
|
|
/* find the transaction this goes into */
|
|
trans_guid = guid_malloc();
|
|
*trans_guid = nullguid; /* just in case the read fails ... */
|
|
string_to_guid (DB_GET_VAL("transGUID",j), trans_guid);
|
|
|
|
xaction_list = g_list_prepend (xaction_list, trans_guid);
|
|
return xaction_list;
|
|
}
|
|
|
|
|
|
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, get_all_trans_cb, xaction_list);
|
|
|
|
/* restore the transactions */
|
|
xaccAccountGroupBeginEdit (grp);
|
|
for (node=xaction_list; node; node=node->next)
|
|
{
|
|
xxxpgendCopyTransactionToEngine (be, (GUID *)node->data);
|
|
guid_free (node->data);
|
|
}
|
|
g_list_free(xaction_list);
|
|
xaccAccountGroupCommitEdit (grp);
|
|
|
|
pgendEnable(be);
|
|
gnc_engine_resume_events();
|
|
}
|
|
#endif
|
|
|
|
|
|
/* ============================================================= */
|
|
/* ============================================================= */
|
|
/* HIGHER LEVEL ROUTINES AND BACKEND PROPER */
|
|
/* ============================================================= */
|
|
/* ============================================================= */
|
|
|
|
|
|
/* ============================================================= */
|
|
/* 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.
|
|
*
|
|
* This routine should also check for deleted transactions (that
|
|
* other users have deleted, but are still present in out cache).
|
|
*/
|
|
|
|
|
|
static void
|
|
pgendSync (QofBackend *bend, QofBook *book)
|
|
{
|
|
PGBackend *be = (PGBackend *)bend;
|
|
AccountGroup *grp = gnc_book_get_group (book);
|
|
|
|
ENTER ("be=%p, grp=%p", be, grp);
|
|
|
|
pgend_set_book (be, book);
|
|
be->version_check = (guint32) time(0);
|
|
|
|
/* For the multi-user modes, we allow a save only once,
|
|
* when the database is created for the first time.
|
|
* Ditto for the single-user update mode: it should never
|
|
* wander out of sync.
|
|
*/
|
|
if ((MODE_SINGLE_FILE != be->session_mode) &&
|
|
(FALSE == be->freshly_created_db))
|
|
{
|
|
LEAVE("no sync");
|
|
return;
|
|
}
|
|
be->freshly_created_db = FALSE;
|
|
|
|
pgendStoreBook (be, book);
|
|
|
|
/* 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);
|
|
pgendGetAllAccountsInBook (be, book);
|
|
if ((MODE_SINGLE_FILE != be->session_mode) &&
|
|
(MODE_SINGLE_UPDATE != be->session_mode))
|
|
{
|
|
Timespec ts = gnc_iso8601_to_timespec_local (CK_BEFORE_LAST_DATE);
|
|
pgendGroupGetAllBalances (be, grp, ts);
|
|
}
|
|
else
|
|
{
|
|
/* in single user mode, read all the transactions */
|
|
pgendGetMassTransactions (be, book);
|
|
}
|
|
|
|
/* hack alert -- In some deranged theory, we should be
|
|
* syncing prices here, as well as syncing any/all other
|
|
* engine structures that need to be stored. But instead,
|
|
* price sync is handled as a separate routine ...
|
|
*/
|
|
|
|
/* 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 int
|
|
trans_traverse_cb (Transaction *trans, void *cb_data)
|
|
{
|
|
pgendStoreTransactionNoLock ((PGBackend *) cb_data, trans, TRUE);
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
pgendSyncSingleFile (QofBackend *bend, QofBook *book)
|
|
{
|
|
char book_guid[40];
|
|
char buff[4000];
|
|
char *p;
|
|
PGBackend *be = (PGBackend *)bend;
|
|
AccountGroup *grp = gnc_book_get_group (book);
|
|
|
|
ENTER ("be=%p, grp=%p", be, grp);
|
|
|
|
pgend_set_book (be, book);
|
|
|
|
/* hack alert -- we shouldn't be doing a global delete,
|
|
* we should only be deleting the stuff that's in this particular
|
|
* book .... */
|
|
p = "BEGIN;\n"
|
|
"LOCK TABLE gncBook IN EXCLUSIVE MODE;\n"
|
|
"LOCK TABLE gncAccount IN EXCLUSIVE MODE;\n"
|
|
"LOCK TABLE gncCommodity IN EXCLUSIVE MODE;\n"
|
|
"LOCK TABLE gncTransaction IN EXCLUSIVE MODE;\n"
|
|
"LOCK TABLE gncSplit IN EXCLUSIVE MODE;\n";
|
|
SEND_QUERY (be,p, );
|
|
FINISH_QUERY(be->connection);
|
|
|
|
guid_to_string_buff (qof_book_get_guid(book), book_guid);
|
|
|
|
/* First, we delete all of the accounts, splits and transactions
|
|
* associated with this book. Its very tempting to just delete
|
|
* everything in the SQL db, but we note that there may be several
|
|
* books stored here, and we want to delete only one book.
|
|
*/
|
|
/* do the one-book equivalent of "DELETE FROM gncTransaction;" */
|
|
p = buff;
|
|
p = stpcpy (p, "DELETE FROM gncTransaction WHERE "
|
|
" gncTransaction.transGuid = gncSplit.transGuid AND "
|
|
" gncSplit.accountGuid = gncAccount.accountGuid AND "
|
|
" gncAccount.bookGuid = '");
|
|
p = stpcpy (p, book_guid);
|
|
p = stpcpy (p, "';");
|
|
SEND_QUERY (be,buff, );
|
|
FINISH_QUERY(be->connection);
|
|
|
|
/* do the one-book equivalent of "DELETE FROM gncSplit;" */
|
|
p = buff;
|
|
p = stpcpy (p, "DELETE FROM gncSplit WHERE "
|
|
" gncSplit.accountGuid = gncAccount.accountGuid AND "
|
|
" gncAccount.bookGuid = '");
|
|
p = stpcpy (p, book_guid);
|
|
p = stpcpy (p, "';");
|
|
SEND_QUERY (be,buff, );
|
|
FINISH_QUERY(be->connection);
|
|
|
|
|
|
/* do the one-book equivalent of "DELETE FROM gncAccount;" */
|
|
p = buff;
|
|
p = stpcpy (p, "DELETE FROM gncAccount WHERE bookGuid='");
|
|
p = stpcpy (p, book_guid);
|
|
p = stpcpy (p, "';");
|
|
SEND_QUERY (be,buff, );
|
|
FINISH_QUERY(be->connection);
|
|
|
|
/* store the book struct */
|
|
pgendStoreBookNoLock (be, book, TRUE);
|
|
|
|
/* 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);
|
|
|
|
/* hack alert -- In some deranged theory, we should be
|
|
* syncing prices here, as well as syncing any/all other
|
|
* engine structures that need to be stored. But instead,
|
|
* price sync is handled as a separate routine ...
|
|
*/
|
|
|
|
|
|
p = "COMMIT;";
|
|
SEND_QUERY (be,p, );
|
|
FINISH_QUERY(be->connection);
|
|
|
|
LEAVE(" ");
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* Please read the comment for pgendSync to truly understand
|
|
* how this routine works. Its somewhat subtle.
|
|
*/
|
|
|
|
static void
|
|
pgendSyncPriceDB (QofBackend *bend, QofBook *book)
|
|
{
|
|
PGBackend *be = (PGBackend *)bend;
|
|
ENTER ("be=%p", be);
|
|
if (!be) return;
|
|
|
|
pgend_set_book (be, book);
|
|
be->version_check = (guint32) time(0);
|
|
|
|
/* for the multi-user modes, we allow a save only once,
|
|
* when the database is created for the first time */
|
|
if ((MODE_SINGLE_FILE != be->session_mode) &&
|
|
(MODE_SINGLE_UPDATE != be->session_mode) &&
|
|
(FALSE == be->freshly_created_prdb))
|
|
{
|
|
LEAVE("no sync");
|
|
return;
|
|
}
|
|
be->freshly_created_prdb = FALSE;
|
|
|
|
pgendStorePriceDB (be, book);
|
|
|
|
/* don't send events to GUI, don't accept callbacks to backend */
|
|
gnc_engine_suspend_events();
|
|
pgendDisable(be);
|
|
|
|
pgendGetAllPricesInBook (be, book);
|
|
|
|
/* 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 (QofBackend *bend, QofBook *book)
|
|
{
|
|
char buff[400], *p;
|
|
PGBackend *be = (PGBackend *)bend;
|
|
ENTER ("be=%p", be);
|
|
|
|
pgend_set_book (be, book);
|
|
|
|
p = buff;
|
|
p = stpcpy (p, "BEGIN;\n"
|
|
"LOCK TABLE gncPrice IN EXCLUSIVE MODE;\n"
|
|
"DELETE FROM gncPrice WHERE bookGuid='\n");
|
|
p = guid_to_string_buff (qof_book_get_guid(book), p);
|
|
p = stpcpy (p, "';");
|
|
SEND_QUERY (be,buff, );
|
|
FINISH_QUERY(be->connection);
|
|
|
|
/* Store accounts and commodities */
|
|
pgendStorePriceDBNoLock (be, book);
|
|
|
|
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";
|
|
/* quiet compiler warnings about MODE_NONE */
|
|
default:
|
|
return "ERROR";
|
|
}
|
|
return "ERROR";
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* Instead of loading the book, just set the lock error */
|
|
|
|
static void
|
|
pgend_book_load_single_lockerr (QofBackend *bend, QofBook *book)
|
|
{
|
|
PGBackend *be = (PGBackend *)bend;
|
|
|
|
if (!be) return;
|
|
|
|
qof_backend_set_error (&be->be, ERR_BACKEND_LOCKED);
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* 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 == GPOINTER_TO_INT (data)) return GINT_TO_POINTER (TRUE);
|
|
|
|
if (0 == strcmp ("gncsession", (DB_GET_VAL ("tablename", j))))
|
|
return GINT_TO_POINTER (TRUE);
|
|
return GINT_TO_POINTER (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 = GPOINTER_TO_INT (pgendGetResults (be,
|
|
is_gnucash_cb,
|
|
GINT_TO_POINTER (FALSE)));
|
|
if (FALSE == retval) {
|
|
qof_backend_set_error (&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);
|
|
|
|
/* Instead of doing a simple FINISH_QUERY(be->connection);
|
|
* We need to see if we actually have permission to lock
|
|
* the table. If its not lockable, then user doesn't have
|
|
* perms. */
|
|
retval = TRUE;
|
|
{
|
|
int i=0;
|
|
PGresult *result;
|
|
do {
|
|
ExecStatusType status;
|
|
result = PQgetResult(be->connection);
|
|
if (!result) break;
|
|
PINFO ("clearing result %d", i);
|
|
status = PQresultStatus(result);
|
|
if (PGRES_COMMAND_OK != status) {
|
|
PINFO("cannot lock:\n"
|
|
"\t%s", PQerrorMessage(be->connection));
|
|
retval = FALSE;
|
|
}
|
|
PQclear(result);
|
|
i++;
|
|
} while (result);
|
|
}
|
|
|
|
if (FALSE == retval)
|
|
{
|
|
qof_backend_set_error (&be->be, ERR_BACKEND_PERM);
|
|
p = "ROLLBACK;";
|
|
SEND_QUERY (be,p, FALSE);
|
|
FINISH_QUERY(be->connection);
|
|
return FALSE;
|
|
}
|
|
|
|
|
|
/* 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.load = pgend_book_load_single_lockerr;
|
|
qof_backend_set_error (&be->be, ERR_BACKEND_LOCKED);
|
|
retval = FALSE;
|
|
} else {
|
|
|
|
/* make note of the session. */
|
|
be->sessionGuid = guid_malloc();
|
|
guid_new (be->sessionGuid);
|
|
guid_to_string_buff (be->sessionGuid, be->session_guid_str);
|
|
pgendStoreOneSessionOnly (be, (void *)-1, SQL_INSERT);
|
|
retval = TRUE;
|
|
}
|
|
|
|
p = "COMMIT;\n"
|
|
"NOTIFY gncSession;";
|
|
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;
|
|
|
|
#if 0
|
|
/* vacuuming w/ analyze can improve performance 20%.
|
|
* Should this really be done on every session end?
|
|
* The postgres manual recommends once every night! */
|
|
p = be->buff; *p=0;
|
|
p = stpcpy (p, "VACUUM ANALYZE;\n");
|
|
|
|
SEND_QUERY (be,be->buff, );
|
|
FINISH_QUERY(be->connection);
|
|
#endif
|
|
|
|
p = be->buff; *p=0;
|
|
p = stpcpy (p, "UPDATE gncSession SET time_off='NOW' "
|
|
"WHERE sessionGuid='");
|
|
p = stpcpy (p, be->session_guid_str);
|
|
p = stpcpy (p, "';\n"
|
|
"NOTIFY gncSession;");
|
|
|
|
SEND_QUERY (be,be->buff, );
|
|
FINISH_QUERY(be->connection);
|
|
|
|
guid_free (be->sessionGuid); be->sessionGuid = NULL;
|
|
guid_to_string_buff (&nullguid, be->session_guid_str);
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* 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 (QofBackend *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.session_begin = NULL;
|
|
be->be.session_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 void
|
|
pgend_book_load_poll (QofBackend *bend, QofBook *book)
|
|
{
|
|
Timespec ts = gnc_iso8601_to_timespec_local (CK_BEFORE_LAST_DATE);
|
|
AccountGroup *grp;
|
|
PGBackend *be = (PGBackend *)bend;
|
|
|
|
if (!be) return;
|
|
|
|
/* don't send events to GUI, don't accept callbacks to backend */
|
|
gnc_engine_suspend_events();
|
|
pgendDisable(be);
|
|
be->version_check = (guint32) time(0);
|
|
|
|
pgendKVPInit(be);
|
|
|
|
if (be->blist)
|
|
{
|
|
PWARN ("old book list not empty--clearing it out ");
|
|
/* XXX not clear what this means ... should we free old books ?? */
|
|
g_list_free (be->blist);
|
|
be->blist = NULL;
|
|
}
|
|
pgendBookRestore (be, book);
|
|
pgend_set_book (be, book);
|
|
|
|
PINFO("Book GUID = %s\n",
|
|
guid_to_string(qof_book_get_guid(book)));
|
|
|
|
pgendGetAllAccountsInBook (be, book);
|
|
|
|
grp = gnc_book_get_group (book);
|
|
xaccAccountGroupBeginEdit (grp);
|
|
pgendGroupGetAllBalances (be, grp, ts);
|
|
xaccAccountGroupCommitEdit (grp);
|
|
|
|
/* re-enable events */
|
|
pgendEnable(be);
|
|
gnc_engine_resume_events();
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* 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 checkpoints, to these are
|
|
* not handled.
|
|
*/
|
|
|
|
static void
|
|
pgend_book_load_single (QofBackend *bend, QofBook *book)
|
|
{
|
|
PGBackend *be = (PGBackend *)bend;
|
|
|
|
if (!be) return;
|
|
|
|
|
|
/* don't send events to GUI, don't accept callbacks to backend */
|
|
gnc_engine_suspend_events();
|
|
pgendDisable(be);
|
|
be->version_check = (guint32) time(0);
|
|
|
|
pgendKVPInit(be);
|
|
|
|
if (be->blist)
|
|
{
|
|
PWARN ("old book list not empty--clearing it out ");
|
|
/* XXX not clear what this means ... should we free old books ?? */
|
|
g_list_free (be->blist);
|
|
be->blist = NULL;
|
|
}
|
|
pgendBookRestore (be, book);
|
|
pgend_set_book (be, book);
|
|
|
|
pgendGetAllAccountsInBook (be, book);
|
|
|
|
pgendGetMassTransactions (be, book);
|
|
|
|
/* re-enable events */
|
|
pgendEnable(be);
|
|
gnc_engine_resume_events();
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* The pgend_price_load_single() routine loads the engine with
|
|
* price data from the database.
|
|
*/
|
|
|
|
static void
|
|
pgend_price_load_single (QofBackend *bend, QofBook *book)
|
|
{
|
|
PGBackend *be = (PGBackend *)bend;
|
|
|
|
ENTER("be = %p", bend);
|
|
|
|
if (!be || !book) { LEAVE("(null) args"); return; }
|
|
|
|
pgend_set_book (be, book);
|
|
|
|
/* don't send events to GUI, don't accept callbacks to backend */
|
|
gnc_engine_suspend_events();
|
|
pgendDisable(be);
|
|
be->version_check = (guint32) time(0);
|
|
|
|
pgendGetAllPricesInBook (be, book);
|
|
|
|
/* re-enable events */
|
|
pgendEnable(be);
|
|
gnc_engine_resume_events();
|
|
LEAVE(" ");
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/*
|
|
* These are the QofBackend Entry Points, which call into the various
|
|
* functions herein.. These are generic, and (eventually) pluggable.
|
|
*
|
|
*/
|
|
|
|
static void
|
|
pgend_do_load_single (QofBackend *bend, QofBook *book)
|
|
{
|
|
ENTER ("be=%p", bend);
|
|
pgend_book_load_single (bend, book);
|
|
pgend_price_load_single (bend, book);
|
|
|
|
/* XXX: Other things to load here.... (dynamic data) */
|
|
LEAVE ("be=%p", bend);
|
|
}
|
|
|
|
static void
|
|
pgendDoSyncSingleFile (QofBackend *bend, QofBook *book)
|
|
{
|
|
ENTER ("be=%p", bend);
|
|
pgendSyncSingleFile (bend, book);
|
|
pgendSyncPriceDBSingleFile (bend, book);
|
|
|
|
/* XXX: Other data types */
|
|
LEAVE ("be=%p", bend);
|
|
}
|
|
|
|
static void
|
|
pgendDoSync (QofBackend *bend, QofBook *book)
|
|
{
|
|
ENTER ("be=%p", bend);
|
|
pgendSync (bend, book);
|
|
pgendSyncPriceDB (bend, book);
|
|
|
|
/* XXX: Other data types */
|
|
LEAVE ("be=%p", bend);
|
|
}
|
|
|
|
static void
|
|
pgend_do_begin (QofBackend *bend, QofIdTypeConst type, gpointer object)
|
|
{
|
|
PGBackend *be = (PGBackend*)bend;
|
|
|
|
ENTER ("be=%p, type=%s", bend, type);
|
|
if (!safe_strcmp (type, GNC_ID_PERIOD))
|
|
return pgend_book_transfer_begin (bend, object);
|
|
|
|
switch (be->session_mode) {
|
|
case MODE_EVENT:
|
|
case MODE_POLL:
|
|
case MODE_SINGLE_UPDATE:
|
|
if (!safe_strcmp (type, GNC_ID_PRICE))
|
|
return pgend_price_begin_edit (bend, object);
|
|
|
|
case MODE_SINGLE_FILE:
|
|
case MODE_NONE:
|
|
break;
|
|
}
|
|
|
|
/* XXX: Add dynamic plug-in here */
|
|
LEAVE ("be=%p, type=%s", bend, type);
|
|
}
|
|
|
|
static void
|
|
pgend_do_commit (QofBackend *bend, QofIdTypeConst type, gpointer object)
|
|
{
|
|
PGBackend *be = (PGBackend*)bend;
|
|
|
|
ENTER ("be=%p, type=%s", bend, type);
|
|
if (!safe_strcmp (type, GNC_ID_PERIOD))
|
|
return pgend_book_transfer_commit (bend, object);
|
|
|
|
switch (be->session_mode) {
|
|
case MODE_EVENT:
|
|
case MODE_POLL:
|
|
case MODE_SINGLE_UPDATE:
|
|
|
|
if (!safe_strcmp (type, GNC_ID_TRANS)) {
|
|
Transaction *txn = (Transaction*) object;
|
|
return pgend_trans_commit_edit (bend, txn, txn->orig);
|
|
}
|
|
|
|
if (!safe_strcmp (type, GNC_ID_PRICE))
|
|
return pgend_price_commit_edit (bend, object);
|
|
|
|
if (!safe_strcmp (type, GNC_ID_ACCOUNT))
|
|
return pgend_account_commit_edit (bend, object);
|
|
|
|
case MODE_SINGLE_FILE:
|
|
case MODE_NONE:
|
|
break;
|
|
|
|
}
|
|
|
|
/* XXX: Add dynamic plug-in here */
|
|
LEAVE ("be=%p, type=%s", bend, type);
|
|
}
|
|
|
|
static void
|
|
pgend_do_rollback (QofBackend *bend, QofIdTypeConst type, gpointer object)
|
|
{
|
|
PGBackend *be = (PGBackend*)bend;
|
|
|
|
ENTER ("be=%p, type=%s", bend, type);
|
|
switch (be->session_mode) {
|
|
case MODE_EVENT:
|
|
case MODE_POLL:
|
|
|
|
if (!safe_strcmp (type, GNC_ID_TRANS))
|
|
return pgend_trans_rollback_edit (bend, object);
|
|
|
|
case MODE_SINGLE_UPDATE:
|
|
case MODE_SINGLE_FILE:
|
|
case MODE_NONE:
|
|
break;
|
|
}
|
|
|
|
/* XXX: Add dynamic plug-in here */
|
|
LEAVE ("be=%p, type=%s", bend, type);
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* 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 gpointer
|
|
has_results_cb (PGBackend *be, PGresult *result, int j, gpointer data)
|
|
{
|
|
return GINT_TO_POINTER (TRUE);
|
|
}
|
|
|
|
static void
|
|
pgend_create_db (PGBackend *be)
|
|
{
|
|
/* 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_version_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);
|
|
be->freshly_created_db = TRUE;
|
|
be->freshly_created_prdb = TRUE;
|
|
}
|
|
|
|
static void
|
|
pgend_session_begin (QofBackend *backend,
|
|
QofSession *session,
|
|
const char * sessionid,
|
|
gboolean ignore_lock,
|
|
gboolean create_new_db)
|
|
{
|
|
int rc;
|
|
PGBackend *be;
|
|
char *url, *start, *end;
|
|
char *password=NULL;
|
|
char *pg_options=NULL;
|
|
char *pg_tty=NULL;
|
|
char *p;
|
|
gboolean db_exists = FALSE;
|
|
|
|
if (!backend) return;
|
|
be = (PGBackend*)backend;
|
|
|
|
ENTER("be=%p, sessionid=%s", be,
|
|
sessionid ? sessionid : "(null)");
|
|
|
|
/* close any dangling sessions from before; reinitialize */
|
|
pgend_session_end ((QofBackend *) be);
|
|
pgendInit (be);
|
|
|
|
be->session = session;
|
|
|
|
if (be->blist)
|
|
{
|
|
/* XXX not clear what this means ... should we free old books ?? */
|
|
PWARN ("old book list not empty ");
|
|
g_list_free (be->blist);
|
|
be->blist = NULL;
|
|
}
|
|
|
|
/* 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))
|
|
{
|
|
qof_backend_set_error (&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)
|
|
{
|
|
qof_backend_set_error (&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-poll")) {
|
|
be->session_mode = MODE_POLL;
|
|
} else
|
|
if (0 == strcasecmp (start, "multi-user")) {
|
|
be->session_mode = MODE_EVENT;
|
|
} else
|
|
{
|
|
PWARN ("the following message should be shown in a gui");
|
|
PWARN ("unknown mode %s, will use multi-user mode",
|
|
start ? start : "(null)");
|
|
qof_backend_set_message((QofBackend *)be, _("Unknown database access mode '%s'. Using default mode: multi-user."),
|
|
start ? start : "(null)");
|
|
be->session_mode = MODE_EVENT;
|
|
}
|
|
|
|
} 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;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- */
|
|
/* New login algorithm. If we haven't been told that we'll
|
|
* need to be creating a database, then lets try to connect,
|
|
* and see if that succeeds. If it fails, we'll tell gui
|
|
* to ask user if the DB needs to be created.
|
|
*/
|
|
if (FALSE == create_new_db)
|
|
{
|
|
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))
|
|
{
|
|
|
|
PINFO("Connection to database '%s' failed:\n"
|
|
"\t%s",
|
|
be->dbName ? be->dbName : "(null)",
|
|
PQerrorMessage(be->connection));
|
|
|
|
PQfinish (be->connection);
|
|
|
|
/* The connection may have failed either because the
|
|
* database doesn't exist, or because there was a
|
|
* network problem, or because the user supplied a
|
|
* bad password or username. Try to tell these apart.
|
|
*/
|
|
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))
|
|
{
|
|
char * msg = PQerrorMessage(be->connection);
|
|
PWARN("Connection to database 'template1' failed:\n"
|
|
"\t%s", msg);
|
|
|
|
PQfinish (be->connection);
|
|
be->connection = NULL;
|
|
|
|
/* I wish that postgres returned usable error codes.
|
|
* Alas, it does not, so we just bomb out.
|
|
*/
|
|
qof_backend_set_error (&be->be, ERR_BACKEND_CANT_CONNECT);
|
|
qof_backend_set_message(&be->be, _("From the Postgresql Server: %s"), msg);
|
|
return;
|
|
}
|
|
|
|
/* If we are here, then we've successfully connected to the
|
|
* server. Now, check to see if database exists */
|
|
p = be->buff; *p = 0;
|
|
p = stpcpy (p, "SELECT datname FROM pg_database "
|
|
" WHERE datname='");
|
|
p = stpcpy (p, be->dbName);
|
|
p = stpcpy (p, "';");
|
|
|
|
SEND_QUERY (be,be->buff, );
|
|
db_exists = GPOINTER_TO_INT(pgendGetResults(be, has_results_cb,
|
|
GINT_TO_POINTER (FALSE)));
|
|
|
|
PQfinish (be->connection);
|
|
be->connection = NULL;
|
|
|
|
if (db_exists)
|
|
{
|
|
/* Weird. We couldn't connect to the database, but it
|
|
* does seem to exist. I presume that this is some
|
|
* sort of access control problem. */
|
|
qof_backend_set_error (&be->be, ERR_BACKEND_PERM);
|
|
return;
|
|
}
|
|
|
|
/* Let GUI know that we connected, but we couldn't find it. */
|
|
qof_backend_set_error (&be->be, ERR_BACKEND_NO_SUCH_DB);
|
|
return;
|
|
}
|
|
|
|
/* Check to see if we have a database version that we can
|
|
* live with */
|
|
rc = pgendDBVersionIsCurrent (be);
|
|
if (rc < 0)
|
|
{
|
|
/* The server is newer than we are, or another error occured,
|
|
* we don't know how to talk to it. The err code is already set. */
|
|
PQfinish (be->connection);
|
|
be->connection = NULL;
|
|
return;
|
|
}
|
|
if (rc > 0)
|
|
{
|
|
/* The server is older than we are; ask user if they want to
|
|
* upgrade the database contents. */
|
|
PQfinish (be->connection);
|
|
be->connection = NULL;
|
|
qof_backend_set_error (&be->be, ERR_SQL_DB_TOO_OLD);
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* If we are here, then we've been asked to create the
|
|
* database. Well, lets do that. But first make sure
|
|
* it really doesn't exist */
|
|
|
|
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("Connection to database '%s' failed:\n"
|
|
"\t%s",
|
|
be->dbName ? be->dbName : "(null)",
|
|
PQerrorMessage(be->connection));
|
|
|
|
PQfinish (be->connection);
|
|
be->connection = NULL;
|
|
|
|
/* I wish that postgres returned usable error codes.
|
|
* Alas, it does not, so we just bomb out.
|
|
*/
|
|
qof_backend_set_error (&be->be, ERR_BACKEND_CANT_CONNECT);
|
|
return;
|
|
}
|
|
|
|
/* If we are here, then we've successfully connected to the
|
|
* server. Now, check to see if database exists */
|
|
p = be->buff; *p = 0;
|
|
p = stpcpy (p, "SELECT datname FROM pg_database "
|
|
" WHERE datname='");
|
|
p = stpcpy (p, be->dbName);
|
|
p = stpcpy (p, "';");
|
|
|
|
SEND_QUERY (be,be->buff, );
|
|
db_exists = GPOINTER_TO_INT (pgendGetResults (be, has_results_cb,
|
|
GINT_TO_POINTER (FALSE)));
|
|
|
|
if (FALSE == db_exists)
|
|
{
|
|
#if HAVE_LANGINFO_CODESET
|
|
char* encoding = nl_langinfo(CODESET);
|
|
#else
|
|
char* encoding = "SQL_ASCII";
|
|
#endif
|
|
if (!strcmp (encoding, "ANSI_X3.4-1968"))
|
|
encoding = "SQL_ASCII";
|
|
|
|
/* create the database */
|
|
p = be->buff; *p =0;
|
|
p = stpcpy (p, "CREATE DATABASE ");
|
|
p = stpcpy (p, be->dbName);
|
|
p = stpcpy (p, " WITH ENCODING = '");
|
|
p = stpcpy (p, encoding);
|
|
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;
|
|
/* We just created the database! If we can't connect now,
|
|
* the server is insane! */
|
|
qof_backend_set_error (&be->be, ERR_BACKEND_SERVER_ERR);
|
|
return;
|
|
}
|
|
|
|
/* Finally, create all the tables and indexes. */
|
|
pgend_create_db (be);
|
|
}
|
|
else
|
|
{
|
|
gboolean gncaccount_exists;
|
|
|
|
/* Database exists, although we were asked to create it.
|
|
* We interpret this to mean that either it's downlevel,
|
|
* and user wants us to upgrade it, or we are installing
|
|
* gnucash tables into an existing database. So do one or
|
|
* the other. */
|
|
|
|
PQfinish (be->connection);
|
|
|
|
/* Connect to the 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))
|
|
{
|
|
PINFO("Can't connect to the database '%s':\n"
|
|
"\t%s",
|
|
be->dbName ? be->dbName : "(null)",
|
|
PQerrorMessage(be->connection));
|
|
PQfinish (be->connection);
|
|
be->connection = NULL;
|
|
|
|
/* Well, if we are here, we were connecting just fine,
|
|
* just not to this database. Therefore, it must be a
|
|
* permission problem.
|
|
*/
|
|
qof_backend_set_error (&be->be, ERR_BACKEND_PERM);
|
|
return;
|
|
}
|
|
|
|
/* See if the gncaccount table exists. If it does not,
|
|
* create all the tables. We assume that there will always
|
|
* be a gncaccount table. */
|
|
p = "SELECT tablename FROM pg_tables WHERE tablename='gncaccount';";
|
|
SEND_QUERY (be,p, );
|
|
gncaccount_exists =
|
|
GPOINTER_TO_INT (pgendGetResults (be, has_results_cb, FALSE));
|
|
|
|
if (!gncaccount_exists)
|
|
{
|
|
pgend_create_db (be);
|
|
}
|
|
else
|
|
{
|
|
rc = pgendDBVersionIsCurrent (be);
|
|
if (0 > rc)
|
|
{
|
|
/* The server is newer than we are, or another error
|
|
* occured, we don't know how to talk to it. The err
|
|
* code is already set. */
|
|
PQfinish (be->connection);
|
|
be->connection = NULL;
|
|
return;
|
|
}
|
|
if (0 < rc)
|
|
{
|
|
gboolean someones_still_on;
|
|
/* The server is older than we are; lets upgrade */
|
|
/* But first, make sure all users are logged off ... */
|
|
p = "BEGIN;\n"
|
|
"LOCK TABLE gncSession IN ACCESS EXCLUSIVE MODE;\n"
|
|
"SELECT time_off FROM gncSession WHERE time_off ='infinity';\n";
|
|
SEND_QUERY (be,p, );
|
|
someones_still_on =
|
|
GPOINTER_TO_INT (pgendGetResults (be, has_results_cb,
|
|
GINT_TO_POINTER (FALSE)));
|
|
if (someones_still_on)
|
|
{
|
|
p = "COMMIT;\n";
|
|
SEND_QUERY (be,p, );
|
|
FINISH_QUERY(be->connection);
|
|
qof_backend_set_error (&be->be, ERR_SQL_DB_BUSY);
|
|
return;
|
|
}
|
|
p = "COMMIT;\n";
|
|
SEND_QUERY (be,p, );
|
|
FINISH_QUERY(be->connection);
|
|
pgendUpgradeDB (be);
|
|
}
|
|
else
|
|
{
|
|
/* Wierd. We were asked to create something that exists.
|
|
* This shouldn't really happen ... */
|
|
PWARN ("Asked to create the database %s,\n"
|
|
"\tbut it already exists!\n"
|
|
"\tThis shouldn't really happen.",
|
|
be->dbName);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* free url only after login completed */
|
|
g_free(url);
|
|
|
|
/* ---------------------------------------------------------------- */
|
|
|
|
// DEBUGCMD (PQtrace(be->connection, stderr));
|
|
|
|
/* set the datestyle to something we can parse */
|
|
p = "SET DATESTYLE='ISO';";
|
|
SEND_QUERY (be,p, );
|
|
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.load = pgend_do_load_single;
|
|
be->be.begin = pgend_do_begin;
|
|
be->be.commit = pgend_do_commit;
|
|
be->be.rollback = pgend_do_rollback;
|
|
|
|
be->be.compile_query = NULL;
|
|
be->be.free_query = NULL;
|
|
be->be.run_query = NULL;
|
|
be->be.price_lookup = NULL;
|
|
|
|
be->be.sync = pgendDoSyncSingleFile;
|
|
be->be.export = NULL;
|
|
be->be.percentage = NULL;
|
|
be->be.events_pending = NULL;
|
|
be->be.process_events = NULL;
|
|
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 it's safe to use.\n");
|
|
break;
|
|
|
|
case MODE_SINGLE_UPDATE:
|
|
pgendEnable(be);
|
|
be->be.load = pgend_do_load_single;
|
|
be->be.begin = pgend_do_begin;
|
|
be->be.commit = pgend_do_commit;
|
|
be->be.rollback = pgend_do_rollback;
|
|
be->be.compile_query = NULL;
|
|
be->be.free_query = NULL;
|
|
be->be.run_query = NULL;
|
|
be->be.price_lookup = NULL;
|
|
|
|
be->be.sync = pgendDoSync;
|
|
be->be.export = NULL;
|
|
be->be.percentage = NULL;
|
|
be->be.events_pending = NULL;
|
|
be->be.process_events = NULL;
|
|
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 it's safe to use.\n");
|
|
break;
|
|
|
|
case MODE_POLL:
|
|
pgendEnable(be);
|
|
be->be.load = pgend_book_load_poll;
|
|
be->be.begin = pgend_do_begin;
|
|
be->be.commit = pgend_do_commit;
|
|
be->be.rollback = pgend_do_rollback;
|
|
|
|
be->be.compile_query = pgendCompileQuery;
|
|
be->be.free_query = pgendFreeQuery;
|
|
be->be.run_query = pgendRunQuery;
|
|
be->be.price_lookup = pgendPriceFind;
|
|
|
|
be->be.sync = pgendDoSync;
|
|
be->be.export = NULL;
|
|
be->be.percentage = NULL;
|
|
be->be.events_pending = NULL;
|
|
be->be.process_events = NULL;
|
|
|
|
PWARN ("mode=multi-user-poll is beta -- \n"
|
|
"we've fixed all known bugs but that doesn't mean\n"
|
|
"there aren't any! If something seems weird, let us know.\n");
|
|
break;
|
|
|
|
case MODE_EVENT:
|
|
pgendEnable(be);
|
|
|
|
pgendSessionGetPid (be);
|
|
pgendSessionSetupNotifies (be);
|
|
|
|
be->be.load = pgend_book_load_poll;
|
|
be->be.begin = pgend_do_begin;
|
|
be->be.commit = pgend_do_commit;
|
|
be->be.rollback = pgend_do_rollback;
|
|
|
|
be->be.compile_query = pgendCompileQuery;
|
|
be->be.free_query = pgendFreeQuery;
|
|
be->be.run_query = pgendRunQuery;
|
|
be->be.price_lookup = pgendPriceFind;
|
|
|
|
be->be.sync = pgendDoSync;
|
|
be->be.export = NULL;
|
|
be->be.percentage = NULL;
|
|
be->be.events_pending = pgendEventsPending;
|
|
be->be.process_events = pgendProcessEvents;
|
|
|
|
PWARN ("mode=multi-user is beta -- \n"
|
|
"we've fixed all known bugs but that doesn't mean\n"
|
|
"there aren't any! If something seems weird, let us know.\n");
|
|
|
|
break;
|
|
|
|
default:
|
|
PERR ("bad mode specified");
|
|
break;
|
|
}
|
|
}
|
|
|
|
LEAVE("be=%p, sessionid=%s", be,
|
|
sessionid ? sessionid : "(null)");
|
|
}
|
|
|
|
/* ============================================================= */
|
|
|
|
void
|
|
pgendDisable (PGBackend *be)
|
|
{
|
|
ENTER("be = %p", 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) {
|
|
LEAVE("be->nest_count > 1: %d", be->nest_count);
|
|
return;
|
|
}
|
|
|
|
/* save hooks */
|
|
be->snr.load = be->be.load;
|
|
be->snr.session_end = be->be.session_end;
|
|
be->snr.destroy_backend = be->be.destroy_backend;
|
|
be->snr.begin = be->be.begin;
|
|
be->snr.commit = be->be.commit;
|
|
be->snr.rollback = be->be.rollback;
|
|
be->snr.compile_query = be->be.compile_query;
|
|
be->snr.run_query = be->be.run_query;
|
|
be->snr.price_lookup = be->be.price_lookup;
|
|
be->snr.sync = be->be.sync;
|
|
be->snr.export = be->be.export;
|
|
be->snr.percentage = be->be.percentage;
|
|
be->snr.events_pending = be->be.events_pending;
|
|
be->snr.process_events = be->be.process_events;
|
|
|
|
be->be.load = NULL;
|
|
be->be.session_end = NULL;
|
|
be->be.destroy_backend = NULL;
|
|
be->be.begin = NULL;
|
|
be->be.commit = NULL;
|
|
be->be.rollback = NULL;
|
|
be->be.compile_query = NULL;
|
|
be->be.run_query = NULL;
|
|
be->be.price_lookup = NULL;
|
|
be->be.sync = NULL;
|
|
be->be.export = NULL;
|
|
be->be.percentage = NULL;
|
|
be->be.events_pending = NULL;
|
|
be->be.process_events = NULL;
|
|
|
|
LEAVE(" ");
|
|
}
|
|
|
|
/* ============================================================= */
|
|
|
|
void
|
|
pgendEnable (PGBackend *be)
|
|
{
|
|
ENTER(" ");
|
|
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.load = be->snr.load;
|
|
be->be.session_end = be->snr.session_end;
|
|
be->be.destroy_backend = be->snr.destroy_backend;
|
|
be->be.begin = be->snr.begin;
|
|
be->be.commit = be->snr.commit;
|
|
be->be.rollback = be->snr.rollback;
|
|
be->be.compile_query = be->snr.compile_query;
|
|
be->be.run_query = be->snr.run_query;
|
|
be->be.price_lookup = be->snr.price_lookup;
|
|
be->be.sync = be->snr.sync;
|
|
be->be.export = be->snr.export;
|
|
be->be.percentage = be->snr.percentage;
|
|
be->be.events_pending = be->snr.events_pending;
|
|
be->be.process_events = be->snr.process_events;
|
|
|
|
LEAVE(" ");
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* The pgendInit() routine initializes the backend private
|
|
* structures, mallocs any needed memory, etc.
|
|
*/
|
|
|
|
static void
|
|
pgendInit (PGBackend *be)
|
|
{
|
|
int i;
|
|
Timespec ts;
|
|
|
|
ENTER(" ");
|
|
|
|
/* initialize global variable */
|
|
nullguid = *(guid_null());
|
|
|
|
/* access mode */
|
|
be->session_mode = MODE_EVENT;
|
|
be->sessionGuid = NULL;
|
|
guid_to_string_buff (&nullguid, be->session_guid_str);
|
|
|
|
/* generic backend handlers */
|
|
qof_backend_init((QofBackend*)be);
|
|
|
|
be->be.session_begin = pgend_session_begin;
|
|
be->be.session_end = pgend_session_end;
|
|
|
|
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->freshly_created_db = FALSE;
|
|
be->freshly_created_prdb = FALSE;
|
|
|
|
be->my_pid = 0;
|
|
be->do_account = 0;
|
|
be->do_book = 0;
|
|
be->do_checkpoint = 0;
|
|
be->do_price = 0;
|
|
be->do_session = 0;
|
|
be->do_transaction = 0;
|
|
|
|
ts.tv_sec = time (0);
|
|
ts.tv_nsec = 0;
|
|
|
|
be->last_account = ts;
|
|
be->last_price = ts;
|
|
be->last_transaction = ts;
|
|
|
|
be->version_check = (guint32) ts.tv_sec;
|
|
|
|
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->session = NULL;
|
|
be->book = NULL;
|
|
be->blist = NULL;
|
|
LEAVE(" ");
|
|
}
|
|
|
|
/* ============================================================= */
|
|
|
|
QofBackend *
|
|
pgendNew (void)
|
|
{
|
|
PGBackend *be;
|
|
|
|
ENTER(" ");
|
|
be = g_new0 (PGBackend, 1);
|
|
pgendInit (be);
|
|
|
|
LEAVE(" ")
|
|
return (QofBackend *) be;
|
|
}
|
|
|
|
/* ======================== END OF FILE ======================== */
|