ODBC-dblog.diff   [plain text]


Add support for logging daemon messages to an SQL database.

To use this patch, run these commands for a successful build:

    patch -p1 <patches/ODBC-dblog.diff
    ./prepare-source
    ./configure --enable-ODBC
    make

See the newly-created file "instructions" for more info.

--- old/Makefile.in
+++ new/Makefile.in
@@ -32,7 +32,7 @@ LIBOBJ=lib/wildmatch.o lib/compat.o lib/
 ZLIBOBJ=zlib/deflate.o zlib/inffast.o zlib/inflate.o zlib/inftrees.o \
 	zlib/trees.o zlib/zutil.o zlib/adler32.o zlib/compress.o zlib/crc32.o
 OBJS1=rsync.o generator.o receiver.o cleanup.o sender.o exclude.o util.o \
-	main.o checksum.o match.o syscall.o log.o backup.o
+	main.o checksum.o match.o syscall.o log.o backup.o @EXTRA_OBJECT@
 OBJS2=options.o flist.o io.o compat.o hlink.o token.o uidlist.o socket.o \
 	fileio.o batch.o clientname.o chmod.o
 OBJS3=progress.o pipe.o
--- old/cleanup.c
+++ new/cleanup.c
@@ -27,6 +27,7 @@ extern int am_server;
 extern int am_daemon;
 extern int io_error;
 extern int keep_partial;
+extern int am_generator;
 extern int log_got_error;
 extern char *partial_dir;
 extern char *logfile_name;
@@ -174,8 +175,13 @@ NORETURN void _exit_cleanup(int code, co
 				code = exit_code = RERR_PARTIAL;
 		}
 
-		if (code || am_daemon || (logfile_name && (am_server || !verbose)))
+		if (code || am_daemon || (logfile_name && (am_server || !verbose))) {
 			log_exit(code, file, line);
+#ifdef HAVE_LIBODBC
+			db_log_exit(code, file, line);
+			db_log_close();
+#endif
+		}
 
 		/* FALLTHROUGH */
 #include "case_N.h"
--- old/clientserver.c
+++ new/clientserver.c
@@ -394,6 +394,9 @@ static int rsync_module(int f_in, int f_
 		   XFLG_ABS_IF_SLASH | XFLG_OLD_PREFIXES);
 
 	log_init(1);
+#ifdef HAVE_LIBODBC
+	db_log_open();
+#endif
 
 #ifdef HAVE_PUTENV
 	if (*lp_prexfer_exec(i) || *lp_postxfer_exec(i)) {
@@ -633,6 +636,9 @@ static int rsync_module(int f_in, int f_
 			rprintf(FLOG, "rsync %s %s from %s@%s (%s)\n",
 				am_sender ? "on" : "to",
 				request, auth_user, host, addr);
+#ifdef HAVE_LIBODBC
+			db_log_session();
+#endif
 		} else {
 			rprintf(FLOG, "rsync %s %s from %s (%s)\n",
 				am_sender ? "on" : "to",
--- old/configure.in
+++ new/configure.in
@@ -610,6 +610,12 @@ if test x"$with_included_popt" != x"yes"
     AC_CHECK_LIB(popt, poptGetContext, , [with_included_popt=yes])
 fi
 
+AC_ARG_ENABLE(ODBC, AC_HELP_STRING([--enable-ODBC], [compile in support for ODBC database logging]),
+    [ AC_CHECK_HEADERS(sql.h sqlext.h sqltypes.h)
+    AC_CHECK_LIB(odbc,SQLExecDirect)
+    EXTRA_OBJECT="$EXTRA_OBJECT dblog.o"
+    AC_SUBST(EXTRA_OBJECT) ])
+
 AC_MSG_CHECKING([whether to use included libpopt])
 if test x"$with_included_popt" = x"yes"; then
     AC_MSG_RESULT($srcdir/popt)
--- old/db_log_error-list.txt
+++ new/db_log_error-list.txt
@@ -0,0 +1,35 @@
+error type		description
+0			not an error.
+1			authentication
+2			file/dir deletion failed
+3			connection closed
+4			read error
+5			multiplexing overflow
+6			unexpected tag
+7			over long v-string received
+8			invalid block length
+9			invalid checksum length
+10			invalid remainder length
+11			failed to write error
+12			attempting to send over-long vstring
+13			temporary filename too long
+14			lseek failed
+15			write failed
+16			rename failed
+17			rsync hack failed
+18			"invalid basis_dir index
+19			fstat failed
+20			is a directory
+21			open file failed
+22			mkstemp failed
+23			close failed
+24			failed verification
+25			IO error, skipping deletion.
+26			directory creation failed
+27			ignoring unsafe symbolic link
+28			symbolic link failed
+29			mknod failed
+30			failed to stat
+31			unlink
+32			failed to open file/directory
+33			open?
--- old/dblog-tables-mysql.sql
+++ new/dblog-tables-mysql.sql
@@ -0,0 +1,64 @@
+drop table transfer;
+drop table exit;
+drop table session;
+
+CREATE TABLE session (
+	id			int auto_increment NOT NULL,
+	date			timestamp NOT NULL,
+	ip_address		varchar(15) NOT NULL,
+	username		varchar(20) NOT NULL,
+	module_name		varchar(20) NOT NULL,
+	module_path		varchar(255) NOT NULL,
+	process_id		int NOT NULL,
+	Primary Key (id)
+);
+
+CREATE TABLE transfer (
+	id			int auto_increment NOT NULL,
+	session_id		int NOT NULL,
+	date			timestamp NOT NULL,
+	file_name		varchar(255) NOT NULL,
+	file_size		bigint NOT NULL,
+	bytes_transferred	bigint NOT NULL,
+	checksum_bytes_transferred bigint NOT NULL,
+	operation		varchar(20),
+	Primary Key (id),
+	foreign key (session_id) references session (id)
+);
+
+CREATE TABLE exit (
+	id			int auto_increment NOT NULL,
+	session_id		int NOT NULL,
+	date			timestamp NOT NULL,
+	total_bytes_written	bigint NOT NULL,
+	total_bytes_read	bigint NOT NULL,
+	total_size		bigint NOT NULL,
+	error_text		varchar(128) NOT NULL,
+	error_code		int NOT NULL,
+	error_file		varchar(64) NOT NULL,
+	error_line		int NOT NULL,
+	process_id		int NOT NULL,
+	Primary Key (id),
+	foreign key (session_id) references session (id)
+);
+
+CREATE TABLE error (
+	id			int auto_increment NOT NULL,
+	session_id		int NOT NULL,
+	date			timestamp NOT NULL,
+	logcode			bigint NOT NULL,
+	error_number		bigint NOT NULL,
+	error_text		varchar(512),
+	PrimaryKey (id),
+	foreign key (session_id) references session (id)
+);
+
+CREATE TABLE delete (
+	id			serial NOT NULL,
+	session_id		int NOT NULL,
+	date			timestamp NOT NULL,
+	path			varchar(512) NOT NULL,
+	mode			int NOT NULL,
+	PrimaryKey (id),
+	foreign key (session_id) references session (id)
+);
--- old/dblog-tables-postgresql.sql
+++ new/dblog-tables-postgresql.sql
@@ -0,0 +1,67 @@
+drop table transfer;
+drop table exit;
+drop table session;
+drop sequence session_id_seq;
+create sequence session_id_seq;
+
+CREATE TABLE "session" (
+	"id"			int NOT NULL,
+	"date"			timestamp NOT NULL default now(),
+	"ip_address"		varchar(15) NOT NULL,
+	"username"		varchar(20) NOT NULL,
+	"module_name"		varchar(20) NOT NULL,
+	"module_path"		varchar(255) NOT NULL,
+	"process_id"		int NOT NULL,
+	Primary Key (id)
+);
+
+CREATE TABLE "transfer" (
+	"id"			serial NOT NULL,
+	"session_id"		int NOT NULL,
+	"date"			timestamp NOT NULL default now(),
+	"file_name"		varchar(512) NOT NULL,
+	"file_size"		bigint NOT NULL,
+	"bytes_transferred"	bigint NOT NULL,
+	"checksum_bytes_transferred" bigint NOT NULL,
+	"operation"		varchar(20),
+	Primary Key (id),
+	foreign key (session_id) references session (id)
+);
+
+CREATE TABLE "exit" (
+	"id"			serial NOT NULL,
+	"session_id"		int NOT NULL,
+	"date"			timestamp NOT NULL default now(),
+	"total_bytes_written"	bigint NOT NULL,
+	"total_bytes_read"	bigint NOT NULL,
+	"total_size"		bigint NOT NULL,
+	"error_text"		varchar(128) NOT NULL,
+	"error_code"		int NOT NULL,
+	"error_file"		varchar(64) NOT NULL,
+	"error_line"		int NOT NULL,
+	"process_id"		int NOT NULL,
+	Primary Key (id),
+	foreign key (session_id) references session (id)
+);
+
+CREATE TABLE "error" (
+	"id"			serial NOT NULL,
+	"session_id"		int NOT NULL,
+	"date"			timestamp NOT NULL default now(),
+	"logcode"		int NOT NULL,
+	"error_number"		int NOT NULL,
+	"error_text"		varchar(512),
+	Primary Key (id),
+	foreign key (session_id) references session (id)
+
+);
+
+CREATE TABLE "delete" (
+	"id"			serial NOT NULL,
+	"session_id"		int NOT NULL,
+	"date"			timestamp NOT NULL default now(),
+	"path"			varchar(512) NOT NULL,
+	"mode"			int NOT NULL,
+	Primary Key (id),
+	foreign key (session_id) references session (id)
+);
--- old/dblog.c
+++ new/dblog.c
@@ -0,0 +1,549 @@
+/*
+ *  ODBC Database logging functions
+ *
+ *  Written by Steve Sether, April 2004
+ *  steve@vellmont.com
+ */
+
+#include "rsync.h"
+
+#ifdef HAVE_SQL_H
+#include <sql.h>
+#else
+#ifdef HAVE_ODBC_SQL_H
+#include <odbc/sql.h>
+#endif
+#endif
+
+#ifdef HAVE_SQLEXT_H
+#include <sqlext.h>
+#else
+#ifdef HAVE_ODBC_SQLEXT_H
+#include <odbc/sqlext.h>
+#endif
+#endif
+
+#ifdef HAVE_SQLTYPES_H
+#include <sqltypes.h>
+#else
+#ifdef HAVE_ODBC_SQLTYPES_H
+#include <odbc/sqltypes.h>
+#endif
+#endif
+
+SQLHENV db_environ_handle;			/* Handle ODBC environment */
+long result;					/* result of functions */
+SQLHDBC db_handle_g = NULL;			/* database connection handle for generator*/
+SQLHDBC db_handle_r = NULL;			/* database connection handle for sender */
+SQLHSTMT sql_statement_handle_g;		/* SQL statement handle for generator*/
+SQLHSTMT sql_statement_handle_r;		/* SQL statement handle for receiver*/
+extern int am_daemon;
+extern int am_sender;
+extern int am_generator;
+extern char *auth_user;
+extern int module_id;
+extern int dry_run;
+
+
+char sql_status[10];				/* Status SQL */
+SQLINTEGER V_OD_err, V_OD_rowanz, V_OD_id;
+SQLSMALLINT V_OD_mlen, V_OD_colanz;
+char V_OD_msg[200], V_OD_buffer[200];
+SQLINTEGER session_id;
+
+
+/* This function simply removes invalid characters from the SQL statement
+ * to prevent SQL injection attacks. */
+char *sanitizeSql(const char *input)
+{
+	char *out, *ptr;
+	const char *c;
+
+	if (strlen(input) > ((~(unsigned int)0)>>1)-3)
+		return 0;
+	if (!(out = ptr = new_array(char, strlen(input) * 2 + 1)))
+		return 0;
+
+	for (c = input;  *c;  c++) {
+		switch (*c) {
+		case '\'':
+			*ptr++ = '\'';
+			*ptr++ = '\'';
+			break;
+		case '\b':
+			*ptr++ = '\\';
+			*ptr++ = 'b';
+			break;
+		case '\n':
+			*ptr++ = '\\';
+			*ptr++ = 'n';
+			break;
+		case '\r':
+			*ptr++ = '\\';
+			*ptr++ = 'r';
+			break;
+		case '\t':
+			*ptr++ = '\\';
+			*ptr++ = 't';
+			break;
+		default:
+			*ptr++ = *c;
+			break;
+		}
+	}
+	*ptr = '\0';
+	return out;
+}
+
+void db_log_open(void)
+{
+	if (!lp_database_logging(module_id))
+		return;
+
+	/* get ODBC environment handle */
+	result = SQLAllocHandle(SQL_HANDLE_ENV,SQL_NULL_HANDLE,&db_environ_handle);
+	if (result != SQL_SUCCESS && result != SQL_SUCCESS_WITH_INFO) {
+		rprintf(FERROR, "Error: couldn't get database environment handle\n");
+		return;
+	}
+
+	/* Setting database enviroment */
+	result = SQLSetEnvAttr(db_environ_handle, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0);
+	if (result != SQL_SUCCESS && result != SQL_SUCCESS_WITH_INFO) {
+		rprintf(FERROR, "Error: couldn't set database environment.\n");
+		SQLFreeHandle(SQL_HANDLE_ENV, db_environ_handle);
+		db_environ_handle = NULL;
+		return;
+	}
+	if (db_handle_g == NULL) {
+		/* Get a database handle for the generator*/
+		result = SQLAllocHandle(SQL_HANDLE_DBC, db_environ_handle, &db_handle_g);
+		if (result != SQL_SUCCESS && result != SQL_SUCCESS_WITH_INFO) {
+			rprintf(FERROR, "Error: couldn't allocate database handle for generator\n");
+			SQLFreeHandle(SQL_HANDLE_ENV, db_environ_handle);
+			db_environ_handle = NULL;
+			return;
+		}
+
+		/* Set connection attributes for the generator db connection */
+		SQLSetConnectAttr(db_handle_g, SQL_LOGIN_TIMEOUT, (SQLPOINTER *)5, 0);
+
+		/* get the database connection for the generator. */
+		result = SQLConnect(db_handle_g, (SQLCHAR*) lp_database_datasource(module_id), SQL_NTS,
+		    (SQLCHAR*) lp_database_username(module_id), SQL_NTS,
+		    (SQLCHAR*) lp_database_password(module_id), SQL_NTS);
+
+		if (result != SQL_SUCCESS && result != SQL_SUCCESS_WITH_INFO) {
+			SQLGetDiagRec(SQL_HANDLE_DBC, db_handle_g, 1,
+			    sql_status, &V_OD_err, V_OD_msg, 100, &V_OD_mlen);
+			rprintf(FERROR,"Error Connecting to Database (generator) %s\n",V_OD_msg);
+			SQLFreeHandle(SQL_HANDLE_DBC,db_handle_g);
+			db_handle_g = NULL;
+			SQLFreeHandle(SQL_HANDLE_ENV, db_environ_handle);
+			db_environ_handle = NULL;
+			return;
+		}
+		rprintf(FLOG,"Connected to database for generator!\n");
+	} else {
+		rprintf(FERROR,"Already connected to database for generator\n");
+	}
+	if (db_handle_r == NULL) {
+		/* Get a database handle for the receiver */
+		result = SQLAllocHandle(SQL_HANDLE_DBC, db_environ_handle, &db_handle_r);
+		if (result != SQL_SUCCESS && result != SQL_SUCCESS_WITH_INFO) {
+			rprintf(FERROR, "Error: couldn't allocate database handle for receiver\n");
+			SQLFreeHandle(SQL_HANDLE_ENV, db_environ_handle);
+			db_environ_handle = NULL;
+			return;
+		}
+
+		/* Set connection attributes for the receiver db connection */
+		SQLSetConnectAttr(db_handle_r, SQL_LOGIN_TIMEOUT, (SQLPOINTER *)5, 0);
+
+		/* get the generator connection for the receiver. */
+		result = SQLConnect(db_handle_r, (SQLCHAR*) lp_database_datasource(module_id), SQL_NTS,
+		    (SQLCHAR*) lp_database_username(module_id), SQL_NTS,
+		    (SQLCHAR*) lp_database_password(module_id), SQL_NTS);
+
+		if (result != SQL_SUCCESS && result != SQL_SUCCESS_WITH_INFO) {
+			SQLGetDiagRec(SQL_HANDLE_DBC, db_handle_r,1,
+			    sql_status, &V_OD_err,V_OD_msg,100,&V_OD_mlen);
+			rprintf(FERROR,"Error Connecting to Database (receiver) %s\n",V_OD_msg);
+			SQLFreeHandle(SQL_HANDLE_DBC,db_handle_r);
+			db_handle_r = NULL;
+			SQLFreeHandle(SQL_HANDLE_ENV, db_environ_handle);
+			db_environ_handle = NULL;
+			return;
+		}
+		rprintf(FLOG,"Connected to database for receiver!\n");
+	} else {
+		rprintf(FERROR,"Already connected to database for receiver\n");
+	}
+
+	/* get SQL statement handle for generator */
+	result = SQLAllocHandle(SQL_HANDLE_STMT, db_handle_g, &sql_statement_handle_g);
+	if (result != SQL_SUCCESS && result != SQL_SUCCESS_WITH_INFO) {
+		SQLGetDiagRec(SQL_HANDLE_DBC, db_handle_g,1, sql_status,&V_OD_err,V_OD_msg,100,&V_OD_mlen);
+		rprintf(FERROR,"Error in allocating SQL statement handle %s\n",V_OD_msg);
+		SQLDisconnect(db_handle_g);
+		SQLFreeHandle(SQL_HANDLE_DBC,db_handle_g);
+		db_handle_g = NULL;
+		SQLFreeHandle(SQL_HANDLE_ENV, db_environ_handle);
+		db_environ_handle = NULL;
+		return;
+	}
+
+	/* get SQL statement handle for receiver */
+	result = SQLAllocHandle(SQL_HANDLE_STMT, db_handle_r, &sql_statement_handle_r);
+	if (result != SQL_SUCCESS && result != SQL_SUCCESS_WITH_INFO) {
+		SQLGetDiagRec(SQL_HANDLE_DBC, db_handle_r,1, sql_status,&V_OD_err,V_OD_msg,100,&V_OD_mlen);
+		rprintf(FERROR,"Error in allocating SQL statement handle %s\n",V_OD_msg);
+		SQLDisconnect(db_handle_r);
+		SQLFreeHandle(SQL_HANDLE_DBC,db_handle_r);
+		db_handle_r = NULL;
+		SQLFreeHandle(SQL_HANDLE_ENV, db_environ_handle);
+		db_environ_handle = NULL;
+		return;
+	}
+}
+
+void db_log_close()
+{
+	if (!lp_database_logging(module_id))
+		return;
+
+	if (am_generator) {
+		if (sql_statement_handle_g != NULL) {
+			/* free the statement handle first */
+			SQLFreeHandle(SQL_HANDLE_STMT,sql_statement_handle_g);
+			sql_statement_handle_g = NULL;
+		} else {
+			rprintf(FERROR,"No generator sql statement handle to close\n");
+		}
+
+		if (db_handle_g != NULL) {
+			/* disconnect, and free the database handle. */
+			SQLDisconnect(db_handle_g);
+			SQLFreeHandle(SQL_HANDLE_DBC,db_handle_g);
+			db_handle_g = NULL;
+		} else {
+			rprintf(FERROR,"Generator database connection already closed\n");
+		}
+	} else { /* must be receiver */
+		if (sql_statement_handle_r != NULL) {
+			/* free the statement handle first */
+			SQLFreeHandle(SQL_HANDLE_STMT,sql_statement_handle_r);
+			sql_statement_handle_r = NULL;
+		} else {
+			rprintf(FERROR,"No receiver sql statement handle to close\n");
+		}
+
+		if (db_handle_r != NULL) {
+			/* disconnect, and free the database handle. */
+			SQLDisconnect(db_handle_r);
+			SQLFreeHandle(SQL_HANDLE_DBC,db_handle_r);
+			db_handle_r = NULL;
+		} else {
+			rprintf(FERROR,"Receiver database connection already closed\n");
+		}
+	}
+
+	if (db_environ_handle != NULL) {
+		/* free the environment handle */
+		SQLFreeHandle(SQL_HANDLE_ENV, db_environ_handle);
+		db_environ_handle = NULL;
+	} else {
+		rprintf(FERROR,"No environment handle to close\n");
+	}
+}
+
+static long get_unique_session_id()
+{
+	long unique;
+	char strSqlStatement[1024];
+	SQLHDBC db_handle = (am_generator) ? db_handle_g : db_handle_r;
+	SQLHSTMT sql_statement_handle = (am_generator) ? sql_statement_handle_g : sql_statement_handle_r;
+
+	if (db_handle != NULL) {
+		/* choose the appropriate select statement based upon which DBMS we're using.
+		 * different datbases use different methods to get a unique ID.  Some use a counter
+		 * object (sequence), others use an auto increment datatype and have a method
+		 * to get the last ID inserted using this connection. */
+		if (strcmp(lp_unique_id_method(module_id),"nextval-postgresql") == 0) {
+			snprintf(strSqlStatement, sizeof strSqlStatement,
+			    "SELECT NEXTVAL('%s');", lp_sequence_name(module_id));
+		} else if (strcmp(lp_unique_id_method(module_id),"nextval-oracle") == 0) {
+			snprintf(strSqlStatement, sizeof strSqlStatement,
+			    "SELECT %s.NEXTVAL FROM dual;", lp_sequence_name(module_id));
+		} else if (strcmp(lp_unique_id_method(module_id),"nextval-db2") == 0) {
+			snprintf(strSqlStatement, sizeof strSqlStatement,
+			    "VALUES NEXTVAL FOR %s;",lp_sequence_name(module_id));
+		} else if (strcmp(lp_unique_id_method(module_id),"last_insert_id") == 0) { /* MySql */
+			snprintf(strSqlStatement, sizeof strSqlStatement,
+			    "SELECT LAST_INSERT_ID()");
+		} else if (strcmp(lp_unique_id_method(module_id),"@@IDENTITY") == 0) { /* Sybase */
+			snprintf(strSqlStatement, sizeof strSqlStatement,
+			    "SELECT @@IDENTITY");
+		} else if (strcmp(lp_unique_id_method(module_id),"custom") == 0){ /* Users custom statement */
+			snprintf(strSqlStatement, sizeof strSqlStatement,
+			    lp_custom_unique_id_select(module_id));
+		}
+
+		/* bind the 1st column to unique */
+		SQLBindCol(sql_statement_handle,1,SQL_C_LONG,&unique,150,&V_OD_err);
+		/* execute the SQL statement */
+		result = SQLExecDirect(sql_statement_handle,strSqlStatement,SQL_NTS);
+		if (result != SQL_SUCCESS && result != SQL_SUCCESS_WITH_INFO) {
+			SQLGetDiagRec(SQL_HANDLE_DBC, db_handle,1, sql_status,&V_OD_err,V_OD_msg,100,&V_OD_mlen);
+			rprintf(FERROR,"Error at get_sequence:  Error in Select! %s %s\n",strSqlStatement,V_OD_msg);
+		} else {
+			result = SQLFetch(sql_statement_handle);
+			if (result != SQL_NO_DATA && unique != 0) {
+				rprintf(FINFO,"Got unique sequence! %ld\n",unique);
+			} else {
+				rprintf(FERROR,"Error at get_sequence:  Didn't get unique session ID\n");
+			}
+			/* Close the cursor so the statement can be re-used */
+			result = SQLFreeStmt(sql_statement_handle,SQL_CLOSE);
+			if (result != SQL_SUCCESS && result != SQL_SUCCESS_WITH_INFO) {
+				SQLGetDiagRec(SQL_HANDLE_DBC, db_handle,1, sql_status,&V_OD_err,V_OD_msg,100,&V_OD_mlen);
+				rprintf(FERROR,"Error at get_sequence: Error in closing SQL statement handle %s\n",V_OD_msg);
+				return unique;
+			}
+			return unique;
+		}
+	}
+	rprintf(FERROR,"Error at get_sequence: Not connected to database\n");
+	return -1;
+}
+
+void db_log_session()
+{
+	char strSqlStatement[1024];
+	int gotSessionID = 0;
+	SQLHDBC db_handle = (am_generator) ? db_handle_g : db_handle_r;
+	SQLHSTMT sql_statement_handle = (am_generator) ? sql_statement_handle_g : sql_statement_handle_r;
+
+	if (!lp_database_logging(module_id))
+		return;
+
+	if (db_handle != NULL) {
+		/* if we're using a sequence via the nextval command to
+		 * get a unique ID, we need to get it before we do the
+		 * insert. We also get the unique ID  now if custom,
+		 * and get_custom_id_before_insert is set. */
+		if (strcmp(lp_unique_id_method(module_id),"nextval-postgresql") == 0
+		 || strcmp(lp_unique_id_method(module_id),"nextval-oracle") == 0
+		 || strcmp(lp_unique_id_method(module_id),"nextval-db2") == 0
+		 || (strcmp(lp_unique_id_method(module_id),"custom") == 0
+		  && lp_get_custom_id_before_insert(module_id))) {
+			session_id = get_unique_session_id();
+			gotSessionID = 1;
+			snprintf(strSqlStatement, sizeof strSqlStatement,
+			    "INSERT INTO %s (id, date, ip_address, username, module_name, module_path, process_id) VALUES ('%ld', '%s', '%s', '%s','%s','%s','%d');",
+			    lp_session_table_name(module_id), session_id, timestring(time(NULL)), client_addr(0),
+			    auth_user, lp_name(module_id), lp_path(module_id), getpid());
+		} else {
+			/* Otherwise the ID gets created automatically, and we get the ID it used after the insert. */
+			snprintf(strSqlStatement, sizeof strSqlStatement,
+			    "INSERT INTO %s (date, ip_address, username, module_name, module_path, process_id) VALUES ('%s', '%s', '%s', '%s','%s','%d');",
+			    lp_session_table_name(module_id), timestring(time(NULL)), client_addr(0), auth_user,
+			    lp_name(module_id), lp_path(module_id), getpid());
+		}
+
+		/* Insert the new session into the database */
+		result = SQLExecDirect(sql_statement_handle,strSqlStatement,SQL_NTS);
+		if (result != SQL_SUCCESS && result != SQL_SUCCESS_WITH_INFO) {
+			SQLGetDiagRec(SQL_HANDLE_DBC, db_handle,1, sql_status,&V_OD_err,V_OD_msg,100,&V_OD_mlen);
+			rprintf(FERROR,"Error at db_log_session: Error in Insert %s %s\n",strSqlStatement,V_OD_msg);
+		}
+
+		/* close the cursor so the statement handle can be re-used. */
+		result = SQLFreeStmt(sql_statement_handle,SQL_CLOSE);
+		if (result != SQL_SUCCESS && result != SQL_SUCCESS_WITH_INFO) {
+			SQLGetDiagRec(SQL_HANDLE_DBC, db_handle,1, sql_status,&V_OD_err,V_OD_msg,100,&V_OD_mlen);
+			rprintf(FERROR,"Error in resetting SQL statement handle %s\n",V_OD_msg);
+		}
+		/* get the session ID for databases that give the unique ID after an insert */
+		if (gotSessionID == 0) {
+			session_id = get_unique_session_id();
+		}
+	} else {
+		rprintf(FERROR,"Error at db_log_session:  Not connected to database!\n");
+	}
+}
+
+void db_log_transfer(struct file_struct *file,struct stats *initial_stats,char *operation)
+{
+	extern struct stats stats;
+	char strSqlStatement[2048];
+	char strFileName[MAXPATHLEN];
+	char *strFileNamePtr;
+	char strFileSize[255];
+	int64 intBytesTransferred;
+	int64 intCheckSumBytes;
+	SQLHDBC db_handle = (am_generator) ? db_handle_g : db_handle_r;
+	SQLHSTMT sql_statement_handle = (am_generator) ? sql_statement_handle_g : sql_statement_handle_r;
+
+	if (!lp_database_logging(module_id))
+		return;
+
+	if (db_handle != NULL) {
+		strFileNamePtr = f_name(file, NULL);
+		if (am_sender && file->dir.root) {
+			pathjoin(strFileName, sizeof strFileName,
+				 file->dir.root, strFileNamePtr);
+			strFileNamePtr = strFileName;
+		}
+		clean_fname(strFileNamePtr, 0);
+		if (*strFileNamePtr == '/')
+			strFileNamePtr++;
+
+		snprintf(strFileSize, sizeof strFileSize, "%.0f", (double)file->length);
+		if (am_sender) {
+			intBytesTransferred = stats.total_written - initial_stats->total_written;
+		} else {
+			intBytesTransferred = stats.total_read - initial_stats->total_read;
+		}
+
+		if (!am_sender) {
+			intCheckSumBytes = stats.total_written - initial_stats->total_written;
+		} else {
+			intCheckSumBytes = stats.total_read - initial_stats->total_read;
+		}
+
+		snprintf(strSqlStatement, sizeof strSqlStatement,
+		    "INSERT INTO %s (session_id,date, file_name, file_size, bytes_transferred, checksum_bytes_transferred, operation) VALUES ('%ld','%s','%s','%s','%Ld','%Ld','%s');",
+		    lp_transfer_table_name(module_id), session_id, timestring(time(NULL)),
+		    sanitizeSql(strFileNamePtr), strFileSize, intBytesTransferred,
+		    intCheckSumBytes, operation);
+		result = SQLExecDirect(sql_statement_handle,strSqlStatement,SQL_NTS);
+		if (result != SQL_SUCCESS && result != SQL_SUCCESS_WITH_INFO) {
+			SQLGetDiagRec(SQL_HANDLE_DBC, db_handle,1, sql_status,&V_OD_err,V_OD_msg,100,&V_OD_mlen);
+			rprintf(FERROR,"Error at db_log_transfer:  Error in Insert %s %s\n",strSqlStatement,V_OD_msg);
+			if (result == SQL_INVALID_HANDLE)
+				rprintf(FERROR,"INVALID HANDLE\n");
+		}
+	} else {
+		rprintf(FERROR,"Error at db_log_transfer: Not connected to database!\n");
+	}
+}
+
+void db_log_exit(int code, const char *file, int line)
+{
+	char strSqlStatement[2048];
+	const char *error_text;
+	extern struct stats stats;
+	SQLHDBC db_handle = (am_generator) ? db_handle_g : db_handle_r;
+	SQLHSTMT sql_statement_handle = (am_generator) ? sql_statement_handle_g : sql_statement_handle_r;
+
+	if (!lp_database_logging(module_id))
+		return;
+
+	if (db_handle != NULL) {
+		if (code != 0) {
+			error_text = rerr_name(code);
+			if (!error_text) {
+				error_text = "unexplained error";
+			}
+		} else {
+			error_text = "";
+		}
+		snprintf(strSqlStatement, sizeof strSqlStatement,
+		    "INSERT INTO %s (session_id, date, total_bytes_written,total_bytes_read,total_size,error_text,error_code,error_file,error_line,process_id) VALUES ('%ld','%s','%Ld','%Ld','%Ld','%s','%d','%s','%d','%d');",
+		    lp_exit_table_name(module_id), session_id, timestring(time(NULL)), stats.total_written,
+		    stats.total_read, stats.total_size, error_text, code, file, line, getpid());
+
+		result = SQLExecDirect(sql_statement_handle,strSqlStatement,SQL_NTS);
+
+		if (result != SQL_SUCCESS && result != SQL_SUCCESS_WITH_INFO) {
+			SQLGetDiagRec(SQL_HANDLE_DBC, db_handle,1, sql_status,&V_OD_err,V_OD_msg,100,&V_OD_mlen);
+			rprintf(FERROR,"Error at db_log_exit: Error in Insert %s %s\n",strSqlStatement,V_OD_msg);
+		}
+	} else {
+		rprintf(FERROR,"Error at db_log_exit: Not connected to database!\n");
+	}
+}
+
+void db_log_delete(char *fname, int mode)
+{
+	char strSqlStatement[2048];
+	SQLHDBC db_handle = (am_generator) ? db_handle_g : db_handle_r;
+	SQLHSTMT sql_statement_handle = (am_generator) ? sql_statement_handle_g : sql_statement_handle_r;
+
+	if (!am_daemon || dry_run || !lp_database_logging(module_id))
+		return;
+
+	if (db_handle != NULL) {
+		snprintf(strSqlStatement, sizeof strSqlStatement,
+		    "INSERT INTO %s (session_id, date, path, mode) VALUES ('%ld','%s','%s','%d');",
+		    lp_delete_table_name(module_id), session_id, timestring(time(NULL)), sanitizeSql(fname), mode);
+
+		result = SQLExecDirect(sql_statement_handle,strSqlStatement,SQL_NTS);
+
+		if (result != SQL_SUCCESS && result != SQL_SUCCESS_WITH_INFO) {
+			SQLGetDiagRec(SQL_HANDLE_DBC, db_handle,1, sql_status,&V_OD_err,V_OD_msg,100,&V_OD_mlen);
+			rprintf(FERROR,"Error at db_log_delete: Error in Insert %s %s\n",strSqlStatement,V_OD_msg);
+		}
+	} else {
+		rprintf(FERROR,"Error at db_log_delete: Not connected to database!\n");
+	}
+}
+
+void db_log_error(enum logcode code, int errcode, const char *format,...)
+{
+	char strSqlStatement[MAXPATHLEN+1024];
+	va_list ap;
+	char buf[MAXPATHLEN+512];
+	size_t len;
+	SQLHDBC db_handle = (am_generator) ? db_handle_g : db_handle_r;
+	SQLHSTMT sql_statement_handle = (am_generator) ? sql_statement_handle_g : sql_statement_handle_r;
+
+	if (!lp_database_logging(module_id))
+		return;
+
+	va_start(ap, format);
+	len = vsnprintf(buf, sizeof buf, format, ap);
+	va_end(ap);
+
+	/* Deal with buffer overruns.  Instead of panicking, just
+	 * truncate the resulting string.  (Note that configure ensures
+	 * that we have a vsnprintf() that doesn't ever return -1.) */
+	if (len > sizeof buf - 1) {
+		const char ellipsis[] = "[...]";
+
+		/* Reset length, and zero-terminate the end of our buffer */
+		len = sizeof buf - 1;
+		buf[len] = '\0';
+
+		/* Copy the ellipsis to the end of the string, but give
+		 * us one extra character:
+		 *
+		 *                  v--- null byte at buf[sizeof buf - 1]
+		 *        abcdefghij0
+		 *     -> abcd[...]00  <-- now two null bytes at end
+		 *
+		 * If the input format string has a trailing newline,
+		 * we copy it into that extra null; if it doesn't, well,
+		 * all we lose is one byte.  */
+		strncpy(buf+len-sizeof ellipsis, ellipsis, sizeof ellipsis);
+		if (format[strlen(format)-1] == '\n') {
+			buf[len-1] = '\n';
+		}
+	}
+
+	if (db_handle != NULL) {
+		snprintf(strSqlStatement, sizeof strSqlStatement,
+		    "INSERT INTO %s (session_id, date, logcode, error_number, error_text) VALUES ('%ld','%s','%d','%d','%s');",
+		    lp_error_table_name(module_id), session_id, timestring(time(NULL)), code, errcode, sanitizeSql(buf));
+
+		result = SQLExecDirect(sql_statement_handle,strSqlStatement,SQL_NTS);
+
+		if (result != SQL_SUCCESS && result != SQL_SUCCESS_WITH_INFO) {
+			SQLGetDiagRec(SQL_HANDLE_DBC, db_handle,1, sql_status,&V_OD_err,V_OD_msg,100,&V_OD_mlen);
+			rprintf(FERROR,"Error at db_log_error: Error in Insert %s %s\n",strSqlStatement,V_OD_msg);
+		}
+	} else {
+		rprintf(FERROR,"Error at db_log_error: Not connected to database!\n");
+	}
+}
--- old/instructions
+++ new/instructions
@@ -0,0 +1,84 @@
+This patch adds the following options:
+
+"database logging"
+    If set to True, rsync will attempt to connect to
+    the specified datasource and write to the named tables.
+    Defaults to False.
+
+"database datasource"
+    Specifies the name of the ODBC data source to use.
+
+"database username"
+    The username to use when connecting to the database.
+
+"database password"
+    The password to use when connecting to the database.
+
+"transfer table name"
+    The name of the transfer table to log to.  This table contains individual
+    filenames, file sizes, bytes transferred, checksum bytes transferred,
+    operation (send or receive), and a timestamp.
+
+"session table name"
+    The name of the session table to log to.  This table contains the username,
+    module name, module path, ip address, process ID, and a timestamp.
+
+"exit table name"
+
+    The name of the exit table to log to.  This table contains the total bytes
+    read, total bytes written, total size of all files, error code, line the
+    error occured at, file the error occured at and the text of the error.
+    (most of which will be blank if the program exited normally).
+
+"delete table name"
+
+    The name of the table to log deleted files/directories to.
+
+"error table name"
+
+   The name of the table errors will be logged to.
+
+"unique id method"
+    Different databases use different methods to get a unique identifier.
+    Some databases support sequence objects, and use various forms of the
+    nextval command to retrieve a unique identifier from it.  Other databases
+    support an autonumber field, and support different methds of retrieving
+    the ID used in the last insert.  Valid values for this option are:
+
+	nextval-postgres
+	    uses the syntax of nextval for PostgreSQL databases
+
+	nextval-oracle
+	    uses the syntax of nextval for Oracle databases
+
+	nextval-db2
+	    uses the syntax of nextval for DB2 databases
+
+	last_insert_id
+	    uses the last_insert_id() command for the MySQL databases
+
+	@@IDENTITY
+	    uses the @@IDENTITY command for Sybase databases
+
+	custom
+	    Define your own method to get a unique identifier.  See the
+	    "custom unique id select", and the "get custom id before select"
+	    parameters.
+
+"sequence name"
+    If your database supports sequences, list the name of the sequence to use
+    for the session unique identifier.
+
+"custom unique id select"
+    Only used if you specify the custom method in "unique id method".  This is
+    a SQL statement to be executed to get a unique ID.  This SQL statement must
+    return one column with the unique ID to use for the session ID.  Should be
+    used in concert with the "get custom id before select" parameter.
+
+"get custom id before insert"
+    This parameter is ignored unless the "unique id method" is set to custom.
+    If set to true, the "custom unique id select" statement will be executed
+    BEFORE the session row is inserted into the database.  (as is done when a
+    sequence is used for unique IDs).  If False the statement will be executed
+    after the session row is inserted (as is done when the session ID is
+    automatically generates unique IDs).  Defaults to True.
--- old/loadparm.c
+++ new/loadparm.c
@@ -122,9 +122,16 @@ typedef struct
 {
 	char *auth_users;
 	char *comment;
+	char *custom_unique_id_select;
+	char *database_datasource;
+	char *database_password;
+	char *database_username;
+	char *delete_table_name;
 	char *dont_compress;
+	char *error_table_name;
 	char *exclude;
 	char *exclude_from;
+	char *exit_table_name;
 	char *filter;
 	char *gid;
 	char *hosts_allow;
@@ -142,14 +149,20 @@ typedef struct
 	char *prexfer_exec;
 	char *refuse_options;
 	char *secrets_file;
+	char *sequence_name;
+	char *session_table_name;
 	char *temp_dir;
+	char *transfer_table_name;
 	char *uid;
+	char *unique_id_method;
 
 	int max_connections;
 	int max_verbosity;
 	int syslog_facility;
 	int timeout;
 
+	BOOL database_logging;
+	BOOL get_custom_id_before_insert;
 	BOOL ignore_errors;
 	BOOL ignore_nonreadable;
 	BOOL list;
@@ -169,9 +182,16 @@ static service sDefault =
 {
  /* auth_users; */		NULL,
  /* comment; */			NULL,
+ /* custom_unique_id_select; */	NULL,
+ /* database_datasource; */	NULL,
+ /* database_password; */	NULL,
+ /* database_username; */	NULL,
+ /* delete_table_name; */	NULL,
  /* dont_compress; */		"*.gz *.tgz *.zip *.z *.rpm *.deb *.iso *.bz2 *.tbz",
+ /* error_table_name; */	NULL,
  /* exclude; */			NULL,
  /* exclude_from; */		NULL,
+ /* exit_table_name; */		NULL,
  /* filter; */			NULL,
  /* gid; */			NOBODY_GROUP,
  /* hosts_allow; */		NULL,
@@ -189,14 +209,20 @@ static service sDefault =
  /* prexfer_exec; */		NULL,
  /* refuse_options; */		NULL,
  /* secrets_file; */		NULL,
+ /* sequence_name; */		NULL,
+ /* session_table_name; */	NULL,
  /* temp_dir; */ 		NULL,
+ /* transfer_table_name; */	NULL,
  /* uid; */			NOBODY_USER,
+ /* unique_id_method; */	NULL,
 
  /* max_connections; */		0,
  /* max_verbosity; */		1,
  /* syslog_facility; */		LOG_DAEMON,
  /* timeout; */			0,
 
+ /* database_logging; */	False,
+ /* get_custom_id_before_insert; */ True,
  /* ignore_errors; */		False,
  /* ignore_nonreadable; */	False,
  /* list; */			True,
@@ -295,10 +321,19 @@ static struct parm_struct parm_table[] =
 
  {"auth users",        P_STRING, P_LOCAL, &sDefault.auth_users,        NULL,0},
  {"comment",           P_STRING, P_LOCAL, &sDefault.comment,           NULL,0},
+ {"custom unique id select",P_STRING,P_LOCAL,&sDefault.custom_unique_id_select,NULL,0},
+ {"database datasource",P_STRING,P_LOCAL, &sDefault.database_datasource,NULL,0},
+ {"database logging",  P_BOOL,   P_LOCAL, &sDefault.database_logging,  NULL,0},
+ {"database password", P_STRING, P_LOCAL, &sDefault.database_password, NULL,0},
+ {"database username", P_STRING, P_LOCAL, &sDefault.database_username, NULL,0},
+ {"delete table name", P_STRING, P_LOCAL, &sDefault.delete_table_name, NULL,0},
  {"dont compress",     P_STRING, P_LOCAL, &sDefault.dont_compress,     NULL,0},
+ {"error table name",  P_STRING, P_LOCAL, &sDefault.error_table_name,  NULL,0},
  {"exclude from",      P_STRING, P_LOCAL, &sDefault.exclude_from,      NULL,0},
  {"exclude",           P_STRING, P_LOCAL, &sDefault.exclude,           NULL,0},
+ {"exit table name",   P_STRING, P_LOCAL, &sDefault.exit_table_name,   NULL,0},
  {"filter",            P_STRING, P_LOCAL, &sDefault.filter,            NULL,0},
+ {"get custom id before insert",P_BOOL,P_LOCAL,&sDefault.get_custom_id_before_insert,NULL,0},
  {"gid",               P_STRING, P_LOCAL, &sDefault.gid,               NULL,0},
  {"hosts allow",       P_STRING, P_LOCAL, &sDefault.hosts_allow,       NULL,0},
  {"hosts deny",        P_STRING, P_LOCAL, &sDefault.hosts_deny,        NULL,0},
@@ -323,12 +358,16 @@ static struct parm_struct parm_table[] =
  {"read only",         P_BOOL,   P_LOCAL, &sDefault.read_only,         NULL,0},
  {"refuse options",    P_STRING, P_LOCAL, &sDefault.refuse_options,    NULL,0},
  {"secrets file",      P_STRING, P_LOCAL, &sDefault.secrets_file,      NULL,0},
+ {"sequence name",     P_STRING, P_LOCAL, &sDefault.sequence_name,     NULL,0},
+ {"session table name",P_STRING, P_LOCAL, &sDefault.session_table_name,NULL,0},
  {"strict modes",      P_BOOL,   P_LOCAL, &sDefault.strict_modes,      NULL,0},
  {"syslog facility",   P_ENUM,   P_LOCAL, &sDefault.syslog_facility,enum_facilities,0},
  {"temp dir",          P_PATH,   P_LOCAL, &sDefault.temp_dir,          NULL,0},
  {"timeout",           P_INTEGER,P_LOCAL, &sDefault.timeout,           NULL,0},
  {"transfer logging",  P_BOOL,   P_LOCAL, &sDefault.transfer_logging,  NULL,0},
+ {"transfer table name",P_STRING,P_LOCAL, &sDefault.transfer_table_name,NULL,0},
  {"uid",               P_STRING, P_LOCAL, &sDefault.uid,               NULL,0},
+ {"unique id method",  P_STRING, P_LOCAL, &sDefault.unique_id_method,  NULL,0},
  {"use chroot",        P_BOOL,   P_LOCAL, &sDefault.use_chroot,        NULL,0},
  {"write only",        P_BOOL,   P_LOCAL, &sDefault.write_only,        NULL,0},
  {NULL,                P_BOOL,   P_NONE,  NULL,                        NULL,0}
@@ -384,9 +423,16 @@ FN_GLOBAL_INTEGER(lp_rsync_port, &Global
 
 FN_LOCAL_STRING(lp_auth_users, auth_users)
 FN_LOCAL_STRING(lp_comment, comment)
+FN_LOCAL_STRING(lp_custom_unique_id_select,custom_unique_id_select)
+FN_LOCAL_STRING(lp_database_datasource, database_datasource)
+FN_LOCAL_STRING(lp_database_password, database_password)
+FN_LOCAL_STRING(lp_database_username, database_username)
+FN_LOCAL_STRING(lp_delete_table_name,delete_table_name)
 FN_LOCAL_STRING(lp_dont_compress, dont_compress)
+FN_LOCAL_STRING(lp_error_table_name,error_table_name)
 FN_LOCAL_STRING(lp_exclude, exclude)
 FN_LOCAL_STRING(lp_exclude_from, exclude_from)
+FN_LOCAL_STRING(lp_exit_table_name, exit_table_name)
 FN_LOCAL_STRING(lp_filter, filter)
 FN_LOCAL_STRING(lp_gid, gid)
 FN_LOCAL_STRING(lp_hosts_allow, hosts_allow)
@@ -404,14 +450,20 @@ FN_LOCAL_STRING(lp_postxfer_exec, postxf
 FN_LOCAL_STRING(lp_prexfer_exec, prexfer_exec)
 FN_LOCAL_STRING(lp_refuse_options, refuse_options)
 FN_LOCAL_STRING(lp_secrets_file, secrets_file)
+FN_LOCAL_STRING(lp_sequence_name,sequence_name)
+FN_LOCAL_STRING(lp_session_table_name,session_table_name)
 FN_LOCAL_INTEGER(lp_syslog_facility, syslog_facility)
 FN_LOCAL_STRING(lp_temp_dir, temp_dir)
+FN_LOCAL_STRING(lp_transfer_table_name, transfer_table_name)
 FN_LOCAL_STRING(lp_uid, uid)
+FN_LOCAL_STRING(lp_unique_id_method,unique_id_method)
 
 FN_LOCAL_INTEGER(lp_max_connections, max_connections)
 FN_LOCAL_INTEGER(lp_max_verbosity, max_verbosity)
 FN_LOCAL_INTEGER(lp_timeout, timeout)
 
+FN_LOCAL_BOOL(lp_database_logging, database_logging)
+FN_LOCAL_BOOL(lp_get_custom_id_before_insert,get_custom_id_before_insert)
 FN_LOCAL_BOOL(lp_ignore_errors, ignore_errors)
 FN_LOCAL_BOOL(lp_ignore_nonreadable, ignore_nonreadable)
 FN_LOCAL_BOOL(lp_list, list)
--- old/log.c
+++ new/log.c
@@ -93,7 +93,7 @@ struct {
 /*
  * Map from rsync error code to name, or return NULL.
  */
-static char const *rerr_name(int code)
+char const *rerr_name(int code)
 {
 	int i;
 	for (i = 0; rerr_names[i].name; i++) {
--- old/receiver.c
+++ new/receiver.c
@@ -110,6 +110,10 @@ static int get_tmpname(char *fnametmp, c
 
 	if (maxname < 1) {
 		rprintf(FERROR, "temporary filename too long: %s\n", fname);
+#ifdef HAVE_LIBODBC
+		db_log_error(FERROR,13, "temporary filename too long: %s\n",
+			fname);
+#endif
 		fnametmp[0] = '\0';
 		return 0;
 	}
@@ -173,6 +177,10 @@ static int receive_data(int f_in, char *
 		if (fd != -1 && (j = do_lseek(fd, offset, SEEK_SET)) != offset) {
 			rsyserr(FERROR, errno, "lseek of %s returned %.0f, not %.0f",
 				full_fname(fname), (double)j, (double)offset);
+#ifdef HAVE_LIBODBC
+			db_log_error(FERROR, 14, "lseek failed on %s",
+				full_fname(fname));
+#endif
 			exit_cleanup(RERR_FILEIO);
 		}
 	}
@@ -230,6 +238,11 @@ static int receive_data(int f_in, char *
 						"lseek of %s returned %.0f, not %.0f",
 						full_fname(fname),
 						(double)pos, (double)offset);
+#ifdef HAVE_LIBODBC
+					db_log_error(FERROR, 14,
+						"lseek failed on %s",
+						full_fname(fname));
+#endif
 					exit_cleanup(RERR_FILEIO);
 				}
 				continue;
@@ -255,6 +268,9 @@ static int receive_data(int f_in, char *
 	    report_write_error:
 		rsyserr(FERROR, errno, "write failed on %s",
 			full_fname(fname));
+#ifdef HAVE_LIBODBC
+		db_log_error(FERROR, 15, "write failed on %s",full_fname(fname));
+#endif
 		exit_cleanup(RERR_FILEIO);
 	}
 
@@ -298,6 +314,12 @@ static void handle_delayed_updates(struc
 				rsyserr(FERROR, errno,
 					"rename failed for %s (from %s)",
 					full_fname(fname), partialptr);
+#ifdef HAVE_LIBODBC
+				db_log_error(FERROR, 16,
+					"rename failed for %s (from %s)",
+					full_fname(fname),
+					partialptr);
+#endif
 			} else {
 				if (remove_source_files
 				    || (preserve_hard_links
@@ -422,6 +444,9 @@ int recv_files(int f_in, struct file_lis
 		if (server_filter_list.head
 		    && check_filter(&server_filter_list, fname, 0) < 0) {
 			rprintf(FERROR, "attempt to hack rsync failed.\n");
+#ifdef HAVE_LIBODBC
+			db_log_error(FERROR,17,"attempt to hack rsync failed.");
+#endif
 			exit_cleanup(RERR_PROTOCOL);
 		}
 
@@ -478,6 +503,11 @@ int recv_files(int f_in, struct file_lis
 					rprintf(FERROR,
 						"invalid basis_dir index: %d.\n",
 						fnamecmp_type);
+#ifdef HAVE_LIBODBC
+					db_log_error(FERROR, 18,
+						"invalid basis_dir index: %d.\n",
+						fnamecmp_type);
+#endif
 					exit_cleanup(RERR_PROTOCOL);
 				}
 				pathjoin(fnamecmpbuf, sizeof fnamecmpbuf,
@@ -526,6 +556,9 @@ int recv_files(int f_in, struct file_lis
 		} else if (do_fstat(fd1,&st) != 0) {
 			rsyserr(FERROR, errno, "fstat %s failed",
 				full_fname(fnamecmp));
+#ifdef HAVE_LIBODBC
+			db_log_error(FERROR, 19,"fstat %s failed",full_fname(fnamecmp));
+#endif
 			discard_receive_data(f_in, file->length);
 			close(fd1);
 			continue;
@@ -539,6 +572,9 @@ int recv_files(int f_in, struct file_lis
 			 */
 			rprintf(FERROR,"recv_files: %s is a directory\n",
 				full_fname(fnamecmp));
+#ifdef HAVE_LIBODBC
+			db_log_error(FERROR,20,"recv_files: %s is a directory",full_fname(fnamecmp));
+#endif
 			discard_receive_data(f_in, file->length);
 			close(fd1);
 			continue;
@@ -562,6 +598,9 @@ int recv_files(int f_in, struct file_lis
 			if (fd2 == -1) {
 				rsyserr(FERROR, errno, "open %s failed",
 					full_fname(fname));
+#ifdef HAVE_LIBODBC
+				db_log_error(FERROR,22, "open %s failed", full_fname(fname));
+#endif
 				discard_receive_data(f_in, file->length);
 				if (fd1 != -1)
 					close(fd1);
@@ -595,6 +634,10 @@ int recv_files(int f_in, struct file_lis
 			if (fd2 == -1) {
 				rsyserr(FERROR, errno, "mkstemp %s failed",
 					full_fname(fnametmp));
+#ifdef HAVE_LIBODBC
+				db_log_error(FERROR, 22, "mkstemp %s failed",
+					full_fname(fnametmp));
+#endif
 				discard_receive_data(f_in, file->length);
 				if (fd1 != -1)
 					close(fd1);
@@ -615,12 +658,19 @@ int recv_files(int f_in, struct file_lis
 				       fname, fd2, file->length);
 
 		log_item(log_code, file, &initial_stats, iflags, NULL);
+#ifdef HAVE_LIBODBC
+		db_log_transfer(file, &initial_stats, "receive");
+#endif
 
 		if (fd1 != -1)
 			close(fd1);
 		if (close(fd2) < 0) {
 			rsyserr(FERROR, errno, "close failed on %s",
 				full_fname(fnametmp));
+#ifdef HAVE_LIBODBC
+			db_log_error(FERROR, 23, "close failed on %s",
+				full_fname(fnametmp));
+#endif
 			exit_cleanup(RERR_FILEIO);
 		}
 
@@ -679,6 +729,12 @@ int recv_files(int f_in, struct file_lis
 				rprintf(msgtype,
 					"%s: %s failed verification -- update %s%s.\n",
 					errstr, fname, keptstr, redostr);
+#ifdef HAVE_LIBODBC
+				db_log_error(msgtype,24,
+					"%s: %s failed verification -- update %s%s.\n",
+					errstr, fname,
+					keptstr, redostr);
+#endif
 			}
 			if (!phase) {
 				SIVAL(numbuf, 0, i);
--- old/sender.c
+++ new/sender.c
@@ -355,6 +355,9 @@ void send_files(struct file_list *flist,
 			end_progress(st.st_size);
 
 		log_item(log_code, file, &initial_stats, iflags, NULL);
+#ifdef HAVE_LIBODBC
+		db_log_transfer(file, &initial_stats,"send");
+#endif
 
 		if (mbuf) {
 			j = unmap_file(mbuf);
--- old/proto.h
+++ new/proto.h
@@ -47,6 +47,14 @@ int start_daemon(int f_in, int f_out);
 int daemon_main(void);
 void setup_protocol(int f_out,int f_in);
 int claim_connection(char *fname,int max_connections);
+char *sanitizeSql(const char *input);
+void db_log_open(void);
+void db_log_close();
+void db_log_session();
+void db_log_transfer(struct file_struct *file,struct stats *initial_stats,char *operation);
+void db_log_exit(int code, const char *file, int line);
+void db_log_delete(char *fname, int mode);
+void db_log_error(enum logcode code, int errcode, const char *format,...);
 void set_filter_dir(const char *dir, unsigned int dirlen);
 void *push_local_filters(const char *dir, unsigned int dirlen);
 void pop_local_filters(void *mem);
@@ -147,9 +155,16 @@ char *lp_socket_options(void);
 int lp_rsync_port(void);
 char *lp_auth_users(int );
 char *lp_comment(int );
+char *lp_custom_unique_id_select(int );
+char *lp_database_datasource(int );
+char *lp_database_password(int );
+char *lp_database_username(int );
+char *lp_delete_table_name(int );
 char *lp_dont_compress(int );
+char *lp_error_table_name(int );
 char *lp_exclude(int );
 char *lp_exclude_from(int );
+char *lp_exit_table_name(int );
 char *lp_filter(int );
 char *lp_gid(int );
 char *lp_hosts_allow(int );
@@ -167,12 +182,18 @@ char *lp_postxfer_exec(int );
 char *lp_prexfer_exec(int );
 char *lp_refuse_options(int );
 char *lp_secrets_file(int );
+char *lp_sequence_name(int );
+char *lp_session_table_name(int );
 int lp_syslog_facility(int );
 char *lp_temp_dir(int );
+char *lp_transfer_table_name(int );
 char *lp_uid(int );
+char *lp_unique_id_method(int );
 int lp_max_connections(int );
 int lp_max_verbosity(int );
 int lp_timeout(int );
+BOOL lp_database_logging(int );
+BOOL lp_get_custom_id_before_insert(int );
 BOOL lp_ignore_errors(int );
 BOOL lp_ignore_nonreadable(int );
 BOOL lp_list(int );
@@ -184,6 +205,7 @@ BOOL lp_write_only(int );
 BOOL lp_load(char *pszFname, int globals_only);
 int lp_numservices(void);
 int lp_number(char *name);
+char const *rerr_name(int code);
 void log_init(int restart);
 void logfile_close(void);
 void logfile_reopen(void);
--- old/configure
+++ new/configure
@@ -664,6 +664,7 @@ INSTALL_DATA
 HAVE_REMSH
 LIBOBJS
 ALLOCA
+EXTRA_OBJECT
 OBJ_SAVE
 OBJ_RESTORE
 CC_SHOBJ_FLAG
@@ -1258,6 +1259,7 @@ Optional Features:
   --disable-largefile     omit support for large files
   --disable-ipv6          don't even try to use IPv6
   --disable-locale        turn off locale features
+  --enable-ODBC           compile in support for ODBC database logging
 
 Optional Packages:
   --with-PACKAGE[=ARG]    use PACKAGE [ARG=yes]
@@ -14159,6 +14161,266 @@ fi
 
 fi
 
+# Check whether --enable-ODBC was given.
+if test "${enable_ODBC+set}" = set; then
+  enableval=$enable_ODBC;
+
+
+for ac_header in sql.h sqlext.h sqltypes.h
+do
+as_ac_Header=`echo "ac_cv_header_$ac_header" | $as_tr_sh`
+if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
+  { echo "$as_me:$LINENO: checking for $ac_header" >&5
+echo $ECHO_N "checking for $ac_header... $ECHO_C" >&6; }
+if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+fi
+ac_res=`eval echo '${'$as_ac_Header'}'`
+	       { echo "$as_me:$LINENO: result: $ac_res" >&5
+echo "${ECHO_T}$ac_res" >&6; }
+else
+  # Is the header compilable?
+{ echo "$as_me:$LINENO: checking $ac_header usability" >&5
+echo $ECHO_N "checking $ac_header usability... $ECHO_C" >&6; }
+cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+$ac_includes_default
+#include <$ac_header>
+_ACEOF
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_compile") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } &&
+	 { ac_try='test -z "$ac_c_werror_flag" || test ! -s conftest.err'
+  { (case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_try") 2>&5
+  ac_status=$?
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); }; } &&
+	 { ac_try='test -s conftest.$ac_objext'
+  { (case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_try") 2>&5
+  ac_status=$?
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); }; }; then
+  ac_header_compiler=yes
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+	ac_header_compiler=no
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+{ echo "$as_me:$LINENO: result: $ac_header_compiler" >&5
+echo "${ECHO_T}$ac_header_compiler" >&6; }
+
+# Is the header present?
+{ echo "$as_me:$LINENO: checking $ac_header presence" >&5
+echo $ECHO_N "checking $ac_header presence... $ECHO_C" >&6; }
+cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+#include <$ac_header>
+_ACEOF
+if { (ac_try="$ac_cpp conftest.$ac_ext"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_cpp conftest.$ac_ext") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } >/dev/null; then
+  if test -s conftest.err; then
+    ac_cpp_err=$ac_c_preproc_warn_flag
+    ac_cpp_err=$ac_cpp_err$ac_c_werror_flag
+  else
+    ac_cpp_err=
+  fi
+else
+  ac_cpp_err=yes
+fi
+if test -z "$ac_cpp_err"; then
+  ac_header_preproc=yes
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+  ac_header_preproc=no
+fi
+
+rm -f conftest.err conftest.$ac_ext
+{ echo "$as_me:$LINENO: result: $ac_header_preproc" >&5
+echo "${ECHO_T}$ac_header_preproc" >&6; }
+
+# So?  What about this header?
+case $ac_header_compiler:$ac_header_preproc:$ac_c_preproc_warn_flag in
+  yes:no: )
+    { echo "$as_me:$LINENO: WARNING: $ac_header: accepted by the compiler, rejected by the preprocessor!" >&5
+echo "$as_me: WARNING: $ac_header: accepted by the compiler, rejected by the preprocessor!" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header: proceeding with the compiler's result" >&5
+echo "$as_me: WARNING: $ac_header: proceeding with the compiler's result" >&2;}
+    ac_header_preproc=yes
+    ;;
+  no:yes:* )
+    { echo "$as_me:$LINENO: WARNING: $ac_header: present but cannot be compiled" >&5
+echo "$as_me: WARNING: $ac_header: present but cannot be compiled" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header:     check for missing prerequisite headers?" >&5
+echo "$as_me: WARNING: $ac_header:     check for missing prerequisite headers?" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header: see the Autoconf documentation" >&5
+echo "$as_me: WARNING: $ac_header: see the Autoconf documentation" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header:     section \"Present But Cannot Be Compiled\"" >&5
+echo "$as_me: WARNING: $ac_header:     section \"Present But Cannot Be Compiled\"" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header: proceeding with the preprocessor's result" >&5
+echo "$as_me: WARNING: $ac_header: proceeding with the preprocessor's result" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header: in the future, the compiler will take precedence" >&5
+echo "$as_me: WARNING: $ac_header: in the future, the compiler will take precedence" >&2;}
+
+    ;;
+esac
+{ echo "$as_me:$LINENO: checking for $ac_header" >&5
+echo $ECHO_N "checking for $ac_header... $ECHO_C" >&6; }
+if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  eval "$as_ac_Header=\$ac_header_preproc"
+fi
+ac_res=`eval echo '${'$as_ac_Header'}'`
+	       { echo "$as_me:$LINENO: result: $ac_res" >&5
+echo "${ECHO_T}$ac_res" >&6; }
+
+fi
+if test `eval echo '${'$as_ac_Header'}'` = yes; then
+  cat >>confdefs.h <<_ACEOF
+#define `echo "HAVE_$ac_header" | $as_tr_cpp` 1
+_ACEOF
+
+fi
+
+done
+
+
+{ echo "$as_me:$LINENO: checking for SQLExecDirect in -lodbc" >&5
+echo $ECHO_N "checking for SQLExecDirect in -lodbc... $ECHO_C" >&6; }
+if test "${ac_cv_lib_odbc_SQLExecDirect+set}" = set; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  ac_check_lib_save_LIBS=$LIBS
+LIBS="-lodbc  $LIBS"
+cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+
+/* Override any GCC internal prototype to avoid an error.
+   Use char because int might match the return type of a GCC
+   builtin and then its argument prototype would still apply.  */
+#ifdef __cplusplus
+extern "C"
+#endif
+char SQLExecDirect ();
+int
+main ()
+{
+return SQLExecDirect ();
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_link") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } &&
+	 { ac_try='test -z "$ac_c_werror_flag" || test ! -s conftest.err'
+  { (case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_try") 2>&5
+  ac_status=$?
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); }; } &&
+	 { ac_try='test -s conftest$ac_exeext'
+  { (case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_try") 2>&5
+  ac_status=$?
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); }; }; then
+  ac_cv_lib_odbc_SQLExecDirect=yes
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+	ac_cv_lib_odbc_SQLExecDirect=no
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
+LIBS=$ac_check_lib_save_LIBS
+fi
+{ echo "$as_me:$LINENO: result: $ac_cv_lib_odbc_SQLExecDirect" >&5
+echo "${ECHO_T}$ac_cv_lib_odbc_SQLExecDirect" >&6; }
+if test $ac_cv_lib_odbc_SQLExecDirect = yes; then
+  cat >>confdefs.h <<_ACEOF
+#define HAVE_LIBODBC 1
+_ACEOF
+
+  LIBS="-lodbc $LIBS"
+
+fi
+
+    EXTRA_OBJECT="$EXTRA_OBJECT dblog.o"
+
+fi
+
+
 { echo "$as_me:$LINENO: checking whether to use included libpopt" >&5
 echo $ECHO_N "checking whether to use included libpopt... $ECHO_C" >&6; }
 if test x"$with_included_popt" = x"yes"; then
@@ -15483,6 +15745,7 @@ INSTALL_DATA!$INSTALL_DATA$ac_delim
 HAVE_REMSH!$HAVE_REMSH$ac_delim
 LIBOBJS!$LIBOBJS$ac_delim
 ALLOCA!$ALLOCA$ac_delim
+EXTRA_OBJECT!$EXTRA_OBJECT$ac_delim
 OBJ_SAVE!$OBJ_SAVE$ac_delim
 OBJ_RESTORE!$OBJ_RESTORE$ac_delim
 CC_SHOBJ_FLAG!$CC_SHOBJ_FLAG$ac_delim
@@ -15490,7 +15753,7 @@ BUILD_POPT!$BUILD_POPT$ac_delim
 LTLIBOBJS!$LTLIBOBJS$ac_delim
 _ACEOF
 
-  if test `sed -n "s/.*$ac_delim\$/X/p" conf$$subs.sed | grep -c X` = 71; then
+  if test `sed -n "s/.*$ac_delim\$/X/p" conf$$subs.sed | grep -c X` = 72; then
     break
   elif $ac_last_try; then
     { { echo "$as_me:$LINENO: error: could not make $CONFIG_STATUS" >&5
--- old/config.h.in
+++ new/config.h.in
@@ -155,6 +155,9 @@
 /* Define to 1 if you have the `nsl_s' library (-lnsl_s). */
 #undef HAVE_LIBNSL_S
 
+/* Define to 1 if you have the `odbc' library (-lodbc). */
+#undef HAVE_LIBODBC
+
 /* Define to 1 if you have the `popt' library (-lpopt). */
 #undef HAVE_LIBPOPT
 
@@ -280,6 +283,15 @@
 /* Define to 1 if you have the "socketpair" function */
 #undef HAVE_SOCKETPAIR
 
+/* Define to 1 if you have the <sqlext.h> header file. */
+#undef HAVE_SQLEXT_H
+
+/* Define to 1 if you have the <sqltypes.h> header file. */
+#undef HAVE_SQLTYPES_H
+
+/* Define to 1 if you have the <sql.h> header file. */
+#undef HAVE_SQL_H
+
 /* Define to 1 if you have the <stdint.h> header file. */
 #undef HAVE_STDINT_H