move prices, transactions to own file

git-svn-id: svn+ssh://svn.gnucash.org/repo/gnucash/trunk@4585 57a11ea4-9604-0410-9ed3-97b8803252fd
This commit is contained in:
Linas Vepstas
2001-06-10 18:06:14 +00:00
parent 081df4d1b3
commit ec425b732d
7 changed files with 1479 additions and 424 deletions

View File

@@ -13,6 +13,7 @@ libgnc_postgres_la_SOURCES = \
events.c \
gncquery.c \
kvp-sql.c \
price.c \
txn.c
noinst_HEADERS = \
@@ -23,6 +24,7 @@ noinst_HEADERS = \
events.h \
gncquery.h \
kvp-sql.h \
price.h \
putil.h \
txn.h

View File

@@ -71,6 +71,7 @@
#include "gncquery.h"
#include "kvp-sql.h"
#include "PostgresBackend.h"
#include "price.h"
#include "txn.h"
#include "putil.h"
@@ -440,57 +441,6 @@ pgendGetAllAccountKVP (PGBackend *be, AccountGroup *grp)
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.
@@ -600,12 +550,6 @@ pgendGetAllAccounts (PGBackend *be, AccountGroup *topgrp)
return topgrp;
}
/* ============================================================= */
/* ============================================================= */
/* TRANSACTION STUFF */
/* ============================================================= */
/* ============================================================= */
/* ============================================================= */
/* QUERY STUFF */
/* ============================================================= */
@@ -868,298 +812,6 @@ pgendGetAllTransactions (PGBackend *be, AccountGroup *grp)
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;\n"
"NOTIFY gncPrice;";
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();
}
/* ============================================================= */
/* ============================================================= */
@@ -1248,81 +900,6 @@ pgend_account_commit_edit (Backend * bend,
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;\n"
"NOTIFY gncPrice;";
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

View File

@@ -114,11 +114,16 @@ void pgendDisable (PGBackend *be);
void pgendEnable (PGBackend *be);
void pgendStoreOneTransactionOnly (PGBackend *be, Transaction *ptr, sqlBuild_QType update);
void pgendPutOneCommodityOnly (PGBackend *be, gnc_commodity *ptr);
void pgendPutOnePriceOnly (PGBackend *be, GNCPrice *ptr);
void pgendPutOneSplitOnly (PGBackend *be, Split *ptr);
void pgendPutOneTransactionOnly (PGBackend *be, Transaction *ptr);
int pgendPriceCompareVersion (PGBackend *be, GNCPrice *ptr);
int pgendTransactionCompareVersion (PGBackend *be, Transaction *ptr);
void pgendStoreAuditPrice (PGBackend *be, GNCPrice *ptr, sqlBuild_QType update);
void pgendStoreAuditSplit (PGBackend *be, Split *ptr, sqlBuild_QType update);
void pgendStoreAuditTransaction (PGBackend *be, Transaction *ptr, sqlBuild_QType update);

473
src/engine/sql/price.c Normal file
View File

@@ -0,0 +1,473 @@
/********************************************************************\
* price.c -- implements price handling for the postgres backend *
* Copyright (c) 2001 Linas Vepstas *
* *
* 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 <glib.h>
#include <string.h>
#include <libpq-fe.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 "PostgresBackend.h"
#include "putil.h"
static short module = MOD_BACKEND;
/* ============================================================= */
/* ============================================================= */
/* COMMODITIES STUFF */
/* ============================================================= */
/* ============================================================= */
/* 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;
}
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 (" ");
}
/* ============================================================= */
/* ============================================================= */
/* 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;
}
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);
}
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;\n"
"NOTIFY gncPrice;";
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;
}
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;
}
/* ============================================================= */
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 */
/* ============================================================= */
/* ============================================================= */
int
pgend_price_begin_edit (Backend * bend, GNCPrice *pr)
{
if (pr && pr->db && pr->db->dirty)
{
PERR ("price db is unexpectedly dirty");
}
return 0;
}
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;\n"
"NOTIFY gncPrice;";
SEND_QUERY (be,bufp,335);
FINISH_QUERY(be->connection);
if (pr->db) pr->db->dirty = FALSE;
LEAVE ("commited");
return 0;
}
/* ======================== END OF FILE ======================== */

39
src/engine/sql/price.h Normal file
View File

@@ -0,0 +1,39 @@
/********************************************************************\
* price.h -- implements price & commodity handling for pg backend *
* Copyright (c) 2000, 2001 Linas Vepstas *
* *
* 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 *
\********************************************************************/
#ifndef __POSTGRES_PRICE_H__
#define __POSTGRES_PRICE_H__
#include "PostgresBackend.h"
void pgendGetAllCommodities (PGBackend *be);
void pgendStorePriceDB (PGBackend *be, GNCPriceDB *prdb);
void pgendStorePriceDBNoLock (PGBackend *be, GNCPriceDB *prdb);
GNCPriceDB * pgendGetAllPrices (PGBackend *be, GNCPriceDB *prdb);
void pgendPriceLookup (Backend *bend, GNCPriceLookup *look);
int pgend_price_begin_edit (Backend * bend, GNCPrice *pr);
int pgend_price_commit_edit (Backend * bend, GNCPrice *pr);
#endif /* __POSTGRES_PRICE_H__ */

904
src/engine/sql/txn.c Normal file
View File

@@ -0,0 +1,904 @@
/********************************************************************\
* txn.c -- implements transaction handlers for postgres backend *
* Copyright (c) 2000, 2001 Linas Vepstas *
* *
* 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 <glib.h>
#include <stdio.h>
#include <string.h>
#include <libpq-fe.h>
#include "Account.h"
#include "AccountP.h"
#include "Group.h"
#include "GroupP.h"
#include "gnc-commodity.h"
#include "gnc-engine-util.h"
#include "gnc-event.h"
#include "guid.h"
#include "Transaction.h"
#include "TransactionP.h"
#include "checkpoint.h"
#include "kvp-sql.h"
#include "PostgresBackend.h"
#include "txn.h"
#include "putil.h"
static short module = MOD_BACKEND;
/* ============================================================= */
/* ============================================================= */
/* 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;
}
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;\n"
"NOTIFY gncTransaction;";
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;
}
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;\n"
"NOTIFY gncTransaction;";
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.
*/
int
pgendCopyTransactionToEngine (PGBackend *be, const 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; j<jrows; j++)
{
Split *s;
GUID guid;
Timespec ts;
gint64 num;
gnc_numeric value, amount;
/* --------------------------------------------- */
/* first, lets see if we've already got this one */
PINFO ("split GUID=%s", DB_GET_VAL("entryGUID",j));
guid = nullguid; /* just in case the read fails ... */
string_to_guid (DB_GET_VAL("entryGUID",j), &guid);
s = xaccSplitLookup (&guid);
if (!s)
{
s = xaccMallocSplit();
xaccSplitSetGUID(s, &guid);
}
/* next, restore some split data */
/* hack alert - not all split fields handled */
xaccSplitSetMemo(s, DB_GET_VAL("memo",j));
xaccSplitSetAction(s, DB_GET_VAL("action",j));
ts = gnc_iso8601_to_timespec_local
(DB_GET_VAL("date_reconciled",j));
xaccSplitSetDateReconciledTS (s, &ts);
num = atoll (DB_GET_VAL("value", j));
value = gnc_numeric_create (num, trans_frac);
xaccSplitSetValue (s, value);
xaccSplitSetReconcile (s, (DB_GET_VAL("reconciled", j))[0]);
/* --------------------------------------------- */
/* next, find the account that this split goes into */
guid = nullguid; /* just in case the read fails ... */
string_to_guid (DB_GET_VAL("accountGUID",j), &guid);
acc = xaccAccountLookup (&guid);
if (!acc)
{
PERR ("account not found, will delete this split\n"
"\t(split with guid=%s\n"
"\twants an acct with guid=%s)\n",
DB_GET_VAL("entryGUID",j),
DB_GET_VAL("accountGUID",j)
);
xaccSplitDestroy (s);
}
else
{
gnc_commodity *modity;
gint64 acct_frac;
num = atoll (DB_GET_VAL("amount", j));
modity = xaccAccountGetCommodity (acc);
acct_frac = gnc_commodity_get_fraction (modity);
amount = gnc_numeric_create (num, acct_frac);
xaccSplitSetShareAmount (s, amount);
xaccTransAppendSplit (trans, s);
if (acc != previous_acc)
{
xaccAccountCommitEdit (previous_acc);
xaccAccountBeginEdit (acc);
previous_acc = acc;
}
if (acc->parent) 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
/* ============================================================= */
/* ============================================================= */
/* HIGHER LEVEL ROUTINES AND BACKEND PROPER */
/* ============================================================= */
/* ============================================================= */
/* ============================================================= */
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
/* roll things back is sql version is newer */
if (0 < pgendTransactionCompareVersion (be, oldtrans)) { rollback = 1; }
/* first, see if someone else has already deleted this transaction */
if (-1 < pgendTransactionGetDeletedVersion (be, oldtrans))
{
if (rollback)
{
/* Although this situation should never happen, we'll try
* to gracefully handle it anyway, because otherwuise the
* transaction becomes un-modifiable, undeleteable.
* (This situation might occur with the right combo of bugs
* and crashes. We've fixed the bugs, but ...
*/
char buf[80];
gnc_timespec_to_iso8601_buff (xaccTransRetDatePostedTS (trans), buf);
PERR ("The impossible has happened, and thats not good!\n"
"\tThe SQL database contains an active transaction that\n"
"\talso appears in the audit trail as deleted !!\n"
"\tWill try to delete transaction for good\n"
"\ttransaction is '%s' %s\n",
xaccTransGetDescription (trans), buf);
rollback = 0;
trans->do_free = TRUE;
}
else
{
rollback = 1;
}
}
#endif
if (rollback) {
bufp = "ROLLBACK;";
SEND_QUERY (be,bufp,444); /* hack alert hard coded literal */
FINISH_QUERY(be->connection);
PINFO ("old tranasction didn't match DB, edit rolled back)\n");
/* What happens here: We return to the engine with an
* error code. This causes the engine to call
* xaccTransRollback(), with then invokes our backend rollback
* routine. Our rollback routine updates from the latest in
* the sql database, and voila! we are good to go.
*/
return 666; /* hack alert- hard coded literal */
}
}
/* if we are here, we are good to go */
pgendStoreTransactionNoLock (be, trans, FALSE);
bufp = "COMMIT;\n"
"NOTIFY gncTransaction;";
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;
}
/* ============================================================= */
/* transaction rollback routine. This routine can be invoked
* in one of two ways: if the user canceled an edited transaction
* by hand, from the gui, or automatically, due to a multi-user
* edit conflict. In this latter case, the commit_edit routine
* above failed, and returned to the engine. Then the engine
* xaccTransRollback routine got invoked, which called us.
* What we do here is to copy the transaction out of the dataabse
* and into the engine. This will bring the local engine up
* to sync from the changes that other users had made.
*/
int
pgend_trans_rollback_edit (Backend * bend, Transaction * trans)
{
PGBackend *be = (PGBackend *)bend;
const GUID * trans_guid;
if (!be || !trans) return 0;
ENTER ("be=%p, trans=%p", be, trans);
/* First, lets see if the other user had deleted this transaction.
* If so, then we want to delete it from the local cache as well.
*/
if (-1 < pgendTransactionGetDeletedVersion (be, trans))
{
LEAVE ("destroyed");
return BACKEND_ROLLBACK_DESTROY;
}
trans_guid = xaccTransGetGUID (trans);
pgendCopyTransactionToEngine (be, trans_guid);
LEAVE ("rolled back");
return 0;
}
/* ======================== END OF FILE ======================== */

55
src/engine/sql/txn.h Normal file
View File

@@ -0,0 +1,55 @@
/********************************************************************\
* txn.h -- transaction handling routines for the 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:
* txn.h
*
* FUNCTION:
* Implements the transaction handling callbacks for the postgres backend.
*
* HISTORY:
* Copyright (c) 2000, 2001 Linas Vepstas
*/
#ifndef __POSTGRES_TXN_H__
#define __POSTGRES_TXN_H__
#include <glib.h>
#include "Group.h"
#include "guid.h"
#include "Transaction.h"
#include "PostgresBackend.h"
int pgendCopyTransactionToEngine (PGBackend *be, const GUID *trans_guid);
void pgendStoreAllTransactions (PGBackend *be, AccountGroup *grp);
void pgendStoreTransactionNoLock (PGBackend *be, Transaction *trans, gboolean do_check_version);
int pgend_trans_commit_edit (Backend * bend, Transaction * trans, Transaction * oldtrans);
int pgend_trans_rollback_edit (Backend * bend, Transaction * trans);
#endif /* __POSTGRES_TXN_H__ */