diff --git a/src/engine/sql/PostgresBackend.c b/src/engine/sql/PostgresBackend.c index 44776dcc2b..0e10f0b3ef 100644 --- a/src/engine/sql/PostgresBackend.c +++ b/src/engine/sql/PostgresBackend.c @@ -4,9 +4,8 @@ * * FUNCTION: * Implements the callbacks for the postgress backend. - * this is somewhat broken code. - * its a quick hack just to check things out. - * it needs extensive review and design checking + * this is code kinda usually works. + * it needs review and design checking * * HISTORY: * Copyright (c) 2000, 2001 Linas Vepstas @@ -24,6 +23,8 @@ #include "BackendP.h" #include "Group.h" #include "gnc-book.h" +#include "gnc-commodity.h" +#include "gnc-engine.h" #include "gnc-engine-util.h" #include "gnc-event.h" #include "guid.h" @@ -37,7 +38,6 @@ static short module = MOD_BACKEND; - static void pgendDisable (PGBackend *be); static void pgendEnable (PGBackend *be); @@ -134,7 +134,7 @@ static void pgendEnable (PGBackend *be); #define GET_DB_VAL(str,n) (PQgetvalue (result, n, PQfnumber (result, str))) #define COMP_STR(sqlname,fun,ndiffs) { \ - if (strcmp (GET_DB_VAL(sqlname,0),fun)) { \ + if (null_strcmp (GET_DB_VAL(sqlname,0),fun)) { \ PINFO("%s sql='%s', eng='%s'", sqlname, \ GET_DB_VAL (sqlname,0), fun); \ ndiffs++; \ @@ -143,7 +143,7 @@ static void pgendEnable (PGBackend *be); #define COMP_GUID(sqlname,fun, ndiffs) { \ const char *tmp = guid_to_string(fun); \ - if (strcmp (GET_DB_VAL(sqlname,0),tmp)) { \ + if (null_strcmp (GET_DB_VAL(sqlname,0),tmp)) { \ PINFO("%s sql='%s', eng='%s'", sqlname, \ GET_DB_VAL(sqlname,0), tmp); \ ndiffs++; \ @@ -177,6 +177,20 @@ static void pgendEnable (PGBackend *be); } \ } +/* a very special date comp */ +#define COMP_NOW(sqlname,fun,ndiffs) { \ + Timespec eng_time = xaccTransRetDateEnteredTS(ptr); \ + Timespec sql_time = gnc_iso8601_to_timespec( \ + GET_DB_VAL(sqlname,0)); \ + if (eng_time.tv_sec != sql_time.tv_sec) { \ + time_t tmp = eng_time.tv_sec; \ + PINFO("%s sql='%s' eng=%s", sqlname, \ + GET_DB_VAL(sqlname,0), ctime(&tmp)); \ + ndiffs++; \ + } \ +} + + #define COMP_INT64(sqlname,fun,ndiffs) { \ if (atoll (GET_DB_VAL(sqlname,0)) != fun) { \ PINFO("%s sql='%s', eng='%lld'", sqlname, \ @@ -184,10 +198,41 @@ static void pgendEnable (PGBackend *be); ndiffs++; \ } \ } + +#define COMP_INT32(sqlname,fun,ndiffs) { \ + if (atol (GET_DB_VAL(sqlname,0)) != fun) { \ + PINFO("%s sql='%s', eng='%d'", sqlname, \ + GET_DB_VAL (sqlname,0), fun); \ + ndiffs++; \ + } \ +} + /* ============================================================= */ #include "tmp.c" +/* ============================================================= */ +/* This routine updates the commodity structure if needed, and/or + * stores it the first time if it hasn't yet been stored. + */ + +static void +pgendStoreCommodityNoLock (PGBackend *be, const gnc_commodity *com) +{ + gnc_commodity *commie = (gnc_commodity *) com; + int ndiffs; + if (!be || !com) return; + + ndiffs = pgendCompareOnegnc_commodityOnly (be, commie); + + /* update commodity if there are differences ... */ + if (0ndiffs) pgendStoreOnegnc_commodityOnly (be, commie, SQL_INSERT); + + LEAVE(" "); +} + /* ============================================================= */ /* This routine updates the account structure if needed, and/or * stores it the first time if it hasn't yet been stored. @@ -200,6 +245,7 @@ static void pgendStoreAccountNoLock (PGBackend *be, Account *acct, gboolean do_mark) { + const gnc_commodity *com; int ndiffs; if (!be || !acct) return; @@ -223,6 +269,11 @@ pgendStoreAccountNoLock (PGBackend *be, Account *acct, if (0ndiffs) pgendStoreOneAccountOnly (be, acct, SQL_INSERT); + + /* make sure the account's commodity is in the commodity table */ + com = xaccAccountGetCurrency (acct); + pgendStoreCommodityNoLock (be, com); + LEAVE(" "); } @@ -396,7 +447,8 @@ pgendSyncTransaction (PGBackend *be, GUID *trans_guid) Account *acc, *previous_acc=NULL; gboolean do_set_guid=FALSE; gboolean engine_data_is_newer = FALSE; - int i, nrows; + int i, j, nrows; + GList *node, *db_splits=NULL, *engine_splits, *delete_splits=NULL; ENTER ("be=%p", be); if (!be || !trans_guid) return; @@ -428,10 +480,11 @@ pgendSyncTransaction (PGBackend *be, GUID *trans_guid) do { GET_RESULTS (be->connection, result); { - int j=0, jrows; + int jrows; int ncols = PQnfields (result); jrows = PQntuples (result); nrows += jrows; + j = 0; PINFO ("query result %d has %d rows and %d cols", i, nrows, ncols); @@ -546,10 +599,6 @@ pgendSyncTransaction (PGBackend *be, GUID *trans_guid) s = xaccMallocSplit(); xaccSplitSetGUID(s, &guid); } - else - { - PERR ("split already exists ... - implement me .."); - } /* next, restore some split data */ /* hack alert - not all split fields handled */ @@ -591,6 +640,11 @@ pgendSyncTransaction (PGBackend *be, GUID *trans_guid) } xaccAccountInsertSplit(acc, s); } + + /* --------------------------------------------- */ + /* finally tally them up; we use this below to clean + * out deleted splits */ + db_splits = g_list_prepend (db_splits, s); } } } while (result); @@ -598,6 +652,28 @@ pgendSyncTransaction (PGBackend *be, GUID *trans_guid) /* close out dangling edit session */ xaccAccountCommitEdit (previous_acc); + 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); + xaccTransCommitEdit (trans); /* reneable events to the backend and GUI */ @@ -653,7 +729,7 @@ pgendGetAllAccounts (PGBackend *be) GUID guid; /* first, lets see if we've already got this one */ - PINFO ("account GUID=%s\n", GET_DB_VAL("accountGUID",j)); + PINFO ("account GUID=%s", GET_DB_VAL("accountGUID",j)); guid = nullguid; /* just in case the read fails ... */ string_to_guid (GET_DB_VAL("accountGUID",j), &guid); acc = (Account *) xaccLookupEntity (&guid, GNC_ID_ACCOUNT); @@ -668,8 +744,27 @@ pgendGetAllAccounts (PGBackend *be) xaccAccountSetDescription(acc, GET_DB_VAL("description",j)); xaccAccountSetCode(acc, GET_DB_VAL("accountCode",j)); xaccAccountSetType(acc, xaccAccountStringToEnum(GET_DB_VAL("type",j))); + + /* hop through a couple of hoops for the commodity */ + /* it would be nice to simplify this ... */ + { + gnc_commodity_table *comtab; + gnc_commodity *com; + char *str, *name; + + str = g_strdup(GET_DB_VAL("commodity",j)); + name = strchr (str, ':'); + *name = 0; + name += 2; + + comtab = gnc_engine_commodities(); + com = gnc_commodity_table_lookup(comtab, str, name); + xaccAccountSetCommodity(acc, com); + g_free (str); + } + /* try to find the parent account */ - PINFO ("parent GUID=%s\n", GET_DB_VAL("parentGUID",j)); + PINFO ("parent GUID=%s", GET_DB_VAL("parentGUID",j)); guid = nullguid; /* just in case the read fails ... */ string_to_guid (GET_DB_VAL("parentGUID",j), &guid); if (guid_equal(xaccGUIDNULL(), &guid)) @@ -709,6 +804,31 @@ pgendGetAllAccounts (PGBackend *be) return topgrp; } +/* ============================================================= */ +/* return TRUE if this appears to be a fresh, 'null' transaction */ +/* it would be better is somehow we could get the gui to mark this + * as a fresh transaction, rather than having to scan a bunch of + * fields. But this is minor in the 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; +} + /* ============================================================= */ static int @@ -729,37 +849,47 @@ pgend_trans_commit_edit (Backend * bend, "LOCK TABLE gncTransaction IN EXCLUSIVE MODE; " "LOCK TABLE gncEntry IN EXCLUSIVE MODE; " "LOCK TABLE gncAccount IN EXCLUSIVE MODE; " + "LOCK TABLE gncCommodity IN EXCLUSIVE MODE; " ); SEND_QUERY (be,be->buff, 555); FINISH_QUERY(be->connection); - /* 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 - * would be best to spit this back at the GUI, and let a human - * decide. + /* 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. */ - ndiffs = pgendCompareOneTransactionOnly (be, oldtrans); - if (ndiffs) rollback++; - - /* be sure to check the old splits as well ... */ - nsplits = xaccTransCountSplits (oldtrans); - for (i=0; ibuff, be->bufflen, "ROLLBACK;"); + SEND_QUERY (be,be->buff,444); + FINISH_QUERY(be->connection); + + LEAVE ("rolled back"); + return 666; /* hack alert */ + } } - if (rollback) { - snprintf (be->buff, be->bufflen, "ROLLBACK;"); - SEND_QUERY (be,be->buff,444); - FINISH_QUERY(be->connection); - - LEAVE (" "); - return 666; /* hack alert */ - } - /* if we are here, we are good to go */ pgendStoreTransactionNoLock (be, trans, FALSE); @@ -767,7 +897,60 @@ pgend_trans_commit_edit (Backend * bend, SEND_QUERY (be,be->buff,333); FINISH_QUERY(be->connection); - LEAVE (" "); + /* hack alert -- the following code will get rid of that annoying + * message from the GUI about saving one's data. However, it doesn't + * do the right thing if the connection to the backend was ever lost. + * what should happen is the user should get a chance to + * resynchronize thier data with the backend, before quiting out. + */ + { + Split * s = xaccTransGetSplit (trans, 0); + Account *acc = xaccSplitGetAccount (s); + AccountGroup *top = xaccGetAccountRoot (acc); + xaccGroupMarkSaved (top); + } + + LEAVE ("commited"); + return 0; +} + +/* ============================================================= */ + +static int +pgend_account_commit_edit (Backend * bend, + Account * acct) +{ + PGBackend *be = (PGBackend *)bend; + + ENTER ("be=%p, acct=%p", be, acct); + if (!be || !acct) return 1; /* hack alert hardcode literal */ + + /* lock it up so that we query and store atomically */ + /* its not at all clear to me that this isn't rife with deadlocks. */ + snprintf (be->buff, be->bufflen, + "BEGIN; " + "LOCK TABLE gncAccount IN EXCLUSIVE MODE; " + "LOCK TABLE gncCommodity IN EXCLUSIVE MODE; " + ); + SEND_QUERY (be,be->buff, 555); + FINISH_QUERY(be->connection); + + /* hack alert -- we aren't comparing old to new, + * i.e. not comparing version numbers, to see if + * we're clobbering someone elses changes. */ + pgendStoreAccountNoLock (be, acct, FALSE); + + snprintf (be->buff, be->bufflen, "COMMIT;"); + SEND_QUERY (be,be->buff,333); + FINISH_QUERY(be->connection); + + /* mark this up so that we don't get that annoying gui dialog + * about having to save to file. unfortunately,however, this + * is too liberal, and could screw up synchronization if we've lost + * contact with the back end at some point. So hack alert -- fix + * this. */ + xaccGroupMarkSaved (xaccAccountGetParent(acct)); + LEAVE ("commited"); return 0; } @@ -858,7 +1041,7 @@ pgend_session_begin (GNCBook *sess, const char * sessionid) g_free(url); /* handle localhost as a special case */ - if (!strcmp("localhost", be->hostname)) + if (!safe_strcmp("localhost", be->hostname)) { g_free (be->hostname); be->hostname = NULL; @@ -882,7 +1065,7 @@ pgend_session_begin (GNCBook *sess, const char * sessionid) return; } - DEBUGCMD (PQtrace(be->connection, stderr)); + // DEBUGCMD (PQtrace(be->connection, stderr)); /* set the datestyle to something we can parse */ snprintf (be->buff, be->bufflen, "SET DATESTYLE='ISO';"); @@ -1032,7 +1215,7 @@ pgendEnable (PGBackend *be) PINFO("nest count=%d", be->nest_count); if (be->nest_count) return; be->be.account_begin_edit = NULL; - be->be.account_commit_edit = NULL; + be->be.account_commit_edit = pgend_account_commit_edit; be->be.trans_begin_edit = NULL; be->be.trans_commit_edit = pgend_trans_commit_edit; be->be.trans_rollback_edit = NULL; diff --git a/src/engine/sql/builder.c b/src/engine/sql/builder.c index d6d2eb5f1e..cf7f585410 100644 --- a/src/engine/sql/builder.c +++ b/src/engine/sql/builder.c @@ -261,6 +261,14 @@ sqlBuild_Set_Int64 (sqlBuilder *b, const char *tag, gint64 nval) /* ================================================ */ +void +sqlBuild_Set_Int32 (sqlBuilder *b, const char *tag, gint32 nval) +{ + sqlBuild_Set_Int64 (b, tag, (gint64) nval); +} + +/* ================================================ */ + void sqlBuild_Where_Str (sqlBuilder *b, const char *tag, const char *val) { diff --git a/src/engine/sql/builder.h b/src/engine/sql/builder.h index 88796a187f..323c8228ec 100644 --- a/src/engine/sql/builder.h +++ b/src/engine/sql/builder.h @@ -44,6 +44,7 @@ void sqlBuild_Set_Char (sqlBuilder *b, const char *tag, char val); void sqlBuild_Set_GUID (sqlBuilder *b, const char *tag, const GUID *val); void sqlBuild_Set_Date (sqlBuilder *b, const char *tag, Timespec val); void sqlBuild_Set_Int64 (sqlBuilder *b, const char *tag, gint64 val); +void sqlBuild_Set_Int32 (sqlBuilder *b, const char *tag, gint32 val); /* build the update 'where' clause */ diff --git a/src/engine/sql/gnc-init.sql b/src/engine/sql/gnc-init.sql index cad577d1eb..1f84965418 100644 --- a/src/engine/sql/gnc-init.sql +++ b/src/engine/sql/gnc-init.sql @@ -4,6 +4,17 @@ -- these tables are hand-built, but maybe they should be -- autobuilt with the m4 macros ... +-- Commodity structure + +DROP TABLE gncCommodity; +CREATE TABLE gncCommodity ( + commodity TEXT PRIMARY KEY, + fullname TEXT, + namespace TEXT, + mnemonic TEXT, + code TEXT, + fraction INT DEFAULT '100' +); -- Account structure -- parentGUID points to parent account -- guid. There is no supports for Groups in this schema. @@ -22,7 +33,7 @@ CREATE TABLE gncAccount ( description TEXT, notes TEXT, type TEXT, - currency TEXT + commodity TEXT ); -- CREATE INDEX gncAccount_pg_idx ON gncAccount (parentGuid); @@ -33,10 +44,11 @@ CREATE TABLE gncAccount ( DROP TABLE gncTransaction; CREATE TABLE gncTransaction ( transGuid CHAR(32) PRIMARY KEY, - date_entered DATETIME, + date_entered DATETIME DEFAULT 'NOW', date_posted DATETIME, num TEXT, - description TEXT + description TEXT, + currency TEXT ); -- a gncEntry is what we call 'Split' elsewhere in the engine @@ -48,7 +60,7 @@ CREATE TABLE gncEntry ( transGuid CHAR(32), memo TEXT, action TEXT, - reconciled CHAR, + reconciled CHAR DEFAULT 'n', date_reconciled DATETIME, amountNum INT8 DEFAULT '0', amountDenom INT8 DEFAULT '100', diff --git a/src/engine/sql/table.m4 b/src/engine/sql/table.m4 index f64909bd48..10a4fea174 100644 --- a/src/engine/sql/table.m4 +++ b/src/engine/sql/table.m4 @@ -11,6 +11,7 @@ define(`account', `gncAccount, Account, description, , char *, xaccAccountGetDescription(ptr), notes, , char *, xaccAccountGetNotes(ptr), type, , char *, xaccAccountTypeEnumAsString(xaccAccountGetType(ptr)), + commodity, , char *, gnc_commodity_get_unique_name(xaccAccountGetCommodity(ptr)), parentGUID, , GUID *, xaccAccountGetGUID(xaccAccountGetParentAccount(ptr)), accountGUID, KEY, GUID *, xaccAccountGetGUID(ptr), ') @@ -40,11 +41,23 @@ define(`split', `gncEntry, Split, define(`transaction', `gncTransaction, Transaction, num, , char *, xaccTransGetNum(ptr), description, , char *, xaccTransGetDescription(ptr), - date_entered, , char *, "CURRENT", + currency, , char *, gnc_commodity_get_unique_name(xaccTransGetCurrency(ptr)), + date_entered, , now, "NOW", date_posted, , Timespec, xaccTransRetDatePostedTS(ptr), transGUID, KEY, GUID *, xaccTransGetGUID(ptr), ') + +define(`modity', `gncCommodity, gnc_commodity, + namespace, , char *, gnc_commodity_get_namespace(ptr), + fullname, , char *, gnc_commodity_get_fullname(ptr), + mnemonic, , char *, gnc_commodity_get_mnemonic(ptr), + code, , char *, gnc_commodity_get_exchange_code(ptr), + fraction, , int32, gnc_commodity_get_fraction(ptr), + commodity, KEY, char *, gnc_commodity_get_unique_name(ptr), + ') + + /* ------------------------------------------------------- */ /* symbolic names for the table accessors */ define(`tablename', $1) @@ -54,7 +67,7 @@ define(`firstrec', `shift(shift($@))') define(`nextrec', `shift(shift(shift(shift($@))))') /* -------- */ -/* macros that use teh sql builder to build a query */ +/* macros that use the sql builder to build a query */ define(`sql_setter', `ifelse($2, `KEY', `ifelse($1, `char *', sqlBuild_Where_Str, @@ -62,6 +75,8 @@ define(`sql_setter', `ifelse($2, `KEY', $2, , `ifelse($1, `char *', sqlBuild_Set_Str, + $1, `now', sqlBuild_Set_Str, + $1, `int32', sqlBuild_Set_Int32, $1, `int64', sqlBuild_Set_Int64, $1, `GUID *', sqlBuild_Set_GUID, $1, `Timespec', sqlBuild_Set_Date, @@ -80,6 +95,8 @@ define(`set_fields', `set_fields_r(firstrec($@))') /* macros to compare a query result */ define(`cmp_value', `ifelse($1, `char *', COMP_STR, + $1, `now', COMP_NOW, + $1, `int32', COMP_INT32, $1, `int64', COMP_INT64, $1, `GUID *', COMP_GUID, $1, `Timespec', COMP_DATE, @@ -180,7 +197,9 @@ divert store_one_only(account) store_one_only(transaction) store_one_only(split) +store_one_only(modity) compare_one_only(account) compare_one_only(transaction) compare_one_only(split) +compare_one_only(modity)