From 8be9b0a9ad53abb971a38c3f2565db69fd8165e9 Mon Sep 17 00:00:00 2001 From: John Ralls Date: Tue, 18 Jan 2011 23:42:48 +0000 Subject: [PATCH] Enhanced database version handling. * Add a macro GNC_RESAVE_VERSION which holds an svn revision number. * When fully saving a database put this number in versions with table_name Gnucash-Resave. * On database load, compare the current GNC_RESAVE_VERSION with the Gnucash revision and Gnucash-Resave revision saved when the database was created. * If the current GNC_RESAVE_VERSION > the saved Gnucash version, then emit ERR_SQL_DB_TOO_OLD. * If GNC_RESAVE_VERSION < the saved Gnucash-Resave, emit ERR_SQL_DB_TOO_NEW. git-svn-id: svn+ssh://svn.gnucash.org/repo/gnucash/trunk@20123 57a11ea4-9604-0410-9ed3-97b8803252fd --- src/backend/dbi/gnc-backend-dbi.c | 18 +++++++ src/backend/dbi/test/test-dbi-basic.c | 5 +- src/backend/dbi/test/test-dbi-stuff.c | 73 ++++++++++++++++++++++++++- src/backend/sql/gnc-backend-sql.c | 2 + src/backend/sql/gnc-backend-sql.h | 21 +++++++- 5 files changed, 116 insertions(+), 3 deletions(-) diff --git a/src/backend/dbi/gnc-backend-dbi.c b/src/backend/dbi/gnc-backend-dbi.c index 5b6611d818..652cb35655 100644 --- a/src/backend/dbi/gnc-backend-dbi.c +++ b/src/backend/dbi/gnc-backend-dbi.c @@ -1190,6 +1190,24 @@ gnc_dbi_load( QofBackend* qbe, /*@ dependent @*/ QofBook *book, QofBackendLoadTy gnc_sql_load( &be->sql_be, book, loadType ); + if ( GNC_RESAVE_VERSION > gnc_sql_get_table_version( &be->sql_be, "Gnucash" ) ) + { + /* The database was loaded with an older database schema or + * data semantics. In order to ensure consistency, the whole + * thing needs to be saved anew. */ + qof_backend_set_error( qbe, ERR_SQL_DB_TOO_OLD ); + } + else if ( GNC_RESAVE_VERSION < gnc_sql_get_table_version( &be->sql_be, + "Gnucash-Resave")) + { + /* Worse, the database was created with a newer version. We + * can't safely write to this database, so the user will have + * to do a "save as" to make one that we can write to. + */ + qof_backend_set_error( qbe, ERR_SQL_DB_TOO_NEW ); + } + + LEAVE( "" ); } diff --git a/src/backend/dbi/test/test-dbi-basic.c b/src/backend/dbi/test/test-dbi-basic.c index 64eb5d7b60..13e6968c73 100644 --- a/src/backend/dbi/test/test-dbi-basic.c +++ b/src/backend/dbi/test/test-dbi-basic.c @@ -114,6 +114,7 @@ int main (int argc, char ** argv) test_dbi_store_and_reload( "sqlite3", session_1, filename ); session_1 = create_session(); test_dbi_safe_save( "sqlite3", filename ); + test_dbi_version_control( "sqlite3", filename ); #ifdef TEST_MYSQL_URL printf( "TEST_MYSQL_URL='%s'\n", TEST_MYSQL_URL ); if ( strlen( TEST_MYSQL_URL ) > 0 ) @@ -121,7 +122,8 @@ int main (int argc, char ** argv) session_1 = create_session(); test_dbi_store_and_reload( "mysql", session_1, TEST_MYSQL_URL ); session_1 = create_session(); - test_dbi_safe_save( "msql", filename ); + test_dbi_safe_save( "mysql", filename ); + test_dbi_version_control( "mysql", filename ); } #endif #ifdef TEST_PGSQL_URL @@ -132,6 +134,7 @@ int main (int argc, char ** argv) test_dbi_store_and_reload( "pgsql", session_1, TEST_PGSQL_URL ); session_1 = create_session(); test_dbi_safe_save( "pgsql", filename ); + test_dbi_version_control( "pgsql", filename ); } #endif print_test_results(); diff --git a/src/backend/dbi/test/test-dbi-stuff.c b/src/backend/dbi/test/test-dbi-stuff.c index 3c24d9f002..a65481be2a 100644 --- a/src/backend/dbi/test/test-dbi-stuff.c +++ b/src/backend/dbi/test/test-dbi-stuff.c @@ -25,6 +25,7 @@ #include "config.h" #include "qof.h" +#include "qofsession-p.h" #include "cashobjects.h" #include "test-engine-stuff.h" #include "test-stuff.h" @@ -159,7 +160,9 @@ compare_books( QofBook* book_1, QofBook* book_2 ) test_conn_get_index_list( be ); } - +/* Given a synthetic session, use the same logic as + * QofSession::save_as to save it to a specified sql url, then load it + * back and compare. */ void test_dbi_store_and_reload( const gchar* driver, QofSession* session_1, const gchar* url ) { @@ -213,6 +216,13 @@ test_dbi_store_and_reload( const gchar* driver, QofSession* session_1, const gch qof_session_destroy( session_3 ); } +/* Given an already-created url (yeah, bad testing practice: Should + * start fresh from a synthetic session) load and safe-save it, then + * load it again into a new session and compare the two. Since + * safe-save is a more-or-less atomic function call, there's no way to + * be sure that it's actually doing what it's supposed to without + * running this test in a debugger and stopping in the middle of the + * safe-save and inspecting the database. */ void test_dbi_safe_save( const gchar* driver, const gchar* url ) { @@ -261,3 +271,64 @@ cleanup: qof_session_destroy( session_1 ); return; } + +/* Test the gnc_dbi_load logic that forces a newer database to be + * opened read-only and an older one to be safe-saved. Again, it would + * be better to do this starting from a fresh file, but instead we're + * being lazy and using an existing one. */ +void +test_dbi_version_control( const gchar* driver, const gchar* url ) +{ + + QofSession *sess; + QofBook *book; + QofBackend *qbe; + QofBackendError err; + gint ourversion = gnc_get_svn_version(); + + printf( "Testing safe save %s\n", driver ); + + // Load the session data + sess = qof_session_new(); + qof_session_begin( sess, url, TRUE, FALSE, FALSE ); + if (sess && qof_session_get_error(sess) != ERR_BACKEND_NO_ERR) + { + g_warning("Session Error: %d, %s", qof_session_get_error(sess), + qof_session_get_error_message(sess)); + do_test( FALSE, "DB Session Creation Failed"); + goto cleanup; + } + qof_session_load( sess, NULL ); + qbe = qof_session_get_backend( sess ); + book = qof_session_get_book( sess ); + qof_book_begin_edit( book ); + gnc_sql_set_table_version( (GncSqlBackend*)qbe, + "Gnucash", GNC_RESAVE_VERSION - 1 ); + qof_book_commit_edit( book ); + qof_session_end( sess ); + qof_session_destroy( sess ); + sess = qof_session_new(); + qof_session_begin( sess, url, TRUE, FALSE, FALSE ); + qof_session_load( sess, NULL ); + err = qof_session_pop_error( sess ); + do_test( err == ERR_SQL_DB_TOO_OLD, "DB Failed to flag too old" ); + qbe = qof_session_get_backend( sess ); + book = qof_session_get_book( sess ); + qof_book_begin_edit( book ); + gnc_sql_set_table_version( (GncSqlBackend*)qbe, + "Gnucash", ourversion ); + gnc_sql_set_table_version( (GncSqlBackend*)qbe, + "Gnucash-Resave", ourversion + 1 ); + qof_book_commit_edit( book ); + qof_session_end( sess ); + qof_session_destroy( sess ); + sess = qof_session_new(); + qof_session_begin( sess, url, TRUE, FALSE, FALSE ); + qof_session_load( sess, NULL ); + qof_session_ensure_all_data_loaded( sess ); + err = qof_session_pop_error( sess ); + do_test( err == ERR_SQL_DB_TOO_NEW, "DB Failed to flag too new" ); +cleanup: + qof_session_end( sess ); + qof_session_destroy( sess ); +} diff --git a/src/backend/sql/gnc-backend-sql.c b/src/backend/sql/gnc-backend-sql.c index 4e2d232237..3696839885 100644 --- a/src/backend/sql/gnc-backend-sql.c +++ b/src/backend/sql/gnc-backend-sql.c @@ -485,6 +485,7 @@ gnc_sql_sync_all( GncSqlBackend* be, /*@ dependent @*/ QofBook *book ) (void)reset_version_info( be ); gnc_sql_set_table_version( be, "Gnucash", gnc_get_svn_version() ); + gnc_sql_set_table_version( be, "Gnucash-Resave", GNC_RESAVE_VERSION ); /* Create new tables */ be->is_pristine_db = TRUE; @@ -538,6 +539,7 @@ gnc_sql_sync_all( GncSqlBackend* be, /*@ dependent @*/ QofBook *book ) } else { + qof_backend_set_error( (QofBackend*)be, ERR_BACKEND_SERVER_ERR ); is_ok = gnc_sql_connection_rollback_transaction( be->conn ); } LEAVE( "book=%p", book ); diff --git a/src/backend/sql/gnc-backend-sql.h b/src/backend/sql/gnc-backend-sql.h index b5d8fb4dce..1d8ca47a64 100644 --- a/src/backend/sql/gnc-backend-sql.h +++ b/src/backend/sql/gnc-backend-sql.h @@ -43,6 +43,25 @@ #include "qofbackend-p.h" #include +/** + * \def GNC_RESAVE_VERSION + * + * Defines the oldest svn revision of Gnucash which stores data in a + * way compatible with the current version. Data stored with an older + * version (or with no version indicated) of Gnucash will cause all + * tables to be moved aside, new tables saved with the current storage + * routines, and the old tables dropped. Any failures will trigger a + * rollback to the original tables. + * + * Encountering a database with a newer resave version will put the + * database in "read only" mode; a "save as" will be required to + * obtain a new database for storing from this instance, and the user + * will be warned of data loss. + * + */ + +#define GNC_RESAVE_VERSION 19920 + typedef struct GncSqlConnection GncSqlConnection; /** @@ -152,7 +171,7 @@ struct GncSqlConnection GncSqlResult* (*executeSelectStatement)( GncSqlConnection*, GncSqlStatement* ); /**< Returns NULL if error */ gint (*executeNonSelectStatement)( GncSqlConnection*, GncSqlStatement* ); /**< Returns -1 if error */ GncSqlStatement* (*createStatementFromSql)( /*@ observer @*/ GncSqlConnection*, const gchar* ); - gboolean (*doesTableExist)( GncSqlConnection*, const gchar* ); + gboolean (*doesTableExist)( GncSqlConnection*, const gchar* ); /**< Returns true if successful */ gboolean (*beginTransaction)( GncSqlConnection* ); /**< Returns TRUE if successful, FALSE if error */ gboolean (*rollbackTransaction)( GncSqlConnection* ); /**< Returns TRUE if successful, FALSE if error */ gboolean (*commitTransaction)( GncSqlConnection* ); /**< Returns TRUE if successful, FALSE if error */