From e51bed25201e89b2fed36e45e1642812d88f6398 Mon Sep 17 00:00:00 2001
From: znone <glyc@sina.com.cn>
Date: Tue, 07 Mar 2017 11:06:28 +0000
Subject: [PATCH] 增加对ODBC的支持。

---
 include/qtl_database_pool.hpp      |    7 
 include/qtl_sqlite_pool.hpp        |   11 
 test/TestOdbc.cpp                  |  271 ++++++++
 include/qtl_common.hpp             |    6 
 test/stdafx.h                      |    4 
 test/TestOdbc.h                    |   39 +
 README.md                          |   61 +
 include/qtl_sqlite.hpp             |   62 -
 test/test_odbc.mak                 |   25 
 test/vs/test_odbc/test_odbc.vcproj |  259 ++++++++
 include/qtl_mysql_pool.hpp         |   12 
 include/qtl_odbc.hpp               | 1047 ++++++++++++++++++++++++++++++++
 include/qtl_mysql.hpp              |    5 
 test/vs/test.sln                   |    6 
 include/qtl_odbc_pool.hpp          |   46 +
 15 files changed, 1,807 insertions(+), 54 deletions(-)

diff --git a/README.md b/README.md
index e19e54c..b519a03 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
 # QTL
-QTL是一个访问SQL数据库的C++库,目前支持MySQL和SQLite。QTL是一个轻量级的库,只由头文件组成,不需要单独编译安装。QTL是对数据库原生客户端接口的薄封装,能提供友好使用方式的同时拥有接近于使用原生接口的性能。
+QTL是一个访问SQL数据库的C++库,目前支持MySQL、SQLite和ODBC。QTL是一个轻量级的库,只由头文件组成,不需要单独编译安装。QTL是对数据库原生客户端接口的薄封装,能提供友好使用方式的同时拥有接近于使用原生接口的性能。
 使用QTL需要支持C++11的编译器。
 
 ## 使用方式
@@ -189,6 +189,65 @@
 - qtl::sqlite::query_result
 表示一个SQLite的查询结果集,用于以迭代器方式遍历查询结果。
 
+## 有关ODBC的说明
+
+通过ODBC访问数据库时,包含头文件qtl_odbc.hpp。
+QTL不支持ODBC的输出参数。
+
+### ODBC的参数数据绑定
+
+| 参数类型 | C++类型 |
+| ------- | ------ |
+| TINYINT | int8_t<br>uint8_t |
+| SMALLINT | int16_t<br>uint16_t |
+| INTEGER | int32_t<br>uint32_t |
+| BIGINT | int64_t<br>uint64_t |
+| FLOAT | float |
+| DOUBLE | double |
+| NUMERIC | SQL_NUMERIC_STRUCT |
+| BIT | bool |
+| CHAR<br>VARCHAR | const char*<br>std::string |
+| WCHAR<br>WVARCHAR | const wchar_t*<br>std::wstring |
+| BINARY | qtl::const_blob_data |
+| LONGVARBINARY | std::istream |
+| DATE | qtl::odbc::date |
+| TIME<br>UTCTIME | qtl::odbc::time |
+| TIMESTAMP<br>UTCDATETIME | qtl::odbc::datetime |
+| GUID | SQLGUID |
+
+### ODBC的字段数据绑定
+
+| 字段类型 | C++类型 |
+| ------- | ------ |
+| TINYINT | int8_t<br>uint8_t |
+| SMALLINT | int16_t<br>uint16_t |
+| INTEGER | int32_t<br>uint32_t |
+| BIGINT | int64_t<br>uint64_t |
+| FLOAT | float |
+| DOUBLE | double |
+| NUMERIC | SQL_NUMERIC_STRUCT |
+| BIT | bool |
+| CHAR<br>VARCHAR | char[N]<br>std::array&lt;char, N&gt;<br>std::string |
+| WCHAR<br>WVARCHAR | wchar_t[N]<br>std::array&lt;wchar_t, N&gt;<br>std::string |
+| BINARY | qtl::blob_data |
+| LONGVARBINARY | std::ostream |
+| DATE | qtl::odbc::date |
+| TIME<br>UTCTIME | qtl::odbc::time |
+| TIMESTAMP<br>UTCDATETIME | qtl::odbc::datetime |
+| GUID | SQLGUID |
+
+### ODBC相关的C++类
+- qtl::odbc::database
+表示一个ODBC的数据库连接,程序主要通过这个类操纵数据库。
+- qtl::odbc::statement
+表示一个ODBC的查询语句,实现查询相关操作。
+- qtl::odbc::error
+表示一个ODBC的错误,当操作出错时,抛出该类型的异常,包含错误信息。
+- qtl::odbc::transaction
+表示一个ODBC的事务操作。
+- qtl::odbc::query_result
+表示一个ODBC的查询结果集,用于以迭代器方式遍历查询结果。
+
 ## 关于测试
 
 编译测试用例的第三方库需要另外下载。除了数据库相关的库外,测试用例用到了测试框架[CppTest](https://sourceforge.net/projects/cpptest/ "CppTest")。
diff --git a/include/qtl_common.hpp b/include/qtl_common.hpp
index 847a6cd..af9f409 100644
--- a/include/qtl_common.hpp
+++ b/include/qtl_common.hpp
@@ -463,12 +463,6 @@
 	binder(command, std::forward<T>(value));
 }
 
-template<typename Command, typename T>
-inline void bind_record(Command& command, T& value)
-{
-	bind_record(command, std::forward<T>(value));
-}
-
 template<typename Command, typename Record>
 class query_iterator final : public std::iterator<std::forward_iterator_tag, Record>
 {
diff --git a/include/qtl_database_pool.hpp b/include/qtl_database_pool.hpp
index 68e31c6..1d04e2f 100644
--- a/include/qtl_database_pool.hpp
+++ b/include/qtl_database_pool.hpp
@@ -79,7 +79,7 @@
 	std::thread m_background_thread;
 	bool m_stop_thread;
 
-	virtual bool open_database(Database& db)=0;
+	virtual Database* new_database() throw()=0;
 	void recovery(Database* db)
 	{
 		if(db==NULL) return;
@@ -101,9 +101,8 @@
 
 	Database* create_database()
 	{
-		Database* db=new Database;
-		if(open_database(*db))
-			return db;
+		Database* db=new_database();
+		if(db) return db;
 
 		{
 			std::lock_guard<std::mutex> lock(m_pool_mutex);
diff --git a/include/qtl_mysql.hpp b/include/qtl_mysql.hpp
index 60c552b..b0cb96b 100644
--- a/include/qtl_mysql.hpp
+++ b/include/qtl_mysql.hpp
@@ -297,6 +297,11 @@
 		m_binderAddins[index].m_after_fetch=[](const binder& bind) {
 			if(*bind.is_null)
 				memset(bind.buffer, 0, bind.buffer_length+1);
+			else
+			{
+				char* text=reinterpret_cast<char*>(bind.buffer);
+				text[*bind.length]='\0';
+			}
 		};
 	}
 
diff --git a/include/qtl_mysql_pool.hpp b/include/qtl_mysql_pool.hpp
index 236d625..53c5a4e 100644
--- a/include/qtl_mysql_pool.hpp
+++ b/include/qtl_mysql_pool.hpp
@@ -15,10 +15,16 @@
 public:
 	database_pool() : m_port(0) { }
 	virtual ~database_pool() { }
-	virtual bool open_database(database& db) override
+	virtual database* new_database() throw() override
 	{
-		db.charset_name("utf8");
-		return db.open(m_host.data(), m_user.data(), m_password.data(), m_database.data(), 0, m_port);
+		database* db=new database;
+		db->charset_name("utf8");
+		if(!db->open(m_host.data(), m_user.data(), m_password.data(), m_database.data(), 0, m_port))
+		{
+			delete db;
+			db=NULL;
+		}
+		return db;
 	}
 
 protected:
diff --git a/include/qtl_odbc.hpp b/include/qtl_odbc.hpp
new file mode 100644
index 0000000..e021ef6
--- /dev/null
+++ b/include/qtl_odbc.hpp
@@ -0,0 +1,1047 @@
+#ifndef _QTL_ODCB_H_
+#define _QTL_ODCB_H_
+
+#include <sql.h>
+#include <sqlext.h>
+#include <sstream>
+#include <vector>
+#include <array>
+#include <time.h>
+#include <assert.h>
+#include <malloc.h>
+#include <limits.h>
+
+#if !defined(_WIN32) || defined(__MINGW32__)
+#include <sys/time.h>
+#endif //_WIN32
+
+#include "qtl_common.hpp"
+
+namespace qtl
+{
+
+namespace odbc
+{
+
+template<SQLSMALLINT> class object;
+class database;
+
+class error : public std::exception
+{
+public:
+	template<SQLSMALLINT Type>
+	error(const object<Type>& h, SQLINTEGER code);
+	error(SQLINTEGER code, const char* msg) : m_errno(code), m_errmsg(msg) { }
+	SQLINTEGER code() const { return m_errno; }
+	operator bool() { return m_errno==SQL_SUCCESS || m_errno==SQL_SUCCESS_WITH_INFO; }
+	virtual const char* what() const throw() override { return m_errmsg.data(); }
+private:
+	SQLINTEGER m_errno;
+	std::string m_errmsg;
+};
+
+template<SQLSMALLINT Type>
+class object
+{
+public:
+	enum { handler_type=Type };
+	object() : m_handle(SQL_NULL_HANDLE) { };
+	object(const object&) = delete;
+	object(object&& src) : m_handle(src.m_handle)
+	{
+		src.m_handle=SQL_NULL_HANDLE;
+	}
+	explicit object(SQLHANDLE parent)
+	{
+		verify_error(SQLAllocHandle(handler_type, parent, &m_handle));
+	}
+	~object()
+	{
+		close();
+	}
+	object& operator=(const object&) = delete;
+	object& operator=(object&& src)
+	{
+		if(this!=&src)
+		{
+			close();
+			m_handle=src.m_handle;
+			src.m_handle=NULL;
+		}
+		return *this;
+	}
+	SQLHANDLE handle() const { return m_handle; }
+
+	void close()
+	{
+		if(m_handle)
+		{
+			verify_error(SQLFreeHandle(handler_type, m_handle));
+			m_handle=SQL_NULL_HANDLE;
+		}
+	}
+
+protected:
+	SQLHANDLE m_handle;
+	void verify_error(SQLINTEGER code) const
+	{
+		if(code<0)
+			throw odbc::error(*this, code); 
+	}
+};
+
+class environment final : public object<SQL_HANDLE_ENV>
+{
+public:
+	environment() : object(SQL_NULL_HANDLE) 
+	{
+		verify_error(SQLSetEnvAttr(m_handle, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0));
+	}
+	environment(environment&& src) : object(std::forward<environment>(src)) { }
+};
+
+class statement final : public object<SQL_HANDLE_STMT>
+{
+public:
+	explicit statement(database& db);
+	statement(statement&& src) 
+		: object(std::forward<statement>(src)), m_params(std::forward<std::vector<param_data>>(src.m_params))
+	{
+		m_binded_cols=src.m_binded_cols;
+		src.m_binded_cols=false;
+		m_blob_buffer=src.m_blob_buffer;
+		src.m_blob_buffer=NULL;
+	}
+	~statement()
+	{
+		if(m_blob_buffer)
+			free(m_blob_buffer);
+	}
+	statement& operator=(statement&& src)
+	{
+		if(this!=&src)
+		{
+			object::operator =(std::forward<statement>(src));
+			m_params=std::forward<std::vector<param_data>>(src.m_params);
+			m_binded_cols=src.m_binded_cols;
+			src.m_binded_cols=false;
+			m_blob_buffer=src.m_blob_buffer;
+			src.m_blob_buffer=NULL;
+		}
+		return *this;
+	}
+
+	void open(const char* query_text, size_t text_length=SQL_NTS)
+	{
+		verify_error(SQLPrepare(m_handle, (SQLCHAR*)query_text, text_length));
+	}
+	void open(const std::string& query_text)
+	{
+		open(query_text.data(), query_text.size());
+	}
+
+	void bind_param(SQLUSMALLINT index, const std::nullptr_t&)
+	{
+		m_params[index].m_indicator=SQL_NULL_DATA;
+		verify_error(SQLBindParameter(m_handle, index+1, 
+			SQL_PARAM_INPUT, SQL_C_DEFAULT, SQL_DEFAULT, 0, 0, NULL, 0, &m_params[index].m_indicator));
+	}
+	void bind_param(SQLUSMALLINT index, const qtl::null&)
+	{
+		bind_param(index, nullptr);
+	}
+	void bind_param(SQLUSMALLINT index, const int8_t& v)
+	{
+		verify_error(SQLBindParameter(m_handle, index+1, SQL_PARAM_INPUT, SQL_C_STINYINT, SQL_TINYINT, 
+			0, 0, (SQLPOINTER)&v, 0, NULL));
+	}
+	void bind_param(SQLUSMALLINT index, const uint8_t& v)
+	{
+		verify_error(SQLBindParameter(m_handle, index+1, SQL_PARAM_INPUT, SQL_C_UTINYINT, SQL_TINYINT, 
+			0, 0, (SQLPOINTER)&v, 0, NULL));
+	}
+	void bind_param(SQLUSMALLINT index, const int16_t& v)
+	{
+		verify_error(SQLBindParameter(m_handle, index+1, SQL_PARAM_INPUT, SQL_C_SSHORT, SQL_SMALLINT, 
+			0, 0, (SQLPOINTER)&v, 0, NULL));
+	}
+	void bind_param(SQLUSMALLINT index, const uint16_t& v)
+	{
+		verify_error(SQLBindParameter(m_handle, index+1, SQL_PARAM_INPUT, SQL_C_USHORT, SQL_SMALLINT, 
+			0, 0, (SQLPOINTER)&v, 0, NULL));
+	}
+	void bind_param(SQLUSMALLINT index, const int32_t& v)
+	{
+		verify_error(SQLBindParameter(m_handle, index+1, SQL_PARAM_INPUT, SQL_C_SLONG, SQL_INTEGER, 
+			0, 0, (SQLPOINTER)&v, 0, NULL));
+	}
+	void bind_param(SQLUSMALLINT index, const uint32_t& v)
+	{
+		verify_error(SQLBindParameter(m_handle, index+1, SQL_PARAM_INPUT, SQL_C_ULONG, SQL_INTEGER, 
+			0, 0, (SQLPOINTER)&v, 0, NULL));
+	}
+	void bind_param(SQLUSMALLINT index, const int64_t& v)
+	{
+		verify_error(SQLBindParameter(m_handle, index+1, SQL_PARAM_INPUT, SQL_C_SBIGINT, SQL_BIGINT, 
+			0, 0, (SQLPOINTER)&v, 0, NULL));
+	}
+	void bind_param(SQLUSMALLINT index, const uint64_t& v)
+	{
+		verify_error(SQLBindParameter(m_handle, index+1, SQL_PARAM_INPUT, SQL_C_UBIGINT, SQL_BIGINT, 
+			0, 0, (SQLPOINTER)&v, 0, NULL));
+	}
+	void bind_param(SQLUSMALLINT index, const double& v)
+	{
+		verify_error(SQLBindParameter(m_handle, index+1, SQL_PARAM_INPUT, SQL_C_DOUBLE, SQL_DOUBLE, 
+			0, 0, (SQLPOINTER)&v, 0, NULL));
+	}
+	void bind_param(SQLUSMALLINT index, const float& v)
+	{
+		verify_error(SQLBindParameter(m_handle, index+1, SQL_PARAM_INPUT, SQL_C_FLOAT, SQL_FLOAT, 
+			0, 0, (SQLPOINTER)&v, 0, NULL));
+	}
+	void bind_param(SQLUSMALLINT index, const bool& v)
+	{
+		verify_error(SQLBindParameter(m_handle, index+1, SQL_PARAM_INPUT, SQL_C_BIT, SQL_BIT, 
+			0, 0, (SQLPOINTER)&v, 0, NULL));
+	}
+	void bind_param(SQLUSMALLINT index, const DATE_STRUCT& v)
+	{
+		verify_error(SQLBindParameter(m_handle, index+1, SQL_PARAM_INPUT, SQL_C_DATE, SQL_DATE, 
+			0, 0, (SQLPOINTER)&v, 0, NULL));
+	}
+	void bind_param(SQLUSMALLINT index, const TIME_STRUCT& v)
+	{
+		verify_error(SQLBindParameter(m_handle, index+1, SQL_PARAM_INPUT, SQL_C_TIME, SQL_TIME, 
+			0, 0, (SQLPOINTER)&v, 0, NULL));
+	}
+	void bind_param(SQLUSMALLINT index, const TIMESTAMP_STRUCT& v)
+	{
+		verify_error(SQLBindParameter(m_handle, index+1, SQL_PARAM_INPUT, SQL_C_TIMESTAMP, SQL_TIMESTAMP, 
+			0, 0, (SQLPOINTER)&v, 0, NULL));
+	}
+	void bind_param(SQLUSMALLINT index, const SQLGUID& v)
+	{
+		verify_error(SQLBindParameter(m_handle, index+1, SQL_PARAM_INPUT, SQL_C_GUID, SQL_GUID, 
+			0, 0, (SQLPOINTER)&v, 0, NULL));
+	}
+	void bind_param(SQLUSMALLINT index, const SQL_NUMERIC_STRUCT& v)
+	{
+		verify_error(SQLBindParameter(m_handle, index+1, SQL_PARAM_INPUT, SQL_C_NUMERIC, SQL_NUMERIC, 
+			0, 0, (SQLPOINTER)&v, 0, NULL));
+	}
+	void bind_param(SQLUSMALLINT index, const char* v, size_t n=SQL_NTS, SQLULEN size=0)
+	{
+		m_params[index].m_indicator=n;
+		if(size==0) size=strlen(v);
+		verify_error(SQLBindParameter(m_handle, index+1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR, 
+			size, 0, (SQLPOINTER)v, 0, &m_params[index].m_indicator));
+	}
+	void bind_param(SQLUSMALLINT index, const wchar_t* v, size_t n=SQL_NTS, SQLULEN size=0)
+	{
+		m_params[index].m_indicator=n;
+		if(size==0) size=wcslen(v);
+		verify_error(SQLBindParameter(m_handle, index+1, SQL_PARAM_INPUT, SQL_C_WCHAR, SQL_WCHAR, 
+			size, 0, (SQLPOINTER)v, 0, &m_params[index].m_indicator));
+	}
+	void bind_param(SQLUSMALLINT index, const std::string& v)
+	{
+		bind_param(index, v.data(), v.size(), v.size());
+	}
+	void bind_param(SQLUSMALLINT index, const std::wstring& v)
+	{
+		bind_param(index, v.data(), v.size(), v.size());
+	}
+	void bind_param(SQLUSMALLINT index, const const_blob_data& v)
+	{
+		m_params[index].m_indicator=v.size;
+		verify_error(SQLBindParameter(m_handle, index+1, SQL_PARAM_INPUT, SQL_C_BINARY, SQL_BINARY, 
+			v.size, 0, (SQLPOINTER)v.data, 0, &m_params[index].m_indicator));
+	}
+	void bind_param(SQLUSMALLINT index, std::istream& s)
+	{
+		if(m_blob_buffer==NULL)
+			m_blob_buffer=malloc(blob_buffer_size);
+		m_params[index].m_data=m_blob_buffer;
+		m_params[index].m_size=blob_buffer_size;
+		m_params[index].m_indicator=SQL_LEN_DATA_AT_EXEC(m_params[index].m_size);
+		verify_error(SQLBindParameter(m_handle, index+1, SQL_PARAM_INPUT, SQL_C_BINARY, SQL_LONGVARBINARY, 
+			INT_MAX, 0, &m_params[index], 0, &m_params[index].m_indicator));
+		m_params[index].m_after_fetch=[this, &s](const param_data& p) {
+			SQLLEN readed=SQL_NULL_DATA;
+			while(!s.eof() && !s.fail())
+			{
+				s.read((char*)p.m_data, p.m_size);
+				readed=(unsigned long)s.gcount();
+				if(readed>0)
+				{
+					verify_error(SQLPutData(m_handle, p.m_data, readed));
+				}
+			}
+		};
+	}
+
+	void bind_field(SQLUSMALLINT index, bool&& v)
+	{
+		verify_error(SQLBindCol(m_handle, index+1, SQL_C_BIT, &v, 0, &m_params[index].m_indicator));
+	}
+	void bind_field(SQLUSMALLINT index, int8_t&& v)
+	{
+		verify_error(SQLBindCol(m_handle, index+1, SQL_C_STINYINT, &v, 0, &m_params[index].m_indicator));
+	}
+	void bind_field(SQLUSMALLINT index, uint8_t&& v)
+	{
+		verify_error(SQLBindCol(m_handle, index+1, SQL_C_UTINYINT, &v, 0, &m_params[index].m_indicator));
+	}
+	void bind_field(SQLUSMALLINT index, int16_t&& v)
+	{
+		verify_error(SQLBindCol(m_handle, index+1, SQL_C_SSHORT, &v, 0, &m_params[index].m_indicator));
+	}
+	void bind_field(SQLUSMALLINT index, uint16_t&& v)
+	{
+		verify_error(SQLBindCol(m_handle, index+1, SQL_C_USHORT, &v, 0, &m_params[index].m_indicator));
+	}
+	void bind_field(SQLUSMALLINT index, int32_t&& v)
+	{
+		verify_error(SQLBindCol(m_handle, index+1, SQL_C_SLONG, &v, 0, &m_params[index].m_indicator));
+	}
+	void bind_field(SQLUSMALLINT index, uint32_t&& v)
+	{
+		verify_error(SQLBindCol(m_handle, index+1, SQL_C_ULONG, &v, 0, &m_params[index].m_indicator));
+	}
+	void bind_field(SQLUSMALLINT index, int64_t&& v)
+	{
+		verify_error(SQLBindCol(m_handle, index+1, SQL_C_SBIGINT, &v, 0, &m_params[index].m_indicator));
+	}
+	void bind_field(SQLUSMALLINT index, uint64_t&& v)
+	{
+		verify_error(SQLBindCol(m_handle, index+1, SQL_C_UBIGINT, &v, 0, &m_params[index].m_indicator));
+	}
+	void bind_field(SQLUSMALLINT index, float&& v)
+	{
+		verify_error(SQLBindCol(m_handle, index+1, SQL_C_FLOAT, &v, 0, &m_params[index].m_indicator));
+	}
+	void bind_field(SQLUSMALLINT index, double&& v)
+	{
+		verify_error(SQLBindCol(m_handle, index+1, SQL_C_DOUBLE, &v, 0, &m_params[index].m_indicator));
+	}
+	void bind_field(SQLUSMALLINT index, DATE_STRUCT&& v)
+	{
+		verify_error(SQLBindCol(m_handle, index+1, SQL_C_TYPE_DATE, &v, 0, &m_params[index].m_indicator));
+	}
+	void bind_field(SQLUSMALLINT index, TIME_STRUCT&& v)
+	{
+		verify_error(SQLBindCol(m_handle, index+1, SQL_C_TYPE_TIME, &v, 0, &m_params[index].m_indicator));
+	}
+	void bind_field(SQLUSMALLINT index, TIMESTAMP_STRUCT&& v)
+	{
+		verify_error(SQLBindCol(m_handle, index+1, SQL_C_TYPE_TIMESTAMP, &v, 0, &m_params[index].m_indicator));
+	}
+	void bind_field(SQLUSMALLINT index, SQLGUID&& v)
+	{
+		verify_error(SQLBindCol(m_handle, index+1, SQL_C_GUID, &v, 0, &m_params[index].m_indicator));
+	}
+	void bind_field(SQLUSMALLINT index, SQL_NUMERIC_STRUCT&& v)
+	{
+		verify_error(SQLBindCol(m_handle, index+1, SQL_C_NUMERIC, &v, 0, &m_params[index].m_indicator));
+	}
+	void bind_field(SQLUSMALLINT index, char* v, size_t n)
+	{
+		m_params[index].m_data=v;
+		m_params[index].m_size=n;
+		m_params[index].m_after_fetch=[](const param_data& p) {
+			if(p.m_indicator==SQL_NULL_DATA)
+				memset(p.m_data, 0, p.m_size*sizeof(char));
+			else
+			{
+				char* text=reinterpret_cast<char*>(p.m_data);
+				text[p.m_indicator]='\0';
+			}
+		};
+		verify_error(SQLBindCol(m_handle, index+1, SQL_C_CHAR, v, n-1, &m_params[index].m_indicator));
+	}
+	void bind_field(SQLUSMALLINT index, wchar_t* v, size_t n)
+	{
+		m_params[index].m_data=v;
+		m_params[index].m_size=n;
+		m_params[index].m_after_fetch=[](const param_data& p) {
+			if(p.m_indicator==SQL_NULL_DATA)
+				memset(p.m_data, 0, p.m_size*sizeof(wchar_t));
+			else
+			{
+				wchar_t* text=reinterpret_cast<wchar_t*>(p.m_data);
+				text[p.m_indicator]='\0';
+			}
+		};
+		verify_error(SQLBindCol(m_handle, index+1, SQL_C_WCHAR, v, n-1, &m_params[index].m_indicator));
+	}
+	template<typename CharType>
+	void bind_field(SQLUSMALLINT index, std::basic_string<CharType>&& v)
+	{
+		SQLLEN length=0;
+		SQLColAttribute(m_handle, index+1, SQL_DESC_LENGTH, NULL, 0, NULL, &length);
+		v.resize(length);
+		bind_field(index, const_cast<char*>(v.data()), v.size()+1);
+		m_params[index].m_after_fetch=[&v](const param_data& p) {
+			if(p.m_indicator==SQL_NULL_DATA)
+				v.clear();
+			else
+				v.resize(p.m_indicator);
+		};
+	}
+	template<size_t N>
+	void bind_field(SQLUSMALLINT index, std::array<char, N>&& value)
+	{
+		bind_field(index, value.data(), value.size());
+	}
+	template<size_t N>
+	void bind_field(SQLUSMALLINT index, std::array<wchar_t, N>&& value)
+	{
+		bind_field(index, value.data(), value.size());
+	}
+	void bind_field(SQLUSMALLINT index, qtl::blob_data&& v)
+	{
+		verify_error(SQLBindCol(m_handle, index+1, SQL_C_BINARY, v.data, v.size, &m_params[index].m_indicator));
+	}
+	void bind_field(SQLUSMALLINT index, std::ostream&& v)
+	{
+		if(m_blob_buffer==NULL)
+			m_blob_buffer=malloc(blob_buffer_size);
+		m_params[index].m_data=m_blob_buffer;
+		m_params[index].m_size=blob_buffer_size;
+		m_params[index].m_after_fetch=[this, index, &v](const param_data& p) {
+			SQLRETURN ret=SQLGetData(m_handle, index+1, SQL_C_BINARY, p.m_data, p.m_size, const_cast<SQLLEN*>(&p.m_indicator));
+			while(ret!=SQL_NO_DATA)
+			{
+				size_t n = (p.m_indicator > blob_buffer_size) || (p.m_indicator == SQL_NO_TOTAL) ?
+					blob_buffer_size : p.m_indicator;
+				verify_error(ret);
+				v.write((const char*)p.m_data, n);
+				ret=SQLGetData(m_handle, index+1, SQL_C_BINARY, p.m_data, p.m_size, const_cast<SQLLEN*>(&p.m_indicator));
+			}
+		};
+	}
+
+	template<typename Type>
+	void bind_field(size_t index, indicator<Type>&& value)
+	{
+		qtl::bind_field(*this, index, value.data);
+		param_data& param=m_params[index];
+		auto fetch_fun=param.m_after_fetch;
+		param.m_after_fetch=[fetch_fun, &value](const param_data& p) {
+			value.is_truncated=false;
+			if(p.m_indicator==SQL_NULL_DATA)
+			{
+				value.is_null=true;
+				value.length=0;
+			}
+			else if(p.m_indicator>=0)
+			{
+				value.is_null=false;
+				value.length=p.m_indicator;
+				if(p.m_size>0 && p.m_indicator>=p.m_size)
+					value.is_truncated=true;
+			}
+			if(fetch_fun) fetch_fun(p);
+		};
+	}
+
+	template<typename Types>
+	void execute(const Types& params)
+	{
+		SQLSMALLINT count=0;
+		verify_error(SQLNumParams(m_handle, &count));
+		if(count>0)
+		{
+			m_params.resize(count);
+			qtl::bind_params(*this, params);
+		}
+
+		SQLRETURN ret=SQLExecute(m_handle);
+		verify_error(ret);
+		if(ret==SQL_NEED_DATA)
+		{
+			SQLPOINTER token;
+			size_t i=0;
+			ret=SQLParamData(m_handle, &token);
+			verify_error(ret);
+			while(ret==SQL_NEED_DATA)
+			{
+				while(i!=count)
+				{
+					if(&m_params[i]==token)
+					{
+						if(m_params[i].m_after_fetch)
+							m_params[i].m_after_fetch(m_params[i]);
+						break;
+					}
+					++i;
+				}
+				ret=SQLParamData(m_handle, &token);
+				verify_error(ret);
+			}
+		}
+	}
+
+	template<typename Types>
+	bool fetch(Types&& values)
+	{
+		if(!m_binded_cols)
+		{
+			SQLSMALLINT count=0;
+			verify_error(SQLNumResultCols(m_handle, &count));
+			if(count>0)
+			{
+				m_params.resize(count);
+				qtl::bind_record(*this, std::forward<Types>(values));
+			}
+			m_binded_cols=true;
+		}
+		return fetch();
+	}
+
+	bool fetch()
+	{
+		SQLRETURN ret=SQLFetch(m_handle);
+		if(ret==SQL_SUCCESS || ret==SQL_SUCCESS_WITH_INFO)
+		{
+			for(const param_data& data : m_params)
+			{
+				if(data.m_after_fetch)
+					data.m_after_fetch(data);
+			}
+			return true;
+		}
+		verify_error(ret);
+		return false;
+	}
+
+	SQLLEN affetced_rows()
+	{
+		SQLLEN count=0;
+		verify_error(SQLRowCount(m_handle, &count));
+		return count;
+	}
+
+	/*
+		ODBC do not support this function, but you can use query to instead it:
+		For MS SQL Server: SELECT @@IDENTITY;
+		For MySQL: SELECT LAST_INSERT_ID();
+		For SQLite: SELECT last_insert_rowid();
+	 */
+	/*uint64_t insert_id()
+	{
+		assert(false);
+		return 0;
+	}*/
+
+	void reset()
+	{
+		verify_error(SQLFreeStmt(m_handle, SQL_RESET_PARAMS));
+	}
+
+private:
+	struct param_data
+	{
+		SQLPOINTER m_data;
+		SQLLEN m_size;
+		SQLLEN m_indicator;
+		std::function<void(const param_data&)> m_after_fetch;
+
+		param_data() : m_data(NULL), m_size(0), m_indicator(0) { }
+	};
+	SQLPOINTER m_blob_buffer;
+	std::vector<param_data> m_params;
+	bool m_binded_cols;
+};
+
+struct connection_parameter
+{
+	std::string m_name;
+	std::string m_prompt;
+	std::string m_value;
+	std::vector<std::string> m_value_list;
+	bool m_optinal;
+	bool m_assigned;
+
+	connection_parameter() : m_optinal(false), m_assigned(false) { }
+	void reset()
+	{
+		m_name.clear();
+		m_prompt.clear();
+		m_value.clear();
+		m_value_list.clear();
+		m_optinal=false;
+		m_assigned=false;
+	}
+};
+typedef std::vector<connection_parameter> connection_parameters;
+
+class database : public object<SQL_HANDLE_DBC>, public qtl::base_database<database, statement>
+{
+public:
+	explicit database(environment& env) : object(env.handle()), m_opened(false)
+	{
+	}
+	database(database&& src) 
+		: object(std::forward<database>(src)), m_connection(std::forward<std::string>(src.m_connection)) 
+	{
+		m_opened=src.m_opened;
+		src.m_opened=false;
+	}
+	~database()
+	{
+		close();
+	}
+	database& operator=(database&& src)
+	{
+		if(this!=&src)
+		{
+			object::operator =(std::forward<database>(src));
+			m_opened=src.m_opened;
+			src.m_opened=false;
+			m_connection=std::forward<std::string>(src.m_connection);
+		}
+		return *this;
+	}
+
+	void open(const char* server_name, size_t server_name_length, 
+		const char* user_name, size_t user_name_length, const char* password, size_t password_length)
+	{
+		if(m_opened) close();
+		verify_error(SQLConnectA(m_handle, (SQLCHAR*)server_name, server_name_length, (SQLCHAR*)user_name, user_name_length, (SQLCHAR*)password, password_length));
+		m_opened=true;
+	}
+	void open(const char* server_name, const char* user_name, const char* password)
+	{
+		verify_error(SQLConnectA(m_handle, (SQLCHAR*)server_name, SQL_NTS, (SQLCHAR*)user_name, SQL_NTS, (SQLCHAR*)password, SQL_NTS));
+	}
+	void open(const std::string& server_name, const std::string& user_name, const std::string& password)
+	{
+		open(server_name.data(), server_name.size(), user_name.data(), user_name.size(), password.data(), password.size());
+	}
+	void open(const char* input_text, size_t text_length=SQL_NTS, SQLSMALLINT driver_completion=SQL_DRIVER_NOPROMPT, SQLHWND hwnd=NULL)
+	{
+		m_connection.resize(512);
+		SQLSMALLINT out_len;
+		if(m_opened) close();
+		verify_error(SQLDriverConnectA(m_handle, hwnd, (SQLCHAR*)input_text, (SQLSMALLINT)text_length, 
+			(SQLCHAR*)m_connection.data(), (SQLSMALLINT)m_connection.size(), &out_len, driver_completion));
+		m_connection.resize(out_len);
+		m_opened=true;
+	}
+	void open(const std::string& input_text, SQLSMALLINT driver_completion=SQL_DRIVER_NOPROMPT, SQLHWND hwnd=NULL)
+	{
+		open(input_text.data(), input_text.size(), driver_completion, hwnd);
+	}
+	void open(SQLHWND hwnd, SQLSMALLINT driver_completion=SQL_DRIVER_COMPLETE)
+	{
+		open("", SQL_NTS, driver_completion, hwnd);
+	}
+	// InputPred like:
+	// bool input_parameters(connection_parameters& parameters);
+	template<typename InputPred>
+	void open(const char* connection_text, size_t text_length, InputPred&& pred)
+	{
+		SQLSMALLINT length=0;
+		SQLINTEGER ret=SQL_SUCCESS;
+		std::string input_text;
+		if(m_opened) close();
+		if(text_length==SQL_NTS)
+			input_text=connection_text;
+		else
+			input_text.assign(connection_text, text_length);
+		m_connection.resize(1024);
+		while( (ret=SQLBrowseConnectA(m_handle, (SQLCHAR*)input_text.data(), SQL_NTS, 
+			(SQLCHAR*)m_connection.data(), m_connection.size(), &length)) == SQL_NEED_DATA)
+		{
+			connection_parameters parameters;
+			parse_browse_string(m_connection.data(), length, parameters);
+			if(!pred(parameters))
+				throw error(SQL_NEED_DATA, "User cancel operation.");
+			input_text=create_connection_text(parameters);
+		}
+		if(ret==SQL_ERROR || ret==SQL_SUCCESS_WITH_INFO)
+			verify_error(ret);
+		m_opened=true;
+	}
+	template<typename InputPred>
+	void open(const char* connection_text, InputPred&& pred)
+	{
+		open(connection_text, SQL_NTS, std::forward<InputPred>(pred));
+	}
+	template<typename InputPred>
+	void open(const std::string& connection_text, InputPred&& pred)
+	{
+		open(connection_text.data(), connection_text.size(), std::forward<InputPred>(pred));
+	}
+	void close()
+	{
+		if(m_opened)
+		{
+			verify_error(SQLDisconnect(m_handle));
+			m_opened=false;
+		}
+	}
+
+	void set_attribute(SQLINTEGER attr, SQLINTEGER value)
+	{
+		verify_error(SQLSetConnectAttr(m_handle, attr, &value, 0));
+	}
+	void get_attribute(SQLINTEGER attr, SQLINTEGER& value) const
+	{
+		verify_error(SQLGetConnectAttr(m_handle, attr, &value, sizeof(SQLINTEGER), 0));
+	}
+	void get_info(SQLSMALLINT info, std::string& value, SQLSMALLINT size=SQL_MAX_OPTION_STRING_LENGTH) const
+	{
+		value.resize(size);
+		verify_error(SQLGetInfo(m_handle, info, (SQLPOINTER)value.data(), size, &size));
+		value.resize(size);
+	}
+
+	void auto_commit(bool on)
+	{
+		set_attribute(SQL_ATTR_AUTOCOMMIT, on ? SQL_AUTOCOMMIT_ON : SQL_AUTOCOMMIT_OFF);
+	}
+	void begin_transaction()
+	{
+		auto_commit(false);
+	}
+	void rollback()
+	{
+		verify_error(SQLEndTran(handler_type, m_handle, SQL_ROLLBACK));
+		auto_commit(true);
+	}
+	void commit()
+	{
+		verify_error(SQLEndTran(handler_type, m_handle, SQL_COMMIT));
+		auto_commit(true);
+	}
+
+	std::string dbms_name() const
+	{
+		std::string name;
+		get_info(SQL_DBMS_NAME, name);
+		return name;
+	}
+	std::string server_name() const
+	{
+		std::string name;
+		get_info(SQL_SERVER_NAME, name);
+		return name;
+	}
+	std::string user_name() const
+	{
+		std::string name;
+		get_info(SQL_USER_NAME, name);
+		return name;
+	}
+	std::string db_name() const
+	{
+		std::string name;
+		get_info(SQL_DATABASE_NAME, name);
+		return name;
+	}
+	const std::string& connection_text() const
+	{
+		return m_connection;
+	}
+
+	bool is_alive()
+	{
+		SQLINTEGER value;
+		get_attribute(SQL_ATTR_CONNECTION_DEAD, value);
+		return value==SQL_CD_FALSE;
+	}
+
+	statement open_command(const char* query_text, size_t text_length)
+	{
+		statement stmt(*this);
+		stmt.open(query_text, text_length);
+		return stmt;
+	}
+	statement open_command(const char* query_text)
+	{
+		return open_command(query_text, strlen(query_text));
+	}
+	statement open_command(const std::string& query_text)
+	{
+		return open_command(query_text.data(), query_text.length());
+	}
+
+	void simple_execute(const char* query_text, size_t text_length=SQL_NTS)
+	{
+		statement command(*this);
+		SQLRETURN ret=SQLExecDirectA(command.handle(), (SQLCHAR*)query_text, text_length);
+		if(ret!=SQL_SUCCESS && ret!=SQL_NO_DATA)
+			verify_error(ret);
+	}
+	void simple_execute(const std::string& query_text)
+	{
+		simple_execute(query_text.data(), query_text.size());
+	}
+
+private:
+	bool m_opened;
+	std::string m_connection;
+
+	void parse_browse_string(const char* output_text, size_t text_length, connection_parameters& parameters);
+	std::string create_connection_text(const connection_parameters& parameters);
+};
+
+struct date : public SQL_DATE_STRUCT
+{
+	date()
+	{
+		memset(this, 0, sizeof(SQL_DATE_STRUCT));
+	}
+};
+
+struct time : public SQL_TIME_STRUCT
+{
+	time()
+	{
+		memset(this, 0, sizeof(SQL_TIME_STRUCT));
+	}
+};
+
+struct timestamp : public SQL_TIMESTAMP_STRUCT
+{
+	timestamp()
+	{
+		memset(this, 0, sizeof(SQL_TIMESTAMP_STRUCT));
+	}
+	timestamp(struct tm& tm)
+	{
+		year=tm.tm_year+1900;
+		month=tm.tm_mon+1;
+		day=tm.tm_mday;
+		hour=tm.tm_hour;
+		minute=tm.tm_min;
+		second=tm.tm_sec;
+	}
+	timestamp(time_t value)
+	{
+		struct tm tm;
+#if defined(_MSC_VER)
+		localtime_s(&tm, &value);
+#elif defined(_POSIX_VERSION)
+		localtime_r(&value, &tm);
+#else
+		tm=*localtime(&value);
+#endif
+		new(this)timestamp(tm);
+	}
+	timestamp(const timestamp& src)
+	{
+		memcpy(this, &src, sizeof(SQL_TIMESTAMP_STRUCT));
+	}
+	timestamp& operator=(const timestamp& src)
+	{
+		if(this!=&src)
+			memcpy(this, &src, sizeof(SQL_TIMESTAMP_STRUCT));
+		return *this;
+	}
+
+	static timestamp now()
+	{
+		time_t value;
+		::time(&value);
+		return timestamp(value);
+	}
+
+	time_t as_tm(struct tm& tm) const
+	{
+		tm.tm_year=year-1900;
+		tm.tm_mon=month-1;
+		tm.tm_mday=day;
+		tm.tm_hour=hour;
+		tm.tm_min=minute;
+		tm.tm_sec=second;
+		return mktime(&tm);
+	}
+	time_t get_time() const
+	{
+		struct tm tm;
+		return as_tm(tm);
+	}
+	timeval get_timeval() const
+	{
+		timeval tv;
+		struct tm tm;
+		tv.tv_sec=as_tm(tm);
+		tv.tv_usec=fraction/1000;
+	}
+};
+
+typedef qtl::transaction<database> transaction;
+
+template<typename Record>
+using query_iterator = qtl::query_iterator<statement, Record>;
+
+template<typename Record>
+using query_result = qtl::query_result<statement, Record>;
+
+template<typename Params>
+inline statement& operator<<(statement& stmt, const Params& params)
+{
+	stmt.reset();
+	stmt.execute(params);
+	return stmt;
+}
+
+template<SQLSMALLINT Type>
+inline error::error(const object<Type>& h, SQLINTEGER code)
+{
+	m_errno=code;
+	if(code==SQL_ERROR || code==SQL_SUCCESS_WITH_INFO)
+	{
+		SQLSMALLINT i=0;
+		SQLINTEGER err=SQL_SUCCESS;
+		SQLCHAR message[SQL_MAX_MESSAGE_LENGTH];
+		SQLCHAR state[SQL_SQLSTATE_SIZE+1];
+		std::ostringstream oss;
+		while(SQLGetDiagRecA(object<Type>::handler_type, h.handle(), ++i, state, &err, 
+			message, SQL_MAX_MESSAGE_LENGTH, NULL)==SQL_SUCCESS)
+		{
+			oss<<"["<<state<<"] ("<<err<<") "<<message<<std::endl;
+		}
+		m_errmsg=oss.str();
+	}
+	else if(code==SQL_INVALID_HANDLE)
+	{
+		m_errmsg="Invalid handle.";
+	}
+}
+
+inline void database::parse_browse_string(const char* output_text, size_t text_length, connection_parameters& parameters)
+{
+	enum { part_name, part_prompt, part_list, part_value };
+	const char* sp=output_text;
+	const char* token=sp;
+	connection_parameter parameter;
+	int part_type=part_name;
+	while(sp!=output_text+text_length)
+	{
+		switch(*sp)
+		{
+		case ';':
+			parameters.emplace_back(parameter);
+			parameter.reset();
+			part_type=part_name;
+			token=sp+1;
+			break;
+		case '=':
+			if(part_type==part_prompt)
+				parameter.m_prompt.assign(token, sp-token);
+			part_type=part_value;
+			token=sp+1;
+			break;
+		case ':':
+			if(part_type==part_name)
+				parameter.m_name.assign(token, sp-token);
+			part_type=part_prompt;
+			token=sp+1;
+			break;
+		case '{':
+			part_type=part_list;
+			parameter.m_value_list.clear();
+			token=sp+1;
+			break;;
+		case '}':
+		case ',':
+			if(part_type==part_list)
+				parameter.m_value_list.emplace_back(token, sp-token);
+			token=sp+1;
+			break;
+		case '*':
+			if(part_type==part_name && token==sp)
+			{
+				parameter.m_optinal=true;
+				token=sp+1;
+			}
+			break;
+		case '?':
+			token=sp+1;
+			break;
+		}
+		++sp;
+	}
+	if(!parameter.m_name.empty())
+		parameters.emplace_back(parameter);
+}
+
+inline std::string database::create_connection_text(const connection_parameters& parameters)
+{
+	std::ostringstream oss;
+	for(auto& parameter : parameters)
+	{
+		if(parameter.m_assigned)
+			oss<<parameter.m_name<<'='<<parameter.m_value<<';';
+	}
+	return oss.str();
+}
+
+inline statement::statement(database& db)
+	: object(db.handle()), m_blob_buffer(NULL), m_binded_cols(false)
+{
+}
+
+} //odbc
+
+#ifdef _WIN32
+
+namespace mssql
+{
+
+class database : public odbc::database
+{
+public:
+	explicit database(odbc::environment& env) : odbc::database(env) { }
+	database(database&& src) : odbc::database(std::forward<database>(src)) { }
+
+	void open(const char* server, const char* db=NULL, const char* user=NULL, const char* password=NULL)
+	{
+		std::ostringstream oss;
+		oss<<"DRIVER={SQL Server};SERVER="<<server<<";";
+		if(user==NULL)
+			oss<<"UID=;PWD=;Trusted_Connection=yes;";
+		else
+		{
+			oss<<"UID="<<user<<";PWD=";
+			if(password) oss<<password;
+			oss<<";Trusted_Connection=no;";
+		}
+		oss<<"DATABASE="<<db;
+		odbc::database::open(oss.str());
+	}
+};
+
+} //mssql
+
+namespace msaccess
+{
+
+class database : public odbc::database
+{
+public:
+	explicit database(odbc::environment& env) : odbc::database(env) { }
+	database(database&& src) : odbc::database(std::forward<database>(src)) { }
+
+	void open(const char* filename, const char* user=NULL, const char* password=NULL)
+	{
+		std::ostringstream oss;
+		oss<<"DRIVER={Microsoft Access Driver};DBQ="<<filename;
+		if(user) oss<<";UID:"<<user;
+		if(password) oss<<";PWD="<<password;
+		odbc::database::open(oss.str());
+	}
+};
+
+} //msaccess
+
+#endif //_WIN32
+
+}
+
+#endif //_QTL_ODCB_H_
diff --git a/include/qtl_odbc_pool.hpp b/include/qtl_odbc_pool.hpp
new file mode 100644
index 0000000..4014cb1
--- /dev/null
+++ b/include/qtl_odbc_pool.hpp
@@ -0,0 +1,46 @@
+#ifndef _QTL_ODBC_POOL_HPP_
+#define _QTL_ODBC_POOL_HPP_
+
+#include "qtl_database_pool.hpp"
+#include "qtl_odbc.hpp"
+
+namespace qtl
+{
+
+namespace odbc
+{
+
+class database_pool : public qtl::database_pool<database>
+{
+public:
+	database_pool() { }
+	virtual ~database_pool() { }
+	virtual database* new_database() throw() override
+	{
+		database* db=NULL;
+		try
+		{
+			db=new database(m_env);
+			db->open(m_connection);
+		}
+		catch(error& e)
+		{
+			if(db)
+			{
+				delete db;
+				db=NULL;
+			}
+		}
+		return db;
+	}
+
+protected:
+	std::string m_connection;
+	environment m_env;
+};
+
+}
+
+}
+
+#endif //_QTL_ODBC_POOL_HPP_
diff --git a/include/qtl_sqlite.hpp b/include/qtl_sqlite.hpp
index 86ee324..20f7e00 100644
--- a/include/qtl_sqlite.hpp
+++ b/include/qtl_sqlite.hpp
@@ -77,63 +77,63 @@
 		}
 	}
 
-	void bind(int col, int value)
+	void bind_param(int index, int value)
 	{
-		verify_error(sqlite3_bind_int(m_stmt, col, value));
+		verify_error(sqlite3_bind_int(m_stmt, index+1, value));
 	}
-	void bind(int col, int64_t value)
+	void bind_param(int index, int64_t value)
 	{
-		verify_error(sqlite3_bind_int64(m_stmt, col, value));
+		verify_error(sqlite3_bind_int64(m_stmt, index+1, value));
 	}
-	void bind(int col, double value)
+	void bind_param(int index, double value)
 	{
-		verify_error(sqlite3_bind_double(m_stmt, col, value));
+		verify_error(sqlite3_bind_double(m_stmt, index+1, value));
 	}
-	void bind(int col, const char* value, size_t n=-1)
+	void bind_param(int index, const char* value, size_t n=-1)
 	{
 		if(value)
-			verify_error(sqlite3_bind_text(m_stmt, col, value, (int)n, NULL));
+			verify_error(sqlite3_bind_text(m_stmt, index+1, value, (int)n, NULL));
 		else
-			verify_error(sqlite3_bind_null(m_stmt, col));
+			verify_error(sqlite3_bind_null(m_stmt, index+1));
 	}
-	void bind(int col, const wchar_t* value, size_t n=-1)
+	void bind_param(int index, const wchar_t* value, size_t n=-1)
 	{
 		if(value)
-			verify_error(sqlite3_bind_text16(m_stmt, col, value, (int)n, NULL));
+			verify_error(sqlite3_bind_text16(m_stmt, index+1, value, (int)n, NULL));
 		else
-			verify_error(sqlite3_bind_null(m_stmt, col));
+			verify_error(sqlite3_bind_null(m_stmt, index+1));
 	}
-	void bind(int col, const std::string& value)
+	void bind_param(int index, const std::string& value)
 	{
-		bind(col, value.data(), value.size());
+		bind_param(index, value.data(), value.size());
 	}
-	void bind(int col, const std::wstring& value)
+	void bind_param(int index, const std::wstring& value)
 	{
-		bind(col, value.data(), value.size());
+		bind_param(index, value.data(), value.size());
 	}
-	void bind(int col, const const_blob_data& value)
+	void bind_param(int index, const const_blob_data& value)
 	{
 		if(value.size)
 		{
 			if(value.data)
-				verify_error(sqlite3_bind_blob(m_stmt, col, value.data, (int)value.size, NULL));
+				verify_error(sqlite3_bind_blob(m_stmt, index+1, value.data, (int)value.size, NULL));
 			else
-				verify_error(sqlite3_bind_zeroblob(m_stmt, col, (int)value.size));
+				verify_error(sqlite3_bind_zeroblob(m_stmt, index+1, (int)value.size));
 		}
 		else
-			verify_error(sqlite3_bind_null(m_stmt, col));
+			verify_error(sqlite3_bind_null(m_stmt, index+1));
 	}
-	void bind_zero_blob(int col, int n)
+	void bind_zero_blob(int index, int n)
 	{
-		verify_error(sqlite3_bind_zeroblob(m_stmt, col, (int)n));
+		verify_error(sqlite3_bind_zeroblob(m_stmt, index+1, (int)n));
 	}
-	void bind(int col, qtl::null value)
+	void bind_param(int index, qtl::null)
 	{
-		verify_error(sqlite3_bind_null(m_stmt, col));
+		verify_error(sqlite3_bind_null(m_stmt, index+1));
 	}
-	void bind(int col)
+	void bind_param(int index, std::nullptr_t)
 	{
-		verify_error(sqlite3_bind_null(m_stmt, col));
+		verify_error(sqlite3_bind_null(m_stmt, index+1));
 	}
 	int get_parameter_count() const
 	{
@@ -146,12 +146,6 @@
 	int get_parameter_index(const char* param_name) const
 	{
 		return sqlite3_bind_parameter_index(m_stmt, param_name);
-	}
-
-	template<class Param>
-	void bind_param(size_t index, const Param& param)
-	{
-		bind((int)index+1, param);
 	}
 
 	template<class Type>
@@ -181,7 +175,7 @@
 	{
 		size_t col_length=get_column_length((int)index);
 		if(col_length>0)
-			strncpy(value, (const char*)sqlite3_column_text(m_stmt, (int)index), std::min(length, col_length));
+			strncpy(value, (const char*)sqlite3_column_text(m_stmt, (int)index), std::min(length, col_length+1));
 		else
 			memset(value, 0, length*sizeof(char));
 	}
@@ -189,7 +183,7 @@
 	{
 		size_t col_length=sqlite3_column_bytes16(m_stmt, (int)index);
 		if(col_length>0)
-			wcsncpy(value, (const wchar_t*)sqlite3_column_text16(m_stmt, (int)index), std::min(length, col_length));
+			wcsncpy(value, (const wchar_t*)sqlite3_column_text16(m_stmt, (int)index), std::min(length, col_length+1));
 		else
 			memset(value, 0, length*sizeof(wchar_t));
 	}
diff --git a/include/qtl_sqlite_pool.hpp b/include/qtl_sqlite_pool.hpp
index 17b9a86..39ada04 100644
--- a/include/qtl_sqlite_pool.hpp
+++ b/include/qtl_sqlite_pool.hpp
@@ -14,17 +14,20 @@
 {
 public:
 	database_pool() : m_flags(SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE) { }
-	virtual bool open_database(database& db) override
+	virtual database* new_database() throw() override
 	{
+		database* db=NULL;
 		try
 		{
-			db.open(m_filename.data(), m_flags);
-			return true;
+			db=new database;
+			db->open(m_filename.data(), m_flags);
 		}
 		catch (error& e)
 		{
-			return false;
+			delete db;
+			db=NULL;
 		}
+		return db;
 	}
 
 protected:
diff --git a/test/TestOdbc.cpp b/test/TestOdbc.cpp
new file mode 100644
index 0000000..58cc44a
--- /dev/null
+++ b/test/TestOdbc.cpp
@@ -0,0 +1,271 @@
+#include "stdafx.h"
+#include "TestOdbc.h"
+#include <fstream>
+#include <array>
+#include "md5.h"
+
+using namespace std;
+
+struct TestOdbcRecord
+{
+	uint32_t id;
+	char name[33];
+	qtl::odbc::timestamp create_time;
+
+	TestOdbcRecord()
+	{
+		memset(this, 0, sizeof(TestOdbcRecord));
+	}
+
+	void print() const
+	{
+		printf("ID=\"%d\", Name=\"%s\"\n",
+			id, name);
+	}
+};
+
+namespace qtl
+{
+	template<>
+	inline void bind_record<qtl::odbc::statement, TestOdbcRecord>(qtl::odbc::statement& command, TestOdbcRecord&& v)
+	{
+		qtl::bind_field(command, 0, v.id);
+		qtl::bind_field(command, 1, v.name);
+		qtl::bind_field(command, 2, v.create_time);
+	}
+}
+
+TestOdbc::TestOdbc() : m_db(m_env)
+{
+	m_db.open("DRIVER={SQL Server};SERVER=(local);UID=;PWD=;Trusted_Connection=yes;DATABASE=test");
+	cout<<"DBMS: "<<m_db.dbms_name()<<endl;
+	cout<<"SERVER: "<<m_db.server_name()<<endl;
+	cout<<"USER: "<<m_db.user_name()<<endl;
+	cout<<"DATABASE: "<<m_db.db_name()<<endl;
+
+	id=0;
+	TEST_ADD(TestOdbc::test_dual)
+	TEST_ADD(TestOdbc::test_clear)
+	TEST_ADD(TestOdbc::test_insert)
+	TEST_ADD(TestOdbc::test_select)
+	TEST_ADD(TestOdbc::test_update)
+	TEST_ADD(TestOdbc::test_insert2)
+	TEST_ADD(TestOdbc::test_iterator)
+	TEST_ADD(TestOdbc::test_insert_blob)
+	TEST_ADD(TestOdbc::test_select_blob)
+}
+
+void TestOdbc::test_dual()
+{
+	try
+	{
+		m_db.query("select 0, 'hello world';",
+			[](uint32_t i, const std::string& str) {
+				printf("0=\"%d\", 'hello world'=\"%s\"\n",
+					i, str.data());
+		});
+	}
+	catch(qtl::odbc::error& e)
+	{
+		if(!e) ASSERT_EXCEPTION(e);
+	}
+}
+
+void TestOdbc::test_select()
+{
+	try
+	{
+		m_db.query("select * from test where id=?", id, 
+			[](const qtl::indicator<uint32_t>& id, const std::string& name, const qtl::odbc::timestamp& create_time) {
+				printf("ID=\"%d\", Name=\"%s\"\n",
+					id.data, name.data());
+		});
+
+		m_db.query("select * from test where id=?", id,
+			[](const TestOdbcRecord& record) {
+				printf("ID=\"%d\", Name=\"%s\"\n",
+					record.id, record.name);
+		});
+
+		m_db.query("select * from test where id=?", id, 
+			&TestOdbcRecord::print);
+	}
+	catch(qtl::odbc::error& e)
+	{
+		ASSERT_EXCEPTION(e);
+	}
+}
+
+void TestOdbc::test_insert()
+{
+	try
+	{
+		m_db.execute("insert into test(Name, CreateTime) values(?, CURRENT_TIMESTAMP)",
+			"test_user");
+		m_db.query_first("SELECT @@IDENTITY", id);
+	}
+	catch(qtl::odbc::error& e)
+	{
+		ASSERT_EXCEPTION(e);
+	}
+	TEST_ASSERT_MSG(id>0, "insert failture.");
+}
+
+void TestOdbc::test_insert2()
+{
+	try
+	{
+		uint64_t affected=0;
+		qtl::odbc::statement stmt=m_db.open_command("insert into test(Name, CreateTime) values(?, CURRENT_TIMESTAMP)");
+		qtl::execute(stmt, &affected, "second_user", "third_user");
+		TEST_ASSERT_MSG(affected==2, "Cannot insert 2 records to table test.");
+	}
+	catch(qtl::odbc::error& e)
+	{
+		ASSERT_EXCEPTION(e);
+	}
+}
+
+void TestOdbc::test_update()
+{
+	try
+	{
+		m_db.execute_direct("update test set Name=? WHERE ID=?", NULL,
+			"other_user", id);
+	}
+	catch(qtl::odbc::error& e)
+	{
+		ASSERT_EXCEPTION(e);
+	}
+	TEST_ASSERT_MSG(id>0, "insert failture.");
+}
+
+void TestOdbc::test_clear()
+{
+	try
+	{
+		m_db.simple_execute("delete from test");
+	}
+	catch(qtl::odbc::error& e)
+	{
+		ASSERT_EXCEPTION(e);
+	}
+}
+
+void TestOdbc::test_iterator()
+{
+	try
+	{
+		cout<<"after insert all:"<<endl;
+		for(auto& record : m_db.result<TestOdbcRecord>("select ID, Name, CreateTime from test"))
+		{
+			printf("ID=\"%d\", Name=\"%s\"\n",
+				record.id, record.name);
+		}
+	}
+	catch(qtl::odbc::error& e)
+	{
+		ASSERT_EXCEPTION(e);
+	}
+}
+
+void TestOdbc::test_insert_blob()
+{
+	try
+	{
+#ifdef _WIN32
+		const char filename[]="C:\\windows\\explorer.exe";
+#else
+		const char filename[]="/bin/sh";
+#endif //_WIN32
+		fstream fs(filename, ios::binary|ios::in);
+		TEST_ASSERT_MSG(fs, "Cannot open test file.");
+		unsigned char md5[16]={0};
+		char md5_hex[33]={0};
+		get_md5(fs, md5);
+		cout<<"MD5 of file "<<filename<<": ";
+		print_hex(md5, sizeof(md5));
+		cout<<endl;
+
+		m_db.simple_execute("DELETE FROM test_blob");
+
+		fs.clear();
+		fs.seekg(0, ios::beg);
+		m_db.execute("INSERT INTO test_blob (Filename, [Content], MD5) values(?, ?, ?)",
+			make_tuple(filename, ref((istream&)fs), qtl::const_blob_data(md5, sizeof(md5))));
+		m_db.query_first("SELECT @@IDENTITY", id);
+	}
+	catch(qtl::odbc::error& e)
+	{
+		ASSERT_EXCEPTION(e);
+	}
+}
+
+void TestOdbc::test_select_blob()
+{
+	try
+	{
+		const char dest_file[]="explorer.exe";
+		fstream fs(dest_file, ios::binary|ios::in|ios::out|ios::trunc);
+		TEST_ASSERT_MSG(fs, "Cannot open test file.");
+		unsigned char md5[16]={0};
+		char md5_hex[33]={0};
+
+		fs.seekg(ios::beg);
+		std::string source_file;
+		m_db.query_first("SELECT Filename, MD5 , [Content]FROM test_blob WHERE id=?", make_tuple(id),
+			make_tuple(ref(source_file), qtl::blob_data(md5, sizeof(md5)), ref((ostream&)fs)));
+		fs.flush();
+		cout<<"MD5 of file "<<source_file<<" stored in database: ";
+		print_hex((const unsigned char*)md5, sizeof(md5));
+		fs.clear();
+		fs.seekg(0, ios::beg);
+		get_md5(fs, md5);
+		cout<<endl<<"MD5 of file "<<dest_file<<": ";
+		print_hex(md5, sizeof(md5));
+		cout<<endl;
+	}
+	catch(qtl::odbc::error& e)
+	{
+		ASSERT_EXCEPTION(e);
+	}
+}
+
+void TestOdbc::get_md5(std::istream& is, unsigned char* result)
+{
+	std::array<char, 64*1024> buffer;
+	MD5_CTX context;
+	MD5Init(&context);
+	while(!is.eof())
+	{
+		is.read(&buffer.front(), buffer.size());
+		MD5Update(&context, (unsigned char*)buffer.data(), (unsigned int)is.gcount());
+	}
+	MD5Final(result, &context);
+}
+
+void TestOdbc::print_hex(const unsigned char* data, size_t n)
+{
+	cout<<hex;
+	for(size_t i=0; i!=n; i++)
+		cout<<(data[i]&0xFF);
+}
+
+int main(int argc, char* argv[])
+{
+	Test::TextOutput output(Test::TextOutput::Verbose);
+
+	cout<<endl<<"Testing MSSQL(ODBC):"<<endl;
+
+	try
+	{
+		TestOdbc test_odbc; 
+		test_odbc.run(output);
+	}
+	catch(qtl::odbc::error& e)
+	{
+		cerr<<e.what()<<endl;
+	}
+	return 0;
+}
+
diff --git a/test/TestOdbc.h b/test/TestOdbc.h
new file mode 100644
index 0000000..ac5cf80
--- /dev/null
+++ b/test/TestOdbc.h
@@ -0,0 +1,39 @@
+#ifndef _TEST_ODBC_H_
+#define _TEST_ODBC_H_
+
+#include "TestSuite.h"
+#include "../include/qtl_odbc.hpp"
+
+namespace qtl
+{
+	namespace odbc
+	{
+		class database;
+	}
+}
+
+class TestOdbc : public TestSuite
+{
+public:
+	TestOdbc();
+
+private:
+	void test_dual();
+	void test_clear();
+	void test_insert();
+	void test_select();
+	void test_update();
+	void test_insert2();
+	void test_iterator();
+	void test_insert_blob();
+	void test_select_blob();
+
+private:
+	uint64_t id;
+	qtl::odbc::environment m_env;
+	qtl::odbc::database m_db;
+	void get_md5(std::istream& is, unsigned char* result);
+	static void print_hex(const unsigned char* data, size_t n);
+};
+
+#endif //_TEST_ODBC_H_
diff --git a/test/stdafx.h b/test/stdafx.h
index 48ad6c9..0f276fa 100644
--- a/test/stdafx.h
+++ b/test/stdafx.h
@@ -10,8 +10,8 @@
 #define NOMINMAX 1
 #endif //NOMINMAX
 
-#ifndef _WIN32_WINNT		// ����ʹ���ض��� Windows XP ����߰汾�Ĺ��ܡ�
-#define _WIN32_WINNT 0x0501	// ����ֵ����Ϊ��Ӧ��ֵ���������� Windows �������汾��
+#ifndef _WIN32_WINNT
+#define _WIN32_WINNT 0x0501
 #endif
 
 #include <WinSock2.h>
diff --git a/test/test_odbc.mak b/test/test_odbc.mak
new file mode 100644
index 0000000..b754a0a
--- /dev/null
+++ b/test/test_odbc.mak
@@ -0,0 +1,25 @@
+TARGET=test_odbc
+CC=g++
+PCH_HEADER=stdafx.h
+PCH=stdafx.h.gch
+OBJ=TestOdbc.o md5.o
+CFLAGS=-g -D_DEBUG -O2 -I/usr/local/include
+CXXFLAGS=-I../include -std=c++11
+LDFLAGS= -L/usr/local/lib -lcpptest -lodbc
+
+all : $(TARGET)
+
+$(PCH) : $(PCH_HEADER)
+	$(CC) $(CFLAGS) $(CXXFLAGS) -x c++-header -o $@ $<
+
+TestOdbc.o : TestOdbc.cpp TestOdbc.h $(PCH)
+	$(CC) -c $(CFLAGS) $(CXXFLAGS) -o $@ $< 
+	
+md5.o : md5.c md5.h
+	gcc -c $(CFLAGS) -o $@ $<
+
+$(TARGET) : $(OBJ)
+	libtool --tag=CXX --mode=link $(CC) $(LDFLAGS) -o $@ $^
+
+clean:
+	rm $(TARGET) $(PCH) $(OBJ) -f
diff --git a/test/vs/test.sln b/test/vs/test.sln
index c297dbf..f69c59d 100644
--- a/test/vs/test.sln
+++ b/test/vs/test.sln
@@ -5,6 +5,8 @@
 EndProject
 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "test_sqlite", "test_sqlite\test_sqlite.vcproj", "{8C793DE3-283E-460C-A876-29455F68CB45}"
 EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "test_odbc", "test_odbc\test_odbc.vcproj", "{86C60EAD-F5B4-482B-9CC2-92A77323E759}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Win32 = Debug|Win32
@@ -19,6 +21,10 @@
 		{8C793DE3-283E-460C-A876-29455F68CB45}.Debug|Win32.Build.0 = Debug|Win32
 		{8C793DE3-283E-460C-A876-29455F68CB45}.Release|Win32.ActiveCfg = Release|Win32
 		{8C793DE3-283E-460C-A876-29455F68CB45}.Release|Win32.Build.0 = Release|Win32
+		{86C60EAD-F5B4-482B-9CC2-92A77323E759}.Debug|Win32.ActiveCfg = Debug|Win32
+		{86C60EAD-F5B4-482B-9CC2-92A77323E759}.Debug|Win32.Build.0 = Debug|Win32
+		{86C60EAD-F5B4-482B-9CC2-92A77323E759}.Release|Win32.ActiveCfg = Release|Win32
+		{86C60EAD-F5B4-482B-9CC2-92A77323E759}.Release|Win32.Build.0 = Release|Win32
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
diff --git a/test/vs/test_odbc/test_odbc.vcproj b/test/vs/test_odbc/test_odbc.vcproj
new file mode 100644
index 0000000..314e9ba
--- /dev/null
+++ b/test/vs/test_odbc/test_odbc.vcproj
@@ -0,0 +1,259 @@
+<?xml version="1.0" encoding="gb2312"?>
+<VisualStudioProject
+	ProjectType="Visual C++"
+	Version="8.00"
+	Name="test_odbc"
+	ProjectGUID="{86C60EAD-F5B4-482B-9CC2-92A77323E759}"
+	RootNamespace="test_odbc"
+	Keyword="Win32Proj"
+	>
+	<Platforms>
+		<Platform
+			Name="Win32"
+		/>
+	</Platforms>
+	<ToolFiles>
+	</ToolFiles>
+	<Configurations>
+		<Configuration
+			Name="Debug|Win32"
+			OutputDirectory="$(SolutionDir)$(ConfigurationName)"
+			IntermediateDirectory="$(ConfigurationName)"
+			ConfigurationType="1"
+			CharacterSet="0"
+			>
+			<Tool
+				Name="VCPreBuildEventTool"
+			/>
+			<Tool
+				Name="VCCustomBuildTool"
+			/>
+			<Tool
+				Name="VCXMLDataGeneratorTool"
+			/>
+			<Tool
+				Name="VCWebServiceProxyGeneratorTool"
+			/>
+			<Tool
+				Name="VCMIDLTool"
+			/>
+			<Tool
+				Name="VCCLCompilerTool"
+				Optimization="0"
+				PreprocessorDefinitions="WIN32;_DEBUG;_CONSOLE"
+				MinimalRebuild="true"
+				BasicRuntimeChecks="3"
+				RuntimeLibrary="3"
+				UsePrecompiledHeader="2"
+				WarningLevel="3"
+				Detect64BitPortabilityProblems="false"
+				DebugInformationFormat="4"
+			/>
+			<Tool
+				Name="VCManagedResourceCompilerTool"
+			/>
+			<Tool
+				Name="VCResourceCompilerTool"
+			/>
+			<Tool
+				Name="VCPreLinkEventTool"
+			/>
+			<Tool
+				Name="VCLinkerTool"
+				AdditionalDependencies=" cpptest_debug.lib"
+				LinkIncremental="2"
+				GenerateDebugInformation="true"
+				SubSystem="1"
+				TargetMachine="1"
+			/>
+			<Tool
+				Name="VCALinkTool"
+			/>
+			<Tool
+				Name="VCManifestTool"
+			/>
+			<Tool
+				Name="VCXDCMakeTool"
+			/>
+			<Tool
+				Name="VCBscMakeTool"
+			/>
+			<Tool
+				Name="VCFxCopTool"
+			/>
+			<Tool
+				Name="VCAppVerifierTool"
+			/>
+			<Tool
+				Name="VCWebDeploymentTool"
+			/>
+			<Tool
+				Name="VCPostBuildEventTool"
+			/>
+		</Configuration>
+		<Configuration
+			Name="Release|Win32"
+			OutputDirectory="$(SolutionDir)$(ConfigurationName)"
+			IntermediateDirectory="$(ConfigurationName)"
+			ConfigurationType="1"
+			CharacterSet="0"
+			WholeProgramOptimization="1"
+			>
+			<Tool
+				Name="VCPreBuildEventTool"
+			/>
+			<Tool
+				Name="VCCustomBuildTool"
+			/>
+			<Tool
+				Name="VCXMLDataGeneratorTool"
+			/>
+			<Tool
+				Name="VCWebServiceProxyGeneratorTool"
+			/>
+			<Tool
+				Name="VCMIDLTool"
+			/>
+			<Tool
+				Name="VCCLCompilerTool"
+				PreprocessorDefinitions="WIN32;NDEBUG;_CONSOLE"
+				RuntimeLibrary="2"
+				UsePrecompiledHeader="2"
+				WarningLevel="3"
+				Detect64BitPortabilityProblems="false"
+				DebugInformationFormat="3"
+			/>
+			<Tool
+				Name="VCManagedResourceCompilerTool"
+			/>
+			<Tool
+				Name="VCResourceCompilerTool"
+			/>
+			<Tool
+				Name="VCPreLinkEventTool"
+			/>
+			<Tool
+				Name="VCLinkerTool"
+				AdditionalDependencies=" cpptest.lib"
+				LinkIncremental="1"
+				GenerateDebugInformation="true"
+				SubSystem="1"
+				OptimizeReferences="2"
+				EnableCOMDATFolding="2"
+				TargetMachine="1"
+			/>
+			<Tool
+				Name="VCALinkTool"
+			/>
+			<Tool
+				Name="VCManifestTool"
+			/>
+			<Tool
+				Name="VCXDCMakeTool"
+			/>
+			<Tool
+				Name="VCBscMakeTool"
+			/>
+			<Tool
+				Name="VCFxCopTool"
+			/>
+			<Tool
+				Name="VCAppVerifierTool"
+			/>
+			<Tool
+				Name="VCWebDeploymentTool"
+			/>
+			<Tool
+				Name="VCPostBuildEventTool"
+			/>
+		</Configuration>
+	</Configurations>
+	<References>
+	</References>
+	<Files>
+		<Filter
+			Name="Դ�ļ�"
+			Filter="cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx"
+			UniqueIdentifier="{4FC737F1-C7A5-4376-A066-2A32D752A2FF}"
+			>
+			<File
+				RelativePath="..\..\md5.c"
+				>
+				<FileConfiguration
+					Name="Debug|Win32"
+					>
+					<Tool
+						Name="VCCLCompilerTool"
+						UsePrecompiledHeader="0"
+					/>
+				</FileConfiguration>
+				<FileConfiguration
+					Name="Release|Win32"
+					>
+					<Tool
+						Name="VCCLCompilerTool"
+						UsePrecompiledHeader="0"
+					/>
+				</FileConfiguration>
+			</File>
+			<File
+				RelativePath="..\..\stdafx.cpp"
+				>
+				<FileConfiguration
+					Name="Debug|Win32"
+					>
+					<Tool
+						Name="VCCLCompilerTool"
+						UsePrecompiledHeader="1"
+					/>
+				</FileConfiguration>
+				<FileConfiguration
+					Name="Release|Win32"
+					>
+					<Tool
+						Name="VCCLCompilerTool"
+						UsePrecompiledHeader="1"
+					/>
+				</FileConfiguration>
+			</File>
+			<File
+				RelativePath="..\..\TestOdbc.cpp"
+				>
+			</File>
+		</Filter>
+		<Filter
+			Name="ͷ�ļ�"
+			Filter="h;hpp;hxx;hm;inl;inc;xsd"
+			UniqueIdentifier="{93995380-89BD-4b04-88EB-625FBE52EBFB}"
+			>
+			<File
+				RelativePath="..\..\md5.h"
+				>
+			</File>
+			<File
+				RelativePath="..\..\stdafx.h"
+				>
+			</File>
+			<File
+				RelativePath="..\..\TestOdbc.h"
+				>
+			</File>
+			<File
+				RelativePath="..\..\TestSuite.h"
+				>
+			</File>
+		</Filter>
+		<Filter
+			Name="��Դ�ļ�"
+			Filter="rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav"
+			UniqueIdentifier="{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}"
+			>
+		</Filter>
+		<File
+			RelativePath=".\ReadMe.txt"
+			>
+		</File>
+	</Files>
+	<Globals>
+	</Globals>
+</VisualStudioProject>

--
Gitblit v1.9.3