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
This commit is contained in:
John Ralls 2011-01-18 23:42:48 +00:00
parent 56f1e28e36
commit 8be9b0a9ad
5 changed files with 116 additions and 3 deletions

View File

@ -1190,6 +1190,24 @@ gnc_dbi_load( QofBackend* qbe, /*@ dependent @*/ QofBook *book, QofBackendLoadTy
gnc_sql_load( &be->sql_be, book, loadType ); 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( "" ); LEAVE( "" );
} }

View File

@ -114,6 +114,7 @@ int main (int argc, char ** argv)
test_dbi_store_and_reload( "sqlite3", session_1, filename ); test_dbi_store_and_reload( "sqlite3", session_1, filename );
session_1 = create_session(); session_1 = create_session();
test_dbi_safe_save( "sqlite3", filename ); test_dbi_safe_save( "sqlite3", filename );
test_dbi_version_control( "sqlite3", filename );
#ifdef TEST_MYSQL_URL #ifdef TEST_MYSQL_URL
printf( "TEST_MYSQL_URL='%s'\n", TEST_MYSQL_URL ); printf( "TEST_MYSQL_URL='%s'\n", TEST_MYSQL_URL );
if ( strlen( TEST_MYSQL_URL ) > 0 ) if ( strlen( TEST_MYSQL_URL ) > 0 )
@ -121,7 +122,8 @@ int main (int argc, char ** argv)
session_1 = create_session(); session_1 = create_session();
test_dbi_store_and_reload( "mysql", session_1, TEST_MYSQL_URL ); test_dbi_store_and_reload( "mysql", session_1, TEST_MYSQL_URL );
session_1 = create_session(); session_1 = create_session();
test_dbi_safe_save( "msql", filename ); test_dbi_safe_save( "mysql", filename );
test_dbi_version_control( "mysql", filename );
} }
#endif #endif
#ifdef TEST_PGSQL_URL #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 ); test_dbi_store_and_reload( "pgsql", session_1, TEST_PGSQL_URL );
session_1 = create_session(); session_1 = create_session();
test_dbi_safe_save( "pgsql", filename ); test_dbi_safe_save( "pgsql", filename );
test_dbi_version_control( "pgsql", filename );
} }
#endif #endif
print_test_results(); print_test_results();

View File

@ -25,6 +25,7 @@
#include "config.h" #include "config.h"
#include "qof.h" #include "qof.h"
#include "qofsession-p.h"
#include "cashobjects.h" #include "cashobjects.h"
#include "test-engine-stuff.h" #include "test-engine-stuff.h"
#include "test-stuff.h" #include "test-stuff.h"
@ -159,7 +160,9 @@ compare_books( QofBook* book_1, QofBook* book_2 )
test_conn_get_index_list( be ); 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 void
test_dbi_store_and_reload( const gchar* driver, QofSession* session_1, const gchar* url ) 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 ); 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 void
test_dbi_safe_save( const gchar* driver, const gchar* url ) test_dbi_safe_save( const gchar* driver, const gchar* url )
{ {
@ -261,3 +271,64 @@ cleanup:
qof_session_destroy( session_1 ); qof_session_destroy( session_1 );
return; 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 );
}

View File

@ -485,6 +485,7 @@ gnc_sql_sync_all( GncSqlBackend* be, /*@ dependent @*/ QofBook *book )
(void)reset_version_info( be ); (void)reset_version_info( be );
gnc_sql_set_table_version( be, "Gnucash", gnc_get_svn_version() ); 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 */ /* Create new tables */
be->is_pristine_db = TRUE; be->is_pristine_db = TRUE;
@ -538,6 +539,7 @@ gnc_sql_sync_all( GncSqlBackend* be, /*@ dependent @*/ QofBook *book )
} }
else else
{ {
qof_backend_set_error( (QofBackend*)be, ERR_BACKEND_SERVER_ERR );
is_ok = gnc_sql_connection_rollback_transaction( be->conn ); is_ok = gnc_sql_connection_rollback_transaction( be->conn );
} }
LEAVE( "book=%p", book ); LEAVE( "book=%p", book );

View File

@ -43,6 +43,25 @@
#include "qofbackend-p.h" #include "qofbackend-p.h"
#include <gmodule.h> #include <gmodule.h>
/**
* \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; typedef struct GncSqlConnection GncSqlConnection;
/** /**
@ -152,7 +171,7 @@ struct GncSqlConnection
GncSqlResult* (*executeSelectStatement)( GncSqlConnection*, GncSqlStatement* ); /**< Returns NULL if error */ GncSqlResult* (*executeSelectStatement)( GncSqlConnection*, GncSqlStatement* ); /**< Returns NULL if error */
gint (*executeNonSelectStatement)( GncSqlConnection*, GncSqlStatement* ); /**< Returns -1 if error */ gint (*executeNonSelectStatement)( GncSqlConnection*, GncSqlStatement* ); /**< Returns -1 if error */
GncSqlStatement* (*createStatementFromSql)( /*@ observer @*/ GncSqlConnection*, const gchar* ); 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 (*beginTransaction)( GncSqlConnection* ); /**< Returns TRUE if successful, FALSE if error */
gboolean (*rollbackTransaction)( 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 */ gboolean (*commitTransaction)( GncSqlConnection* ); /**< Returns TRUE if successful, FALSE if error */