You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by sw...@apache.org on 2023/04/25 02:53:17 UTC

[logging-log4cxx] branch master updated: Add support for using SQLPrepare and bound parameters to the ODBC appender (#205)

This is an automated email from the ASF dual-hosted git repository.

swebb2066 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/logging-log4cxx.git


The following commit(s) were added to refs/heads/master by this push:
     new 5235de3d Add support for using SQLPrepare and bound parameters to the ODBC appender (#205)
5235de3d is described below

commit 5235de3d4f39e22e47a113d94052b83bc495ec74
Author: Stephen Webb <st...@ieee.org>
AuthorDate: Tue Apr 25 12:53:12 2023 +1000

    Add support for using SQLPrepare and bound parameters to the ODBC appender (#205)
    
    * Document proposed configuration
    
    * Output the timestamp with the extra precision supported by the 'datetime2' SQL type fields
    
    * Move new fields of ODBCAppenderPriv after existing fields (for ABI compatibility report)
    
    * Prevent faults when using a DSN intead of user and password on Linux
    
    ---------
    
    Co-authored-by: Stephen Webb <sw...@gmail.com>
    Co-authored-by: Robert Middleton <ro...@rm5248.com>
---
 src/main/cpp/odbcappender.cpp                      | 303 +++++++++++++++++++--
 src/main/include/log4cxx/db/odbcappender.h         |  85 +++---
 .../include/log4cxx/private/odbcappender_priv.h    |  68 ++++-
 .../input/xml/odbcAppenderDSN-Log4cxxTest.xml      |  18 +-
 4 files changed, 404 insertions(+), 70 deletions(-)

diff --git a/src/main/cpp/odbcappender.cpp b/src/main/cpp/odbcappender.cpp
index 513debf3..7a2ed5ab 100644
--- a/src/main/cpp/odbcappender.cpp
+++ b/src/main/cpp/odbcappender.cpp
@@ -21,7 +21,22 @@
 #include <log4cxx/helpers/transcoder.h>
 #include <log4cxx/patternlayout.h>
 #include <apr_strings.h>
-#include <log4cxx/private/odbcappender_priv.h>
+#include <apr_time.h>
+#include <cmath> // std::pow
+
+#include <log4cxx/pattern/loggerpatternconverter.h>
+#include <log4cxx/pattern/classnamepatternconverter.h>
+#include <log4cxx/pattern/datepatternconverter.h>
+#include <log4cxx/pattern/filelocationpatternconverter.h>
+#include <log4cxx/pattern/fulllocationpatternconverter.h>
+#include <log4cxx/pattern/shortfilelocationpatternconverter.h>
+#include <log4cxx/pattern/linelocationpatternconverter.h>
+#include <log4cxx/pattern/messagepatternconverter.h>
+#include <log4cxx/pattern/methodlocationpatternconverter.h>
+#include <log4cxx/pattern/levelpatternconverter.h>
+#include <log4cxx/pattern/threadpatternconverter.h>
+#include <log4cxx/pattern/threadusernamepatternconverter.h>
+#include <log4cxx/pattern/ndcpatternconverter.h>
 
 #if !defined(LOG4CXX)
 	#define LOG4CXX 1
@@ -32,13 +47,22 @@
 		#include <windows.h>
 	#endif
 	#include <sqlext.h>
+#else
+	typedef void* SQLHSTMT;
 #endif
+#include <log4cxx/private/odbcappender_priv.h>
+#if defined(min)
+	#undef min
+#endif
+#include <cstring>
+#include <algorithm>
 
 
 using namespace log4cxx;
 using namespace log4cxx::helpers;
 using namespace log4cxx::db;
 using namespace log4cxx::spi;
+using namespace log4cxx::pattern;
 
 SQLException::SQLException(short fHandleType,
 	void* hInput, const char* prolog,
@@ -103,6 +127,31 @@ ODBCAppender::~ODBCAppender()
 	finalize();
 }
 
+#define RULES_PUT(spec, cls) \
+	specs.insert(PatternMap::value_type(LogString(LOG4CXX_STR(spec)), cls ::newInstance))
+
+static PatternMap getFormatSpecifiers()
+{
+	PatternMap specs;
+	if (specs.empty())
+	{
+		RULES_PUT("logger", LoggerPatternConverter);
+		RULES_PUT("class", ClassNamePatternConverter);
+		RULES_PUT("time", DatePatternConverter);
+		RULES_PUT("shortfilename", ShortFileLocationPatternConverter);
+		RULES_PUT("fullfilename", FileLocationPatternConverter);
+		RULES_PUT("location", FullLocationPatternConverter);
+		RULES_PUT("line", LineLocationPatternConverter);
+		RULES_PUT("message", MessagePatternConverter);
+		RULES_PUT("method", MethodLocationPatternConverter);
+		RULES_PUT("level", LevelPatternConverter);
+		RULES_PUT("thread", ThreadPatternConverter);
+		RULES_PUT("threadname", ThreadUsernamePatternConverter);
+		RULES_PUT("ndc", NDCPatternConverter);
+	}
+	return specs;
+}
+
 void ODBCAppender::setOption(const LogString& option, const LogString& value)
 {
 	if (StringHelper::equalsIgnoreCase(option, LOG4CXX_STR("BUFFERSIZE"), LOG4CXX_STR("buffersize")))
@@ -127,16 +176,44 @@ void ODBCAppender::setOption(const LogString& option, const LogString& value)
 	{
 		setUser(value);
 	}
+	else if (StringHelper::equalsIgnoreCase(option, LOG4CXX_STR("COLUMNMAPPING"), LOG4CXX_STR("columnmapping")))
+	{
+		_priv->mappedName.push_back(value);
+	}
 	else
 	{
 		AppenderSkeleton::setOption(option, value);
 	}
 }
 
+//* Does ODBCAppender require a layout?
+
+bool ODBCAppender::requiresLayout() const
+{
+	return _priv->parameterValue.empty();
+}
+
 void ODBCAppender::activateOptions(log4cxx::helpers::Pool&)
 {
 #if !LOG4CXX_HAVE_ODBC
 	LogLog::error(LOG4CXX_STR("Can not activate ODBCAppender unless compiled with ODBC support."));
+#else
+	auto specs = getFormatSpecifiers();
+	for (auto& name : _priv->mappedName)
+	{
+		auto pItem = specs.find(StringHelper::toLowerCase(name));
+		if (specs.end() == pItem)
+			LogLog::error(name + LOG4CXX_STR(" is not a supported ColumnMapping value"));
+		else
+		{
+			ODBCAppenderPriv::DataBinding paramData{ 0, 0, 0, 0, 0 };
+			std::vector<LogString> options;
+			if (LOG4CXX_STR("time") == pItem->first)
+				options.push_back(LOG4CXX_STR("yyyy-MM-dd HH:mm:ss.SSSSSS"));
+			paramData.converter = log4cxx::cast<LoggingEventPatternConverter>((pItem->second)(options));
+			_priv->parameterValue.push_back(paramData);
+		}
+	}
 #endif
 }
 
@@ -176,16 +253,21 @@ void ODBCAppender::execute(const LogString& sql, log4cxx::helpers::Pool& p)
 
 		if (ret < 0)
 		{
-			throw SQLException( SQL_HANDLE_DBC, con, "Failed to allocate sql handle.", p);
+			throw SQLException( SQL_HANDLE_DBC, con, "Failed to allocate sql handle", p);
 		}
 
+#if LOG4CXX_LOGCHAR_IS_WCHAR
+		ret = SQLExecDirectW(stmt, (SQLWCHAR*)sql.c_str(), SQL_NTS);
+#elif LOG4CXX_LOGCHAR_IS_UTF8
+		ret = SQLExecDirectA(stmt, (SQLCHAR*)sql.c_str(), SQL_NTS);
+#else
 		SQLWCHAR* wsql;
 		encode(&wsql, sql, p);
 		ret = SQLExecDirectW(stmt, wsql, SQL_NTS);
-
+#endif
 		if (ret < 0)
 		{
-			throw SQLException(SQL_HANDLE_STMT, stmt, "Failed to execute sql statement.", p);
+			throw SQLException(SQL_HANDLE_STMT, stmt, "Failed to execute sql statement", p);
 		}
 	}
 	catch (SQLException&)
@@ -211,10 +293,6 @@ void ODBCAppender::closeConnection(ODBCAppender::SQLHDBC /* con */)
 {
 }
 
-
-
-
-
 ODBCAppender::SQLHDBC ODBCAppender::getConnection(log4cxx::helpers::Pool& p)
 {
 #if LOG4CXX_HAVE_ODBC
@@ -226,7 +304,7 @@ ODBCAppender::SQLHDBC ODBCAppender::getConnection(log4cxx::helpers::Pool& p)
 
 		if (ret < 0)
 		{
-			SQLException ex(SQL_HANDLE_ENV, _priv->env, "Failed to allocate SQL handle.", p);
+			SQLException ex(SQL_HANDLE_ENV, _priv->env, "Failed to allocate SQL handle", p);
 			_priv->env = SQL_NULL_HENV;
 			throw ex;
 		}
@@ -235,7 +313,7 @@ ODBCAppender::SQLHDBC ODBCAppender::getConnection(log4cxx::helpers::Pool& p)
 
 		if (ret < 0)
 		{
-			SQLException ex(SQL_HANDLE_ENV, _priv->env, "Failed to set odbc version.", p);
+			SQLException ex(SQL_HANDLE_ENV, _priv->env, "Failed to set odbc version", p);
 			SQLFreeHandle(SQL_HANDLE_ENV, _priv->env);
 			_priv->env = SQL_NULL_HENV;
 			throw ex;
@@ -248,16 +326,18 @@ ODBCAppender::SQLHDBC ODBCAppender::getConnection(log4cxx::helpers::Pool& p)
 
 		if (ret < 0)
 		{
-			SQLException ex(SQL_HANDLE_DBC, _priv->connection, "Failed to allocate sql handle.", p);
+			SQLException ex(SQL_HANDLE_DBC, _priv->connection, "Failed to allocate sql handle", p);
 			_priv->connection = SQL_NULL_HDBC;
 			throw ex;
 		}
 
 
-		SQLWCHAR* wURL, *wUser, *wPwd;
+		SQLWCHAR* wURL, *wUser = nullptr, *wPwd = nullptr;
 		encode(&wURL, _priv->databaseURL, p);
-		encode(&wUser, _priv->databaseUser, p);
-		encode(&wPwd, _priv->databasePassword, p);
+		if (!_priv->databaseUser.empty())
+			encode(&wUser, _priv->databaseUser, p);
+		if (!_priv->databasePassword.empty())
+			encode(&wPwd, _priv->databasePassword, p);
 
 		ret = SQLConnectW( _priv->connection,
 				wURL, SQL_NTS,
@@ -267,7 +347,7 @@ ODBCAppender::SQLHDBC ODBCAppender::getConnection(log4cxx::helpers::Pool& p)
 
 		if (ret < 0)
 		{
-			SQLException ex(SQL_HANDLE_DBC, _priv->connection, "Failed to connect to database.", p);
+			SQLException ex(SQL_HANDLE_DBC, _priv->connection, "Failed to connect to database", p);
 			SQLFreeHandle(SQL_HANDLE_DBC, _priv->connection);
 			_priv->connection = SQL_NULL_HDBC;
 			throw ex;
@@ -316,14 +396,203 @@ void ODBCAppender::close()
 	_priv->closed = true;
 }
 
+#if LOG4CXX_HAVE_ODBC
+void ODBCAppender::ODBCAppenderPriv::setPreparedStatement(SQLHDBC con, Pool& p)
+{
+	auto ret = SQLAllocHandle( SQL_HANDLE_STMT, con, &this->preparedStatement);
+	if (ret < 0)
+	{
+		throw SQLException( SQL_HANDLE_DBC, con, "Failed to allocate statement handle.", p);
+	}
+
+#if LOG4CXX_LOGCHAR_IS_WCHAR
+	ret = SQLPrepareW(this->preparedStatement, (SQLWCHAR*)this->sqlStatement.c_str(), SQL_NTS);
+#elif LOG4CXX_LOGCHAR_IS_UTF8
+	ret = SQLPrepareA(this->preparedStatement, (SQLCHAR*)this->sqlStatement.c_str(), SQL_NTS);
+#else
+	SQLWCHAR* wsql;
+	encode(&wsql, this->sqlStatement, p);
+	ret = SQLPrepareW(this->preparedStatement, wsql, SQL_NTS);
+#endif
+	if (ret < 0)
+	{
+		throw SQLException(SQL_HANDLE_STMT, this->preparedStatement, "Failed to prepare sql statement.", p);
+	}
+
+	int parameterNumber = 0;
+	for (auto& item : this->parameterValue)
+	{
+		++parameterNumber;
+		SQLSMALLINT  targetType;
+		SQLULEN      targetMaxCharCount;
+		SQLSMALLINT  decimalDigits;
+		SQLSMALLINT  nullable;
+		auto ret = SQLDescribeParam
+			( this->preparedStatement
+			, parameterNumber
+			, &targetType
+			, &targetMaxCharCount
+			, &decimalDigits
+			, &nullable
+			);
+		if (ret < 0)
+		{
+			throw SQLException(SQL_HANDLE_STMT, this->preparedStatement, "Failed to describe parameter", p);
+		}
+		if (SQL_CHAR == targetType || SQL_VARCHAR == targetType || SQL_LONGVARCHAR == targetType)
+		{
+			item.paramType = SQL_C_CHAR;
+			item.paramMaxCharCount = targetMaxCharCount;
+			item.paramValueSize = (SQLINTEGER)(item.paramMaxCharCount) * sizeof(char) + sizeof(char);
+			item.paramValue = (SQLPOINTER)p.palloc(item.paramValueSize + sizeof(char));
+		}
+		else if (SQL_WCHAR == targetType || SQL_WVARCHAR == targetType || SQL_WLONGVARCHAR == targetType)
+		{
+			item.paramType = SQL_C_WCHAR;
+			item.paramMaxCharCount = targetMaxCharCount;
+			item.paramValueSize = (SQLINTEGER)(targetMaxCharCount) * sizeof(wchar_t) + sizeof(wchar_t);
+			item.paramValue = (SQLPOINTER)p.palloc(item.paramValueSize + sizeof(wchar_t));
+		}
+		else if (SQL_TYPE_TIMESTAMP == targetType || SQL_TYPE_DATE == targetType || SQL_TYPE_TIME == targetType
+			|| SQL_DATETIME == targetType)
+		{
+			item.paramType = SQL_C_TYPE_TIMESTAMP;
+			item.paramMaxCharCount = decimalDigits;
+			item.paramValueSize = sizeof(SQL_TIMESTAMP_STRUCT);
+			item.paramValue = (SQLPOINTER)p.palloc(item.paramValueSize);
+		}
+		else
+		{
+			if (SQL_INTEGER != targetType)
+			{
+				LogString msg(LOG4CXX_STR("Unexpected targetType ("));
+				helpers::StringHelper::toString(targetType, p, msg);
+				msg += LOG4CXX_STR(") at parameter ");
+				helpers::StringHelper::toString(parameterNumber, p, msg);
+				msg += LOG4CXX_STR(" while preparing SQL");
+				LogLog::warn(msg);
+			}
+			item.paramMaxCharCount = 30;
+#if LOG4CXX_LOGCHAR_IS_UTF8
+			item.paramType = SQL_C_CHAR;
+			item.paramValueSize = (SQLINTEGER)(item.paramMaxCharCount) * sizeof(char);
+			item.paramValue = (SQLPOINTER)p.palloc(item.paramValueSize + sizeof(char));
+#else
+			item.paramType = SQL_C_WCHAR;
+			item.paramValueSize = (SQLINTEGER)(item.paramMaxCharCount) * sizeof(wchar_t);
+			item.paramValue = (SQLPOINTER)p.palloc(item.paramValueSize + sizeof(wchar_t));
+#endif
+		}
+		item.strLen_or_Ind = SQL_NTS;
+		ret = SQLBindParameter
+			( this->preparedStatement
+			, parameterNumber
+			, SQL_PARAM_INPUT
+			, item.paramType  // ValueType
+			, targetType
+			, targetMaxCharCount
+			, decimalDigits
+			, item.paramValue
+			, item.paramValueSize
+			, &item.strLen_or_Ind
+			);
+		if (ret < 0)
+		{
+			throw SQLException(SQL_HANDLE_STMT, this->preparedStatement, "Failed to bind parameter", p);
+		}
+	}
+}
+
+void ODBCAppender::ODBCAppenderPriv::setParameterValues(const spi::LoggingEventPtr& event, Pool& p)
+{
+	for (auto& item : this->parameterValue)
+	{
+		if (!item.paramValue || item.paramValueSize <= 0)
+			;
+		else if (SQL_C_WCHAR == item.paramType)
+		{
+			LogString sbuf;
+			item.converter->format(event, sbuf, p);
+#if LOG4CXX_LOGCHAR_IS_WCHAR_T
+			std::wstring& tmp = sbuf;
+#else
+			std::wstring tmp;
+			Transcoder::encode(sbuf, tmp);
+#endif
+			auto dst = (wchar_t*)item.paramValue;
+			auto charCount = std::min(size_t(item.paramMaxCharCount), tmp.size());
+			auto copySize = std::min(size_t(item.paramValueSize - 1), charCount * sizeof(wchar_t));
+			std::memcpy(dst, tmp.data(), copySize);
+			dst[copySize / sizeof(wchar_t)] = 0;
+		}
+		else if (SQL_C_CHAR == item.paramType)
+		{
+			LogString sbuf;
+			item.converter->format(event, sbuf, p);
+#if LOG4CXX_LOGCHAR_IS_UTF8
+			std::string& tmp = sbuf;
+#else
+			std::string tmp;
+			Transcoder::encode(sbuf, tmp);
+#endif
+			auto dst = (char*)item.paramValue;
+			auto sz = std::min(size_t(item.paramMaxCharCount), tmp.size());
+			auto copySize = std::min(size_t(item.paramValueSize - 1), sz * sizeof(char));
+			std::memcpy(dst, tmp.data(), copySize);
+			dst[copySize] = 0;
+		}
+		else if (SQL_C_TYPE_TIMESTAMP == item.paramType)
+		{
+			apr_time_exp_t exploded;
+			apr_status_t stat = this->timeZone->explode(&exploded, event->getTimeStamp());
+			if (stat == APR_SUCCESS)
+			{
+				auto dst = (SQL_TIMESTAMP_STRUCT*)item.paramValue;
+				dst->year = 1900 + exploded.tm_year;
+				dst->month = 1 + exploded.tm_mon;
+				dst->day = exploded.tm_mday;
+				dst->hour = exploded.tm_hour;
+				dst->minute = exploded.tm_min;
+				dst->second = exploded.tm_sec;
+				// Prevent '[ODBC SQL Server Driver]Datetime field overflow' by rounding to the target field precision
+				int roundingExponent = 6 - (int)item.paramMaxCharCount;
+				if (0 < roundingExponent)
+				{
+					int roundingDivisor = (int)std::pow(10, roundingExponent);
+					dst->fraction = 1000 * roundingDivisor * ((exploded.tm_usec + roundingDivisor / 2) / roundingDivisor);
+				}
+				else
+					dst->fraction = 1000 * exploded.tm_usec;
+			}
+		}
+	}
+}
+#endif
+
 void ODBCAppender::flushBuffer(Pool& p)
 {
 	for (auto& logEvent : _priv->buffer)
 	{
 		try
 		{
-			auto sql = getLogStatement(logEvent, p);
-			execute(sql, p);
+			if (!_priv->parameterValue.empty())
+			{
+#if LOG4CXX_HAVE_ODBC
+				if (0 == _priv->preparedStatement)
+					_priv->setPreparedStatement(getConnection(p), p);
+				_priv->setParameterValues(logEvent, p);
+				auto ret = SQLExecute(_priv->preparedStatement);
+				if (ret < 0)
+				{
+					throw SQLException(SQL_HANDLE_STMT, _priv->preparedStatement, "Failed to execute prepared statement", p);
+				}
+#endif
+			}
+			else
+			{
+				auto sql = getLogStatement(logEvent, p);
+				execute(sql, p);
+			}
 		}
 		catch (SQLException& e)
 		{
diff --git a/src/main/include/log4cxx/db/odbcappender.h b/src/main/include/log4cxx/db/odbcappender.h
index aa85aaf6..60db2ccb 100644
--- a/src/main/include/log4cxx/db/odbcappender.h
+++ b/src/main/include/log4cxx/db/odbcappender.h
@@ -47,15 +47,25 @@ class LOG4CXX_EXPORT SQLException : public log4cxx::helpers::Exception
 /**
 The ODBCAppender sends log events to a database.
 
-<p>Each append call adds to an <code>ArrayList</code> buffer.  When
-the buffer is filled each log event is placed in a sql statement
-(which is configured in the <b>sql</b> element or the attached PatternLayout) and executed.
-
-The SQL insert statement pattern must be provided.
-The SQL statement can be specified in the Log4cxx configuration file
-either using the <b>sql</b> parameter element
-or by attaching a PatternLayout layout element.
-  
+<p>Each append call adds the spi::LoggingEvent to a buffer.
+When the buffer is full, values are extracted from each spi::LoggingEvent
+and the sql insert statement executed.
+
+The SQL insert statement pattern must be provided
+either in the Log4cxx configuration file
+using the <b>sql</b> parameter element
+or programatically by calling the <code>setSql(String sql)</code> method.
+
+If no <b>ColumnMapping</b> element is provided in the configuration file
+the sql statement is assumed to be a PatternLayout layout.
+In this case all the conversion patterns in PatternLayout
+can be used inside of the statement. (see the test cases for examples)
+
+If the <b>sql</b> element is not provided
+and no <b>ColumnMapping</b> element is provided
+the attached a PatternLayout layout element
+is assumed to contain the sql statement.
+
 The following <b>param</b> elements are optional:
 - one of <b>DSN</b>, <b>URL</b>, <b>ConnectionString</b> -
   The <b>serverName</b> parameter value in the <a href="https://learn.microsoft.com/en-us/sql/odbc/reference/syntax/sqlconnect-function">SQLConnect</a> call.
@@ -67,16 +77,22 @@ The following <b>param</b> elements are optional:
   Delay executing the sql until this many logging events are available.
   One by default, meaning an sql statement is executed
   whenever a logging event is appended.
-
-<p>The <code>setSql(String sql)</code> sets the SQL statement to be
-used for logging -- this statement is sent to a
-<code>PatternLayout</code> (either created automaticly by the
-appender or added by the user).  Therefore by default all the
-conversion patterns in <code>PatternLayout</code> can be used
-inside of the statement.  (see the test cases for examples)
-
-<p>Overriding the {@link #getLogStatement} method allows more
-explicit control of the statement used for logging.
+- <b>ColumnMapping</b> -
+  One element for each "?" in the <b>sql</b> statement
+  in a sequence corresponding to the columns in the insert statement.
+  The following values are supported:
+  - logger
+  - level
+  - thread
+  - threadname
+  - time
+  - shortfilename
+  - fullfilename
+  - line
+  - class
+  - method
+  - message
+  - ndc
 
 <p>For use as a base class:
 
@@ -92,18 +108,21 @@ you override getConnection make sure to implement
 generated.  Typically this would return the connection to the
 pool it came from.
 
-<li>Override getLogStatement to
-produce specialized or dynamic statements. The default uses the
-sql option value.
-
 </ul>
 
 An example configuration that writes to the data source named "LoggingDSN" is:
 ~~~{.xml}
 <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
-appender name="SqlAppender" class="ODBCAppender">
+<appender name="SqlAppender" class="ODBCAppender">
  <param name="DSN" value="LoggingDSN"/>
- <param name="sql" value="INSERT INTO [SomeDatabaseName].[SomeUserName].[SomeTableName] ([Thread],[LogName],[LogTime],[LogLevel],[FileName],[FileLine],[Message]) VALUES ('%t', '%c','%d{dd MMM yyyy HH:mm:ss.SSS}','%p','%f','%L','%m{'}')" />
+ <param name="sql" value="INSERT INTO [SomeDatabaseName].[SomeUserName].[SomeTableName] ([Thread],[LogName],[LogTime],[LogLevel],[FileName],[FileLine],[Message]) VALUES (?,?,?,?,?,?,?)" />
+ <param name="ColumnMapping" value="thread"/>
+ <param name="ColumnMapping" value="logger"/>
+ <param name="ColumnMapping" value="time"/>
+ <param name="ColumnMapping" value="level"/>
+ <param name="ColumnMapping" value="shortfilename"/>
+ <param name="ColumnMapping" value="line"/>
+ <param name="ColumnMapping" value="message"/>
 </appender>
 <appender name="ASYNC" class="AsyncAppender">
   <param name="BufferSize" value="1000"/>
@@ -150,21 +169,18 @@ class LOG4CXX_EXPORT ODBCAppender : public AppenderSkeleton
 		*/
 		void append(const spi::LoggingEventPtr& event, helpers::Pool&) override;
 
+	protected:
 		/**
-		* By default getLogStatement sends the event to the required Layout object.
+		* Sends the event to the attached PatternLayout object.
 		* The layout will format the given pattern into a workable SQL string.
 		*
-		* Overriding this provides direct access to the LoggingEvent
-		* when constructing the logging statement.
-		*
 		*/
-	protected:
 		LogString getLogStatement(const spi::LoggingEventPtr& event,
 			helpers::Pool& p) const;
 
 		/**
 		*
-		* Override this to provide an alertnate method of getting
+		* Override this to provide an alternate method of getting
 		* connections (such as caching).  One method to fix this is to open
 		* connections at the start of flushBuffer() and close them at the
 		* end.  I use a connection pool outside of ODBCAppender which is
@@ -207,12 +223,9 @@ class LOG4CXX_EXPORT ODBCAppender : public AppenderSkeleton
 		virtual void flushBuffer(log4cxx::helpers::Pool& p);
 
 		/**
-		* ODBCAppender requires a layout.
+		* Does this appender require a layout?
 		* */
-		bool requiresLayout() const override
-		{
-			return true;
-		}
+		bool requiresLayout() const override;
 
 		/**
 		* Set pre-formated statement eg: insert into LogTable (msg) values ("%m")
diff --git a/src/main/include/log4cxx/private/odbcappender_priv.h b/src/main/include/log4cxx/private/odbcappender_priv.h
index 4a78ae55..de519582 100644
--- a/src/main/include/log4cxx/private/odbcappender_priv.h
+++ b/src/main/include/log4cxx/private/odbcappender_priv.h
@@ -19,9 +19,26 @@
 #define LOG4CXX_ODBCAPPENDER_PRIV
 
 #include <log4cxx/db/odbcappender.h>
+#include <log4cxx/pattern/loggingeventpatternconverter.h>
 #include "appenderskeleton_priv.h"
 
-#include <list>
+#if !defined(LOG4CXX)
+	#define LOG4CXX 1
+#endif
+#include <log4cxx/private/log4cxx_private.h>
+#if LOG4CXX_HAVE_ODBC
+	#if defined(WIN32) || defined(_WIN32)
+		#include <windows.h>
+	#endif
+	#include <sqlext.h>
+#else
+	typedef void* SQLHSTMT;
+	typedef void* SQLPOINTER;
+	typedef uint64_t SQLULEN;
+	typedef int64_t SQLLEN;
+	typedef long SQLINTEGER;
+	typedef short SQLSMALLINT;
+#endif
 
 namespace log4cxx
 {
@@ -30,11 +47,14 @@ namespace db
 
 struct ODBCAppender::ODBCAppenderPriv : public AppenderSkeleton::AppenderSkeletonPrivate
 {
-	ODBCAppenderPriv() :
-		AppenderSkeletonPrivate(),
-		connection(nullptr),
-		env(nullptr),
-		bufferSize(1) {}
+	ODBCAppenderPriv()
+		: AppenderSkeletonPrivate()
+		, connection(0)
+		, env(0)
+		, preparedStatement(0)
+		, bufferSize(1)
+		, timeZone(helpers::TimeZone::getDefault())
+		{}
 
 	/**
 	* URL of the DB for default connection handling
@@ -58,20 +78,13 @@ struct ODBCAppender::ODBCAppenderPriv : public AppenderSkeleton::AppenderSkeleto
 	* sub-class and overriding the <code>getConnection</code> and
 	* <code>closeConnection</code> methods.
 	*/
-	log4cxx::db::ODBCAppender::SQLHDBC connection;
-	log4cxx::db::ODBCAppender::SQLHENV env;
+	SQLHDBC connection;
+	SQLHENV env;
 
 	/**
 	* Stores the string given to the pattern layout for conversion into a SQL
-	* statement, eg: insert into LogTable (Thread, File, Message) values
-	* ("%t", "%F", "%m")
-	*
-	* Be careful of quotes in your messages!
-	*
-	* Also see PatternLayout.
 	*/
 	LogString sqlStatement;
-
 	/**
 	* size of LoggingEvent buffer before writing to the database.
 	* Default is 1.
@@ -82,6 +95,31 @@ struct ODBCAppender::ODBCAppenderPriv : public AppenderSkeleton::AppenderSkeleto
 	* ArrayList holding the buffer of Logging Events.
 	*/
 	std::vector<spi::LoggingEventPtr> buffer;
+
+	/** Provides timestamp components
+	*/
+	helpers::TimeZonePtr timeZone;
+
+	/**
+	* The prepared statement handle and the bound column names, converters and buffers
+	*/
+	SQLHSTMT preparedStatement;
+	struct DataBinding
+	{
+		using ConverterPtr = pattern::LoggingEventPatternConverterPtr;
+		ConverterPtr converter;
+		SQLSMALLINT  paramType;
+		SQLULEN      paramMaxCharCount;
+		SQLPOINTER   paramValue;
+		SQLINTEGER   paramValueSize;
+		SQLLEN       strLen_or_Ind;
+	};
+	std::vector<LogString>   mappedName;
+	std::vector<DataBinding> parameterValue;
+#if LOG4CXX_HAVE_ODBC
+	void setPreparedStatement(SQLHDBC con, helpers::Pool& p);
+	void setParameterValues(const spi::LoggingEventPtr& event, helpers::Pool& p);
+#endif
 };
 
 }
diff --git a/src/test/resources/input/xml/odbcAppenderDSN-Log4cxxTest.xml b/src/test/resources/input/xml/odbcAppenderDSN-Log4cxxTest.xml
index ff5bcbe2..8873ab40 100644
--- a/src/test/resources/input/xml/odbcAppenderDSN-Log4cxxTest.xml
+++ b/src/test/resources/input/xml/odbcAppenderDSN-Log4cxxTest.xml
@@ -25,16 +25,30 @@
     <param name="ConversionPattern" value="%d %-5p %c{2} - %m%n"/>
   </layout>
 </appender>
-<appender name="SqlAppender" class="ODBCAppender">
+<appender name="DirectAppender" class="ODBCAppender">
  <param name="DSN" value="Log4cxxTest"/>
  <param name="sql" value="INSERT INTO [ApplicationLogs].[dbo].[UnitTestLog] ([Thread],[LogName],[LogTime],[LogLevel],[FileName],[FileLine],[Message]) VALUES ('%t', '%c','%d{dd MMM yyyy HH:mm:ss.SSS}','%p','%f','%L','%m{'}')" />
  <!--param name="User" value="dbo"/-->
  <!--param name="Password" value="myLog4cxxTestPassword"/-->
 </appender>
+<appender name="PreparedAppender" class="ODBCAppender">
+ <param name="DSN" value="Log4cxxTest"/>
+ <param name="sql" value="INSERT INTO [ApplicationLogs].[dbo].[UnitTestLog] ([Thread],[LogName],[LogTime],[LogLevel],[FileName],[FileLine],[Message]) VALUES (?,?,?,?,?,?,?)" />
+ <param name="ColumnMapping" value="thread"/>
+ <param name="ColumnMapping" value="logger"/>
+ <param name="ColumnMapping" value="time"/>
+ <param name="ColumnMapping" value="level"/>
+ <param name="ColumnMapping" value="shortfilename"/>
+ <param name="ColumnMapping" value="line"/>
+ <param name="ColumnMapping" value="message"/>
+ <!--param name="User" value="dbo"/-->
+ <!--param name="User" value="dbo"/-->
+ <!--param name="Password" value="myLog4cxxTestPassword"/-->
+</appender>
 <appender name="ASYNC" class="AsyncAppender">
   <param name="BufferSize" value="1000"/>
   <param name="Blocking" value="false"/>
-  <appender-ref ref="SqlAppender"/>
+  <appender-ref ref="PreparedAppender"/>
 </appender>
 <logger name="DB">
   <level value="INFO" />