//! \file nanodbc.cpp Implementation details.
#ifndef DOXYGEN

// ASCII art banners are helpful for code editors with a minimap display.
// Generated with http://patorjk.com/software/taag/#p=display&v=0&f=Colossal

#include "nanodbc.h"

#include <algorithm>
#include <clocale>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <iomanip>
#include <map>
#include <sstream>

#ifndef __clang__
    #include <cstdint>
#endif

// User may redefine NANODBC_ASSERT macro in nanodbc.h
#ifndef NANODBC_ASSERT
#include <cassert>
#define NANODBC_ASSERT(expr) assert(expr)
#endif

#ifdef NANODBC_USE_BOOST_CONVERT
    #include <boost/locale/encoding_utf.hpp>
#else
    #include <codecvt>
#endif

#if defined(_MSC_VER) && _MSC_VER <= 1800
    // silence spurious Visual C++ warnings
    #pragma warning(disable:4244) // warning about integer conversion issues.
    #pragma warning(disable:4312) // warning about 64-bit portability issues.
    #pragma warning(disable:4996) // warning about snprintf() deprecated.
#endif

#ifdef __APPLE__
    // silence spurious OS X deprecation warnings
    #define MAC_OS_X_VERSION_MIN_REQUIRED MAC_OS_X_VERSION_10_6
#endif

#ifdef _WIN32
    // needs to be included above sql.h for windows
    #define NOMINMAX
    #include <windows.h>
#endif

#include <sql.h>
#include <sqlext.h>

// Default to ODBC version defined by NANODBC_ODBC_VERSION if provided.
#ifndef NANODBC_ODBC_VERSION
    #ifdef SQL_OV_ODBC3_80
        // Otherwise, use ODBC v3.8 if it's available...
        #define NANODBC_ODBC_VERSION SQL_OV_ODBC3_80
    #else
        // or fallback to ODBC v3.x.
        #define NANODBC_ODBC_VERSION SQL_OV_ODBC3
    #endif
#endif

// 888     888          d8b                       888
// 888     888          Y8P                       888
// 888     888                                    888
// 888     888 88888b.  888  .d8888b .d88b.   .d88888  .d88b.
// 888     888 888 "88b 888 d88P"   d88""88b d88" 888 d8P  Y8b
// 888     888 888  888 888 888     888  888 888  888 88888888
// Y88b. .d88P 888  888 888 Y88b.   Y88..88P Y88b 888 Y8b.
//  "Y88888P"  888  888 888  "Y8888P "Y88P"   "Y88888  "Y8888
// MARK: Unicode -

#if defined(_MSC_VER)
    #ifdef NANODBC_USE_UNICODE
        #define NANODBC_TEXT(s) L ## s
        #define NANODBC_SNPRINTF std::swprintf
        #define NANODBC_STRFTIME std::wcsftime
        #define NANODBC_STRLEN std::wcslen
        #define NANADBC_STRNCMP std::wcsncmp
        #define NANODBC_UNICODE(f) f ## W
        #define NANODBC_SQLCHAR SQLWCHAR
    #else
        #define NANODBC_TEXT(s) s
        #define NANODBC_SNPRINTF(buffer, count, format, ...) _snprintf_s(buffer, count, _TRUNCATE, format, __VA_ARGS__)
        #define NANODBC_STRFTIME std::strftime
        #define NANODBC_STRLEN std::strlen
        #define NANADBC_STRNCMP std::strncmp
        #define NANODBC_UNICODE(f) f
        #define NANODBC_SQLCHAR SQLCHAR

        // Disable unicode in sqlucode.h on Windows when NANODBC_USE_UNICODE
        // is not defined. This is required because unicode is enabled by
        // default on many Windows systems.
        #define SQL_NOUNICODEMAP
    #endif
#else
    #ifdef NANODBC_USE_UNICODE
        #define NANODBC_TEXT(s) L ## s
        #define NANODBC_SNPRINTF std::swprintf
        #define NANODBC_STRFTIME std::wcsftime
        #define NANODBC_STRLEN std::wcslen
        #define NANADBC_STRNCMP std::wcsncmp
        #define NANODBC_UNICODE(f) f ## W
        #define NANODBC_SQLCHAR SQLWCHAR
    #else
        #define NANODBC_TEXT(s) s
        #define NANODBC_SNPRINTF std::snprintf
        #define NANODBC_STRFTIME std::strftime
        #define NANODBC_STRLEN std::strlen
        #define NANADBC_STRNCMP std::strncmp
        #define NANODBC_UNICODE(f) f
        #define NANODBC_SQLCHAR SQLCHAR
    #endif
#endif

//  .d88888b.  8888888b.  888888b.    .d8888b.       888b     d888
// d88P" "Y88b 888  "Y88b 888  "88b  d88P  Y88b      8888b   d8888
// 888     888 888    888 888  .88P  888    888      88888b.d88888
// 888     888 888    888 8888888K.  888             888Y88888P888  8888b.   .d8888b 888d888 .d88b.  .d8888b
// 888     888 888    888 888  "Y88b 888             888 Y888P 888     "88b d88P"    888P"  d88""88b 88K
// 888     888 888    888 888    888 888    888      888  Y8P  888 .d888888 888      888    888  888 "Y8888b.
// Y88b. .d88P 888  .d88P 888   d88P Y88b  d88P      888   "   888 888  888 Y88b.    888    Y88..88P      X88
//  "Y88888P"  8888888P"  8888888P"   "Y8888P"       888       888 "Y888888  "Y8888P 888     "Y88P"   88888P'
// MARK: ODBC Macros -

#define NANODBC_STRINGIZE_I(text) #text
#define NANODBC_STRINGIZE(text) NANODBC_STRINGIZE_I(text)

// By making all calls to ODBC functions through this macro, we can easily get
// runtime debugging information of which ODBC functions are being called,
// in what order, and with what parameters by defining NANODBC_ODBC_API_DEBUG.
#ifdef NANODBC_ODBC_API_DEBUG
    #include <iostream>
    #define NANODBC_CALL_RC(FUNC, RC, ...)                                    \
        do                                                                    \
        {                                                                     \
            std::cerr << __FILE__ ":" NANODBC_STRINGIZE(__LINE__) " "         \
                NANODBC_STRINGIZE(FUNC) "(" #__VA_ARGS__ ")" << std::endl;    \
            RC = FUNC(__VA_ARGS__);                                           \
        } while(false)                                                        \
        /**/
    #define NANODBC_CALL(FUNC, ...)                                           \
        do                                                                    \
        {                                                                     \
            std::cerr << __FILE__ ":" NANODBC_STRINGIZE(__LINE__) " "         \
                NANODBC_STRINGIZE(FUNC) "(" #__VA_ARGS__ ")" << std::endl;    \
            FUNC(__VA_ARGS__);                                                \
        } while(false)                                                        \
        /**/
#else
    #define NANODBC_CALL_RC(FUNC, RC, ...) RC = FUNC(__VA_ARGS__)
    #define NANODBC_CALL(FUNC, ...) FUNC(__VA_ARGS__)
#endif

// 8888888888                                      888    888                        888 888 d8b
// 888                                             888    888                        888 888 Y8P
// 888                                             888    888                        888 888
// 8888888    888d888 888d888 .d88b.  888d888      8888888888  8888b.  88888b.   .d88888 888 888 88888b.   .d88b.
// 888        888P"   888P"  d88""88b 888P"        888    888     "88b 888 "88b d88" 888 888 888 888 "88b d88P"88b
// 888        888     888    888  888 888          888    888 .d888888 888  888 888  888 888 888 888  888 888  888
// 888        888     888    Y88..88P 888          888    888 888  888 888  888 Y88b 888 888 888 888  888 Y88b 888
// 8888888888 888     888     "Y88P"  888          888    888 "Y888888 888  888  "Y88888 888 888 888  888  "Y88888
//                                                                                                             888
//                                                                                                        Y8b d88P
//                                                                                                         "Y88P"
// MARK: Error Handling -

namespace
{
    #ifdef NANODBC_ODBC_API_DEBUG
        inline nanodbc::string_type return_code(RETCODE rc)
        {
            switch(rc)
            {
                case SQL_SUCCESS: return NANODBC_UNICODE("SQL_SUCCESS");
                case SQL_SUCCESS_WITH_INFO: return NANODBC_UNICODE("SQL_SUCCESS_WITH_INFO");
                case SQL_ERROR: return NANODBC_UNICODE("SQL_ERROR");
                case SQL_INVALID_HANDLE: return NANODBC_UNICODE("SQL_INVALID_HANDLE");
                case SQL_NO_DATA: return NANODBC_UNICODE("SQL_NO_DATA");
                case SQL_NEED_DATA: return NANODBC_UNICODE("SQL_NEED_DATA");
                case SQL_STILL_EXECUTING: return NANODBC_UNICODE("SQL_STILL_EXECUTING");
            }
            NANODBC_ASSERT(0);
            return "unknown"; // should never make it here
        }
    #endif

    // Easy way to check if a return code signifies success.
    inline bool success(RETCODE rc)
    {
        #ifdef NANODBC_ODBC_API_DEBUG
            std::cerr << "<-- rc: " << return_code(rc) << " | ";
        #endif
        return rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO;
    }

    // Returns the array size.
    template<typename T, std::size_t N>
    inline std::size_t arrlen(T(&)[N])
    {
        return N;
    }

    // Operates like strlen() on a character array.
    template<typename T, std::size_t N>
    inline std::size_t strarrlen(T(&a)[N])
    {
        const T* s = &a[0];
        std::size_t i = 0;
        while(*s++ && i < N) i++;
        return i;
    }

    inline void convert(const std::wstring& in, std::string& out)
    {
        #ifdef NANODBC_USE_BOOST_CONVERT
            using boost::locale::conv::utf_to_utf;
            out = utf_to_utf<char>(in.c_str(), in.c_str() + in.size());
        #else
            out = std::wstring_convert<std::codecvt_utf8<wchar_t>>().to_bytes(in);
        #endif
    }

    #ifdef NANODBC_USE_UNICODE
        inline void convert(const std::string& in, std::wstring& out)
        {
            #ifdef NANODBC_USE_BOOST_CONVERT
                using boost::locale::conv::utf_to_utf;
                out = utf_to_utf<wchar_t>(in.c_str(), in.c_str() + in.size());
            #else
                out = std::wstring_convert<std::codecvt_utf8<wchar_t>>().from_bytes(in);
            #endif
        }

        inline void convert(const std::wstring& in, std::wstring & out)
        {
            out = in;
        }
    #else
        inline void convert(const std::string& in, std::string& out)
        {
            out = in;
        }
    #endif

    // Attempts to get the most recent ODBC error as a string.
    // Always returns std::string, even in unicode mode.
    inline std::string recent_error(
        SQLHANDLE handle
        , SQLSMALLINT handle_type
        , long &native
        , std::string &state)
    {
        nanodbc::string_type result;
        std::string rvalue;
        std::vector<NANODBC_SQLCHAR> sql_message(SQL_MAX_MESSAGE_LENGTH);
        sql_message[0] = '\0';

        SQLINTEGER i = 1;
        SQLINTEGER native_error;
        SQLSMALLINT total_bytes;
        NANODBC_SQLCHAR sql_state[6];
        RETCODE rc;

        do
        {
            NANODBC_CALL_RC(
                NANODBC_UNICODE(SQLGetDiagRec)
                , rc
                , handle_type
                , handle
                , (SQLSMALLINT)i
                , sql_state
                , &native_error
                , 0
                , 0
                , &total_bytes);

            if(success(rc) && total_bytes > 0)
                sql_message.resize(total_bytes + 1);

            if(rc == SQL_NO_DATA)
                break;

            NANODBC_CALL_RC(
                NANODBC_UNICODE(SQLGetDiagRec)
                , rc
                , handle_type
                , handle
                , (SQLSMALLINT)i
                , sql_state
                , &native_error
                , sql_message.data()
                , (SQLSMALLINT)sql_message.size()
                , &total_bytes);

            if(!success(rc))
            {
                convert(result, rvalue);
                return rvalue;
            }

            if(!result.empty())
                result += ' ';

            result += nanodbc::string_type(sql_message.begin(), sql_message.end());
            i++;

            // NOTE: unixODBC using PostgreSQL and SQLite drivers crash if you call SQLGetDiagRec()
            // more than once. So as a (terrible but the best possible) workaround just exit
            // this loop early on non-Windows systems.
            #ifndef _MSC_VER
                break;
            #endif
        } while(rc != SQL_NO_DATA);

        convert(result, rvalue);
        state = std::string(&sql_state[0], &sql_state[arrlen(sql_state) - 1]);
        native = native_error;
        std::string status = state;
        status += ": ";
        status += rvalue;

        // some drivers insert \0 into error messages for unknown reasons
        using std::replace;
        replace(status.begin(), status.end(), '\0', ' ');

        return status;
    }
} // namespace

namespace nanodbc
{
    type_incompatible_error::type_incompatible_error()
    : std::runtime_error("type incompatible") { }

    const char* type_incompatible_error::what() const NANODBC_NOEXCEPT
    {
        return std::runtime_error::what();
    }

    null_access_error::null_access_error()
    : std::runtime_error("null access") { }

    const char* null_access_error::what() const NANODBC_NOEXCEPT
    {
        return std::runtime_error::what();
    }

    index_range_error::index_range_error()
    : std::runtime_error("index out of range") { }

    const char* index_range_error::what() const NANODBC_NOEXCEPT
    {
        return std::runtime_error::what();
    }

    programming_error::programming_error(const std::string& info)
    : std::runtime_error(info.c_str()) { }

    const char* programming_error::what() const NANODBC_NOEXCEPT
    {
        return std::runtime_error::what();
    }

    database_error::database_error(void* handle, short handle_type, const std::string& info)
    : std::runtime_error(info), native_error(0), sql_state("00000")
    {
        message = std::string(std::runtime_error::what()) + recent_error(handle, handle_type, native_error, sql_state);
    }

    const char* database_error::what() const NANODBC_NOEXCEPT
    {
        return message.c_str();
    }

    const long database_error::native() const NANODBC_NOEXCEPT
    {
        return native_error;
    }

    const std::string database_error::state() const NANODBC_NOEXCEPT
    {
        return sql_state;
    }

} // namespace nanodbc

// Throwing exceptions using NANODBC_THROW_DATABASE_ERROR enables file name
// and line numbers to be inserted into the error message. Useful for debugging.
#define NANODBC_THROW_DATABASE_ERROR(handle, handle_type)                     \
    throw nanodbc::database_error(                                            \
        handle                                                                \
        , handle_type                                                         \
        , __FILE__ ":" NANODBC_STRINGIZE(__LINE__) ": ")                      \
    /**/

// 8888888b.           888             d8b 888
// 888  "Y88b          888             Y8P 888
// 888    888          888                 888
// 888    888  .d88b.  888888  8888b.  888 888 .d8888b
// 888    888 d8P  Y8b 888        "88b 888 888 88K
// 888    888 88888888 888    .d888888 888 888 "Y8888b.
// 888  .d88P Y8b.     Y88b.  888  888 888 888      X88
// 8888888P"   "Y8888   "Y888 "Y888888 888 888  88888P'
// MARK: Details -

namespace
{
    using namespace std; // if int64_t is in std namespace (in c++11)

    // A utility for calculating the ctype from the given type T.
    // I essentially create a lookup table based on the MSDN ODBC documentation.
    // See http://msdn.microsoft.com/en-us/library/windows/desktop/ms714556(v=vs.85).aspx
    template<class T>
    struct sql_ctype { };

    template<>
    struct sql_ctype<nanodbc::string_type::value_type>
    {
        #ifdef NANODBC_USE_UNICODE
            static const SQLSMALLINT value = SQL_C_WCHAR;
        #else
            static const SQLSMALLINT value = SQL_C_CHAR;
        #endif
    };

    template<>
    struct sql_ctype<short>
    {
        static const SQLSMALLINT value = SQL_C_SSHORT;
    };

    template<>
    struct sql_ctype<unsigned short>
    {
        static const SQLSMALLINT value = SQL_C_USHORT;
    };

    template<>
    struct sql_ctype<int32_t>
    {
        static const SQLSMALLINT value = SQL_C_SLONG;
    };

    template<>
    struct sql_ctype<uint32_t>
    {
        static const SQLSMALLINT value = SQL_C_ULONG;
    };

    template<>
    struct sql_ctype<int64_t>
    {
        static const SQLSMALLINT value = SQL_C_SBIGINT;
    };

    template<>
    struct sql_ctype<uint64_t>
    {
        static const SQLSMALLINT value = SQL_C_UBIGINT;
    };

    template<>
    struct sql_ctype<float>
    {
        static const SQLSMALLINT value = SQL_C_FLOAT;
    };

    template<>
    struct sql_ctype<double>
    {
        static const SQLSMALLINT value = SQL_C_DOUBLE;
    };

    template<>
    struct sql_ctype<nanodbc::string_type>
    {
        #ifdef NANODBC_USE_UNICODE
            static const SQLSMALLINT value = SQL_C_WCHAR;
        #else
            static const SQLSMALLINT value = SQL_C_CHAR;
        #endif
    };

    template<>
    struct sql_ctype<nanodbc::date>
    {
        static const SQLSMALLINT value = SQL_C_DATE;
    };

    template<>
    struct sql_ctype<nanodbc::timestamp>
    {
        static const SQLSMALLINT value = SQL_C_TIMESTAMP;
    };

    // Encapsulates resources needed for column binding.
    class bound_column
    {
    public:
        bound_column(const bound_column& rhs) =delete;
        bound_column& operator=(bound_column rhs) =delete;

        bound_column()
        : name_()
        , column_(0)
        , sqltype_(0)
        , sqlsize_(0)
        , scale_(0)
        , ctype_(0)
        , clen_(0)
        , blob_(false)
        , cbdata_(0)
        , pdata_(0)
        {

        }

        ~bound_column()
        {
            delete[] cbdata_;
            delete[] pdata_;
        }

    public:
        nanodbc::string_type name_;
        short column_;
        SQLSMALLINT sqltype_;
        SQLULEN sqlsize_;
        SQLSMALLINT scale_;
        SQLSMALLINT ctype_;
        SQLULEN clen_;
        bool blob_;
        nanodbc::null_type* cbdata_;
        char* pdata_;
    };

    // Allocates the native ODBC handles.
    inline void allocate_handle(SQLHENV& env, SQLHDBC& conn)
    {
        RETCODE rc;
        NANODBC_CALL_RC(
            SQLAllocHandle
            , rc
            , SQL_HANDLE_ENV
            , SQL_NULL_HANDLE
            , &env);
        if(!success(rc))
            NANODBC_THROW_DATABASE_ERROR(env, SQL_HANDLE_ENV);

        try
        {
            NANODBC_CALL_RC(
                SQLSetEnvAttr
                , rc
                , env
                , SQL_ATTR_ODBC_VERSION
                , (SQLPOINTER)NANODBC_ODBC_VERSION
                , SQL_IS_UINTEGER);
            if(!success(rc))
                NANODBC_THROW_DATABASE_ERROR(env, SQL_HANDLE_ENV);

            NANODBC_CALL_RC(
                SQLAllocHandle
                , rc
                , SQL_HANDLE_DBC
                , env
                , &conn);
            if(!success(rc))
                NANODBC_THROW_DATABASE_ERROR(env, SQL_HANDLE_ENV);
        }
        catch(...)
        {
            NANODBC_CALL(
                SQLFreeHandle
                , SQL_HANDLE_ENV
                , env);
            throw;
        }
    }
} // namespace

//  .d8888b.                                               888    d8b                             8888888                        888
// d88P  Y88b                                              888    Y8P                               888                          888
// 888    888                                              888                                      888                          888
// 888         .d88b.  88888b.  88888b.   .d88b.   .d8888b 888888 888  .d88b.  88888b.              888   88888b.d88b.  88888b.  888
// 888        d88""88b 888 "88b 888 "88b d8P  Y8b d88P"    888    888 d88""88b 888 "88b             888   888 "888 "88b 888 "88b 888
// 888    888 888  888 888  888 888  888 88888888 888      888    888 888  888 888  888             888   888  888  888 888  888 888
// Y88b  d88P Y88..88P 888  888 888  888 Y8b.     Y88b.    Y88b.  888 Y88..88P 888  888             888   888  888  888 888 d88P 888
//  "Y8888P"   "Y88P"  888  888 888  888  "Y8888   "Y8888P  "Y888 888  "Y88P"  888  888           8888888 888  888  888 88888P"  888
//                                                                                                                      888
//                                                                                                                      888
//                                                                                                                      888
// MARK: Connection Impl -

namespace nanodbc
{

class connection::connection_impl
{
public:
    connection_impl(const connection_impl&) =delete;
    connection_impl& operator=(const connection_impl&) =delete;

    connection_impl()
    : env_(0)
    , conn_(0)
    , connected_(false)
    , transactions_(0)
    , rollback_(false)
    {
        allocate_handle(env_, conn_);
    }

    connection_impl(
        const string_type& dsn
        , const string_type& user
        , const string_type& pass
        , long timeout)
    : env_(0)
    , conn_(0)
    , connected_(false)
    , transactions_(0)
    , rollback_(false)
    {
        allocate_handle(env_, conn_);
        try
        {
            connect(dsn, user, pass, timeout);
        }
        catch(...)
        {
            NANODBC_CALL(
                SQLFreeHandle
                , SQL_HANDLE_DBC
                , conn_);
            NANODBC_CALL(
                SQLFreeHandle
                , SQL_HANDLE_ENV
                , env_);
            throw;
        }
    }

    connection_impl(const string_type& connection_string, long timeout)
    : env_(0)
    , conn_(0)
    , connected_(false)
    , transactions_(0)
    , rollback_(false)
    {
        allocate_handle(env_, conn_);
        try
        {
            connect(connection_string, timeout);
        }
        catch(...)
        {
            NANODBC_CALL(
                SQLFreeHandle
                , SQL_HANDLE_DBC
                , conn_);
            NANODBC_CALL(
                SQLFreeHandle
                , SQL_HANDLE_ENV
                , env_);
            throw;
        }
    }

    ~connection_impl() NANODBC_NOEXCEPT
    {
        try
        {
            disconnect();
        }
        catch(...)
        {
            // ignore exceptions thrown during disconnect
        }
        NANODBC_CALL(
            SQLFreeHandle
            , SQL_HANDLE_DBC
            , conn_);
        NANODBC_CALL(
            SQLFreeHandle
            , SQL_HANDLE_ENV
            , env_);
    }

#ifdef SQL_ATTR_ASYNC_DBC_EVENT
    void enable_async(void* event_handle)
    {
        RETCODE rc;
        NANODBC_CALL_RC(
            SQLSetConnectAttr
            , rc
            , conn_
            , SQL_ATTR_ASYNC_DBC_FUNCTIONS_ENABLE
            , (SQLPOINTER)SQL_ASYNC_DBC_ENABLE_ON
            , SQL_IS_INTEGER);
        if(!success(rc))
            NANODBC_THROW_DATABASE_ERROR(conn_, SQL_HANDLE_DBC);

        NANODBC_CALL_RC(
            SQLSetConnectAttr
            , rc
            , conn_
            , SQL_ATTR_ASYNC_DBC_EVENT
            , event_handle
            , SQL_IS_POINTER);
        if(!success(rc))
            NANODBC_THROW_DATABASE_ERROR(conn_, SQL_HANDLE_DBC);
    }

    void async_complete()
    {
        RETCODE rc, arc;
        NANODBC_CALL_RC(
            SQLCompleteAsync
            , rc
            , SQL_HANDLE_DBC
            , conn_
            , &arc);
        if(!success(rc) || !success(arc))
            NANODBC_THROW_DATABASE_ERROR(conn_, SQL_HANDLE_DBC);

        NANODBC_CALL_RC(
            SQLSetConnectAttr
            , rc
            , conn_
            , SQL_ATTR_ASYNC_ENABLE
            , (SQLPOINTER)SQL_ASYNC_ENABLE_OFF
            , SQL_IS_INTEGER);
        if(!success(rc))
            NANODBC_THROW_DATABASE_ERROR(conn_, SQL_HANDLE_DBC);

        connected_ = success(rc);
    }
#endif // SQL_ATTR_ASYNC_DBC_EVENT

    void connect(
        const string_type& dsn
        , const string_type& user
        , const string_type& pass
        , long timeout
        , void* event_handle = NULL)
    {
        disconnect();

        RETCODE rc;
        NANODBC_CALL_RC(
            SQLFreeHandle
            , rc
            , SQL_HANDLE_DBC
            , conn_);
        if(!success(rc))
            NANODBC_THROW_DATABASE_ERROR(conn_, SQL_HANDLE_DBC);

        NANODBC_CALL_RC(
            SQLAllocHandle
            , rc
            , SQL_HANDLE_DBC
            , env_
            , &conn_);
        if(!success(rc))
            NANODBC_THROW_DATABASE_ERROR(env_, SQL_HANDLE_ENV);

        NANODBC_CALL_RC(
            SQLSetConnectAttr
            , rc
            , conn_
            , SQL_LOGIN_TIMEOUT
            , (SQLPOINTER)(std::intptr_t)timeout
            , 0);
        if(!success(rc))
            NANODBC_THROW_DATABASE_ERROR(conn_, SQL_HANDLE_DBC);

        #ifdef SQL_ATTR_ASYNC_DBC_EVENT
            if(event_handle != NULL)
                enable_async(event_handle);
        #endif

        NANODBC_CALL_RC(
            NANODBC_UNICODE(SQLConnect)
            , rc
            , conn_
            , (NANODBC_SQLCHAR*)dsn.c_str(), SQL_NTS
            , !user.empty() ? (NANODBC_SQLCHAR*)user.c_str() : 0, SQL_NTS
            , !pass.empty() ? (NANODBC_SQLCHAR*)pass.c_str() : 0, SQL_NTS);
        if(!success(rc) && (event_handle == NULL || rc != SQL_STILL_EXECUTING))
            NANODBC_THROW_DATABASE_ERROR(conn_, SQL_HANDLE_DBC);

        connected_ = success(rc);
    }

    void connect(const string_type& connection_string, long timeout, void* event_handle = NULL)
    {
        disconnect();

        RETCODE rc;
        NANODBC_CALL_RC(
            SQLFreeHandle
            , rc
            , SQL_HANDLE_DBC
            , conn_);
        if(!success(rc))
            NANODBC_THROW_DATABASE_ERROR(conn_, SQL_HANDLE_DBC);

        NANODBC_CALL_RC(
            SQLAllocHandle
            , rc
            , SQL_HANDLE_DBC
            , env_
            , &conn_);
        if(!success(rc))
            NANODBC_THROW_DATABASE_ERROR(env_, SQL_HANDLE_ENV);

        NANODBC_CALL_RC(
            SQLSetConnectAttr
            , rc
            , conn_
            , SQL_LOGIN_TIMEOUT
            , (SQLPOINTER)(std::intptr_t)timeout
            , 0);
        if(!success(rc))
            NANODBC_THROW_DATABASE_ERROR(conn_, SQL_HANDLE_DBC);

        #ifdef SQL_ATTR_ASYNC_DBC_EVENT
            if(event_handle != NULL)
                enable_async(event_handle);
        #endif

        NANODBC_SQLCHAR dsn[1024];
        SQLSMALLINT dsn_size = 0;
        NANODBC_CALL_RC(
            NANODBC_UNICODE(SQLDriverConnect)
            , rc
            , conn_
            , 0
            , (NANODBC_SQLCHAR*)connection_string.c_str(), SQL_NTS
            , dsn
            , sizeof(dsn) / sizeof(NANODBC_SQLCHAR)
            , &dsn_size
            , SQL_DRIVER_NOPROMPT);
        if(!success(rc) && (event_handle == NULL || rc != SQL_STILL_EXECUTING))
            NANODBC_THROW_DATABASE_ERROR(conn_, SQL_HANDLE_DBC);

        connected_ = success(rc);
    }

    bool connected() const
    {
        return connected_;
    }

    void disconnect()
    {
        if(connected())
        {
            RETCODE rc;
            NANODBC_CALL_RC(
                SQLDisconnect
                , rc
                , conn_);
            if(!success(rc))
                NANODBC_THROW_DATABASE_ERROR(conn_, SQL_HANDLE_DBC);
        }
        connected_ = false;
    }

    std::size_t transactions() const
    {
        return transactions_;
    }

    void* native_dbc_handle() const
    {
        return conn_;
    }

    void* native_env_handle() const
    {
        return env_;
    }

    string_type driver_name() const
    {
        NANODBC_SQLCHAR name[1024];
        SQLSMALLINT length;
        RETCODE rc;
        NANODBC_CALL_RC(
            NANODBC_UNICODE(SQLGetInfo)
            , rc
            , conn_
            , SQL_DRIVER_NAME
            , name
            , sizeof(name) / sizeof(NANODBC_SQLCHAR)
            , &length);
        if(!success(rc))
            NANODBC_THROW_DATABASE_ERROR(conn_, SQL_HANDLE_DBC);
        return string_type(&name[0], &name[strarrlen(name)]);
    }

    string_type database_name() const
    {
        // FIXME: Allocate buffer of dynamic size as drivers do not agree on universal size
        // MySQL driver limits MAX_NAME_LEN=255
        // PostgreSQL driver MAX_INFO_STIRNG=128
        // MFC CDatabase allocates buffer dynamically.
        NANODBC_SQLCHAR name[255] = { 0 };
        SQLSMALLINT length(0);
        RETCODE rc;
        NANODBC_CALL_RC(
            NANODBC_UNICODE(SQLGetInfo)
            , rc
            , conn_
            , SQL_DATABASE_NAME
            , name
            , sizeof(name) / sizeof(NANODBC_SQLCHAR)
            , &length);
        if(!success(rc))
            NANODBC_THROW_DATABASE_ERROR(conn_, SQL_HANDLE_DBC);
        return string_type(&name[0], &name[strarrlen(name)]);
    }

    string_type catalog_name() const
    {
        NANODBC_SQLCHAR name[SQL_MAX_OPTION_STRING_LENGTH] = { 0 };
        SQLINTEGER length(0);
        RETCODE rc;
        NANODBC_CALL_RC(
            NANODBC_UNICODE(SQLGetConnectAttr)
            , rc
            , conn_
            , SQL_ATTR_CURRENT_CATALOG
            , name
            , sizeof(name) / sizeof(NANODBC_SQLCHAR)
            , &length);
        if(!success(rc))
            NANODBC_THROW_DATABASE_ERROR(conn_, SQL_HANDLE_DBC);
        return string_type(&name[0], &name[strarrlen(name)]);
    }


    std::size_t ref_transaction()
    {
        return --transactions_;
    }

    std::size_t unref_transaction()
    {
        return ++transactions_;
    }

    bool rollback() const
    {
        return rollback_;
    }

    void rollback(bool onoff)
    {
        rollback_ = onoff;
    }

private:
    HENV env_;
    HDBC conn_;
    bool connected_;
    std::size_t transactions_;
    bool rollback_; // if true, this connection is marked for eventual transaction rollback
};

} // namespace nanodbc

// 88888888888                                                  888    d8b                             8888888                        888
//     888                                                      888    Y8P                               888                          888
//     888                                                      888                                      888                          888
//     888  888d888 8888b.  88888b.  .d8888b   8888b.   .d8888b 888888 888  .d88b.  88888b.              888   88888b.d88b.  88888b.  888
//     888  888P"      "88b 888 "88b 88K          "88b d88P"    888    888 d88""88b 888 "88b             888   888 "888 "88b 888 "88b 888
//     888  888    .d888888 888  888 "Y8888b. .d888888 888      888    888 888  888 888  888             888   888  888  888 888  888 888
//     888  888    888  888 888  888      X88 888  888 Y88b.    Y88b.  888 Y88..88P 888  888             888   888  888  888 888 d88P 888
//     888  888    "Y888888 888  888  88888P' "Y888888  "Y8888P  "Y888 888  "Y88P"  888  888           8888888 888  888  888 88888P"  888
//                                                                                                                           888
//                                                                                                                           888
//                                                                                                                           888
// MARK: Transaction Impl -

namespace nanodbc
{

class transaction::transaction_impl
{
public:
    transaction_impl(const transaction_impl&) =delete;
    transaction_impl& operator=(const transaction_impl&) =delete;

    transaction_impl(const class connection& conn)
    : conn_(conn)
    , committed_(false)
    {
        if(conn_.transactions() == 0 && conn_.connected())
        {
            RETCODE rc;
            NANODBC_CALL_RC(
                SQLSetConnectAttr
                , rc
                , conn_.native_dbc_handle()
                , SQL_ATTR_AUTOCOMMIT
                , (SQLPOINTER)SQL_AUTOCOMMIT_OFF
                , SQL_IS_UINTEGER);
            if(!success(rc))
                NANODBC_THROW_DATABASE_ERROR(conn_.native_dbc_handle(), SQL_HANDLE_DBC);
        }
        conn_.ref_transaction();
    }

    ~transaction_impl() NANODBC_NOEXCEPT
    {
        if(!committed_)
        {
            conn_.rollback(true);
            conn_.unref_transaction();
        }

        if(conn_.transactions() == 0 && conn_.connected())
        {
            if(conn_.rollback())
            {
                NANODBC_CALL(
                    SQLEndTran
                    , SQL_HANDLE_DBC
                    , conn_.native_dbc_handle()
                    , SQL_ROLLBACK);
                conn_.rollback(false);
            }

            NANODBC_CALL(
                SQLSetConnectAttr
                , conn_.native_dbc_handle()
                , SQL_ATTR_AUTOCOMMIT
                , (SQLPOINTER)SQL_AUTOCOMMIT_ON
                , SQL_IS_UINTEGER);
        }
    }

    void commit()
    {
        if(committed_)
            return;
        committed_ = true;
        if(conn_.unref_transaction() == 0 && conn_.connected())
        {
            RETCODE rc;
            NANODBC_CALL_RC(
                SQLEndTran
                , rc
                , SQL_HANDLE_DBC
                , conn_.native_dbc_handle()
                , SQL_COMMIT);
            if(!success(rc))
                NANODBC_THROW_DATABASE_ERROR(conn_.native_dbc_handle(), SQL_HANDLE_DBC);
        }
    }

    void rollback() NANODBC_NOEXCEPT
    {
        if(committed_)
            return;
        conn_.rollback(true);
    }

    class connection& connection()
    {
        return conn_;
    }

    const class connection& connection() const
    {
        return conn_;
    }

private:
    class connection conn_;
    bool committed_;
};

} // namespace nanodbc

//  .d8888b.  888             888                                            888              8888888                        888
// d88P  Y88b 888             888                                            888                888                          888
// Y88b.      888             888                                            888                888                          888
//  "Y888b.   888888  8888b.  888888 .d88b.  88888b.d88b.   .d88b.  88888b.  888888             888   88888b.d88b.  88888b.  888
//     "Y88b. 888        "88b 888   d8P  Y8b 888 "888 "88b d8P  Y8b 888 "88b 888                888   888 "888 "88b 888 "88b 888
//       "888 888    .d888888 888   88888888 888  888  888 88888888 888  888 888                888   888  888  888 888  888 888
// Y88b  d88P Y88b.  888  888 Y88b. Y8b.     888  888  888 Y8b.     888  888 Y88b.              888   888  888  888 888 d88P 888
//  "Y8888P"   "Y888 "Y888888  "Y888 "Y8888  888  888  888  "Y8888  888  888  "Y888           8888888 888  888  888 88888P"  888
//                                                                                                                  888
//                                                                                                                  888
//                                                                                                                  888
// MARK: Statement Impl -

namespace nanodbc
{

class statement::statement_impl
{
public:
    statement_impl(const statement_impl&) =delete;
    statement_impl& operator=(const statement_impl&) =delete;

    statement_impl()
    : stmt_(0)
    , open_(false)
    , conn_()
    , bind_len_or_null_()
    {

    }

    statement_impl(class connection& conn)
    : stmt_(0)
    , open_(false)
    , conn_()
    , bind_len_or_null_()
    {
        open(conn);
    }

    statement_impl(class connection& conn, const string_type& query, long timeout)
    : stmt_(0)
    , open_(false)
    , conn_()
    , bind_len_or_null_()
    {
        prepare(conn, query, timeout);
    }

    ~statement_impl() NANODBC_NOEXCEPT
    {
        if(open() && connected())
        {
            NANODBC_CALL(
                SQLCancel
                , stmt_);
            reset_parameters();
            NANODBC_CALL(
                SQLFreeHandle
                , SQL_HANDLE_STMT
                , stmt_);
        }
    }

    void open(class connection& conn)
    {
        close();
        RETCODE rc;
        NANODBC_CALL_RC(
            SQLAllocHandle
            , rc
            , SQL_HANDLE_STMT
            , conn.native_dbc_handle()
            , &stmt_);
        open_ = success(rc);
        if(!open_)
            NANODBC_THROW_DATABASE_ERROR(stmt_, SQL_HANDLE_STMT);
        conn_ = conn;
    }

    bool open() const
    {
        return open_;
    }

    bool connected() const
    {
        return conn_.connected();
    }

    const class connection& connection() const
    {
        return conn_;
    }

    class connection& connection()
    {
        return conn_;
    }

    void* native_statement_handle() const
    {
        return stmt_;
    }

    void close()
    {
        if(open() && connected())
        {
            RETCODE rc;
            NANODBC_CALL_RC(
                SQLCancel
                , rc
                , stmt_);
            if(!success(rc))
                NANODBC_THROW_DATABASE_ERROR(stmt_, SQL_HANDLE_STMT);

            reset_parameters();

            NANODBC_CALL_RC(
                SQLFreeHandle
                , rc
                , SQL_HANDLE_STMT
                , stmt_);
            if(!success(rc))
                NANODBC_THROW_DATABASE_ERROR(stmt_, SQL_HANDLE_STMT);
        }

        open_ = false;
        stmt_ = 0;
    }

    void cancel()
    {
        RETCODE rc;
        NANODBC_CALL_RC(
            SQLCancel
            , rc
            , stmt_);
        if(!success(rc))
            NANODBC_THROW_DATABASE_ERROR(stmt_, SQL_HANDLE_STMT);
    }

    void prepare(class connection& conn, const string_type& query, long timeout)
    {
        open(conn);
        prepare(query, timeout);
    }

    void prepare(const string_type& query, long timeout)
    {
        if(!open())
            throw programming_error("statement has no associated open connection");

        RETCODE rc;
        NANODBC_CALL_RC(
            NANODBC_UNICODE(SQLPrepare)
            , rc
            , stmt_
            , (NANODBC_SQLCHAR*)query.c_str()
            , (SQLINTEGER)query.size());
        if(!success(rc))
            NANODBC_THROW_DATABASE_ERROR(stmt_, SQL_HANDLE_STMT);

        this->timeout(timeout);
    }

    void timeout(long timeout)
    {
        RETCODE rc;
        NANODBC_CALL_RC(
            SQLSetStmtAttr
            , rc
            , stmt_
            , SQL_ATTR_QUERY_TIMEOUT
            , (SQLPOINTER)(std::intptr_t)timeout,
             0);

        // some drivers don't support timeout for statements,
        // so only raise the error if a non-default timeout was requested.
        if(!success(rc) && (timeout != 0))
            NANODBC_THROW_DATABASE_ERROR(stmt_, SQL_HANDLE_STMT);
    }

#if defined(SQL_ATTR_ASYNC_STMT_EVENT) && defined(SQL_API_SQLCOMPLETEASYNC)
    void enable_async(void* event_handle)
    {
        RETCODE rc;
        NANODBC_CALL_RC(
            SQLSetStmtAttr
            , rc
            , stmt_
            , SQL_ATTR_ASYNC_ENABLE
            , (SQLPOINTER)SQL_ASYNC_ENABLE_ON
            , SQL_IS_INTEGER);
        if(!success(rc))
            NANODBC_THROW_DATABASE_ERROR(stmt_, SQL_HANDLE_STMT);

        NANODBC_CALL_RC(
            SQLSetStmtAttr
            , rc
            , stmt_
            , SQL_ATTR_ASYNC_STMT_EVENT
            , event_handle
            , SQL_IS_POINTER);
        if(!success(rc))
            NANODBC_THROW_DATABASE_ERROR(stmt_, SQL_HANDLE_STMT);
    }

    void async_execute_direct(
        class connection& conn
        , void* event_handle
        , const string_type& query
        , long batch_operations
        , long timeout
        , statement& statement)
    {
        RETCODE rc = just_execute_direct(
            conn
            , query
            , batch_operations
            , timeout
            , statement
            , event_handle);

        if(rc != SQL_STILL_EXECUTING)
            NANODBC_THROW_DATABASE_ERROR(stmt_, SQL_HANDLE_STMT);
    }

    result async_complete(long batch_operations, statement& statement)
    {
        RETCODE rc, arc;
        NANODBC_CALL_RC(
            SQLCompleteAsync
            , rc
            , SQL_HANDLE_STMT
            , stmt_
            , &arc);
        if(!success(rc) || !success(arc))
            NANODBC_THROW_DATABASE_ERROR(stmt_, SQL_HANDLE_STMT);

        NANODBC_CALL_RC(
            SQLSetStmtAttr
            , rc
            , stmt_
            , SQL_ATTR_ASYNC_ENABLE
            , (SQLPOINTER)SQL_ASYNC_ENABLE_OFF
            , SQL_IS_INTEGER);
        if(!success(rc))
            NANODBC_THROW_DATABASE_ERROR(stmt_, SQL_HANDLE_STMT);

        return result(statement, batch_operations);
    }
#endif // SQL_ATTR_ASYNC_STMT_EVENT && SQL_API_SQLCOMPLETEASYNC

    result execute_direct(
        class connection& conn
        , const string_type& query
        , long batch_operations
        , long timeout
        , statement& statement)
    {
        #ifdef NANODBC_HANDLE_NODATA_BUG
            const RETCODE rc = just_execute_direct(conn, query, batch_operations, timeout, statement);
            if(rc == SQL_NO_DATA)
                return result();
        #else
            just_execute_direct(conn, query, batch_operations, timeout, statement);
        #endif
        return result(statement, batch_operations);
    }

    RETCODE just_execute_direct(
        class connection& conn
        , const string_type& query
        , long batch_operations
        , long timeout
        , statement& /*statement*/
        , void* event_handle = NULL)
    {
        open(conn);

        #if defined(SQL_ATTR_ASYNC_STMT_EVENT) && defined(SQL_API_SQLCOMPLETEASYNC)
            if(event_handle != NULL)
                enable_async(event_handle);
        #endif

        RETCODE rc;
        NANODBC_CALL_RC(
            SQLSetStmtAttr
            , rc
            , stmt_
            , SQL_ATTR_PARAMSET_SIZE
            , (SQLPOINTER)(std::intptr_t)batch_operations
            , 0);
        if(!success(rc))
            NANODBC_THROW_DATABASE_ERROR(stmt_, SQL_HANDLE_STMT);

        this->timeout(timeout);

        NANODBC_CALL_RC(
            NANODBC_UNICODE(SQLExecDirect)
            , rc
            , stmt_
            , (NANODBC_SQLCHAR*)query.c_str()
            , SQL_NTS);
        if(!success(rc) && rc != SQL_NO_DATA && rc != SQL_STILL_EXECUTING)
            NANODBC_THROW_DATABASE_ERROR(stmt_, SQL_HANDLE_STMT);

        return rc;
    }

    result execute(long batch_operations, long timeout, statement& statement)
    {
        #ifdef NANODBC_HANDLE_NODATA_BUG
            const RETCODE rc = just_execute(batch_operations, timeout, statement);
            if(rc == SQL_NO_DATA)
                return result();
        #else
            just_execute(batch_operations, timeout, statement);
        #endif
        return result(statement, batch_operations);
    }

    RETCODE just_execute(long batch_operations, long timeout, statement& /*statement*/)
    {
        RETCODE rc;

        if(open())
        {
            // The ODBC cursor must be closed before subsequent executions, as described
            // here http://msdn.microsoft.com/en-us/library/windows/desktop/ms713584%28v=vs.85%29.aspx
            //
            // However, we don't necessarily want to call SQLCloseCursor() because that
            // will cause an invalid cursor state in the case that no cursor is currently open.
            // A better solution is to use SQLFreeStmt() with the SQL_CLOSE option, which has
            // the same effect without the undesired limitations.
            NANODBC_CALL_RC(
                SQLFreeStmt
                , rc
                , stmt_
                , SQL_CLOSE);
            if(!success(rc))
                NANODBC_THROW_DATABASE_ERROR(stmt_, SQL_HANDLE_STMT);
        }

        NANODBC_CALL_RC(
            SQLSetStmtAttr
            , rc
            , stmt_
            , SQL_ATTR_PARAMSET_SIZE
            , (SQLPOINTER)(std::intptr_t)batch_operations
            , 0);
        if(!success(rc) && rc != SQL_NO_DATA)
            NANODBC_THROW_DATABASE_ERROR(stmt_, SQL_HANDLE_STMT);

        this->timeout(timeout);

        NANODBC_CALL_RC(
            SQLExecute
            , rc
            , stmt_);
        if(!success(rc) && rc != SQL_NO_DATA)
            NANODBC_THROW_DATABASE_ERROR(stmt_, SQL_HANDLE_STMT);

        return rc;
    }

    result procedure_columns(
        const string_type& catalog
        , const string_type& schema
        , const string_type& procedure
        , const string_type& column
        , statement& statement)
    {
        if(!open())
            throw programming_error("statement has no associated open connection");

        RETCODE rc;
        NANODBC_CALL_RC(
            NANODBC_UNICODE(SQLProcedureColumns)
            , rc
            , stmt_
            , (NANODBC_SQLCHAR*)(catalog.empty() ? NULL : catalog.c_str())
            , (catalog.empty() ? 0 : SQL_NTS)
            , (NANODBC_SQLCHAR*)(schema.empty() ? NULL : schema.c_str())
            , (schema.empty() ? 0 : SQL_NTS)
            , (NANODBC_SQLCHAR*)procedure.c_str()
            , SQL_NTS
            , (NANODBC_SQLCHAR*)(column.empty() ? NULL : column.c_str())
            , (column.empty() ? 0 : SQL_NTS));

        if(!success(rc))
            NANODBC_THROW_DATABASE_ERROR(stmt_, SQL_HANDLE_STMT);

        return result(statement, 1);
    }

    long affected_rows() const
    {
        SQLLEN rows;
        RETCODE rc;
        NANODBC_CALL_RC(
            SQLRowCount
            , rc
            , stmt_
            , &rows);
        if(!success(rc))
            NANODBC_THROW_DATABASE_ERROR(stmt_, SQL_HANDLE_STMT);
        NANODBC_ASSERT(rows <= static_cast<SQLLEN>(std::numeric_limits<long>::max()));
        return static_cast<long>(rows);
    }

    short columns() const
    {
        SQLSMALLINT cols;
        RETCODE rc;
        NANODBC_CALL_RC(
            SQLNumResultCols
            , rc
            , stmt_
            , &cols);
        if(!success(rc))
            NANODBC_THROW_DATABASE_ERROR(stmt_, SQL_HANDLE_STMT);
        return cols;
    }

    void reset_parameters() NANODBC_NOEXCEPT
    {
        NANODBC_CALL(
            SQLFreeStmt
            , stmt_
            , SQL_RESET_PARAMS);
    }

    unsigned long parameter_size(short param) const
    {
        RETCODE rc;
        SQLSMALLINT data_type;
        SQLULEN parameter_size;
        NANODBC_CALL_RC(
            SQLDescribeParam
            , rc
            , stmt_
            , param + 1
            , &data_type
            , &parameter_size
            , 0
            , 0);
        if(!success(rc))
            NANODBC_THROW_DATABASE_ERROR(stmt_, SQL_HANDLE_STMT);
        NANODBC_ASSERT(parameter_size <= static_cast<SQLULEN>(std::numeric_limits<unsigned long>::max()));
        return static_cast<unsigned long>(parameter_size);
    }

    static SQLSMALLINT param_type_from_direction(param_direction direction)
    {
        switch(direction)
        {
            case PARAM_IN:
                return SQL_PARAM_INPUT;
                break;
            case PARAM_OUT:
                return SQL_PARAM_OUTPUT;
                break;
            case PARAM_INOUT:
                return SQL_PARAM_INPUT_OUTPUT;
                break;
            case PARAM_RETURN:
                return SQL_PARAM_OUTPUT;
                break;
            default:
                NANODBC_ASSERT(false);
                throw programming_error("unrecognized param_direction value");
        }
    }

    // initializes bind_len_or_null_ and gets information for bind
    void prepare_bind(
        short param
        , std::size_t elements
        , param_direction direction
        , SQLSMALLINT& data_type
        , SQLSMALLINT& param_type
        , SQLULEN& parameter_size
        , SQLSMALLINT& scale)
    {
        RETCODE rc;
        NANODBC_CALL_RC(
            SQLDescribeParam
            , rc
            , stmt_
            , param + 1
            , &data_type
            , &parameter_size
            , &scale
            , 0);
        if(!success(rc))
            NANODBC_THROW_DATABASE_ERROR(stmt_, SQL_HANDLE_STMT);

        param_type = param_type_from_direction(direction);

        if(!bind_len_or_null_.count(param))
            bind_len_or_null_[param] = std::vector<null_type>();
        std::vector<null_type>().swap(bind_len_or_null_[param]);

        // ODBC weirdness: this must be at least 8 elements in size
        const std::size_t indicator_size = elements > 8 ? elements : 8;

        bind_len_or_null_[param].reserve(indicator_size);
        bind_len_or_null_[param].assign(indicator_size, SQL_NULL_DATA);
    }

    // calls actual ODBC bind parameter function
    template<class T>
    void bind_parameter(
        short param
        , const T* data
        , std::size_t /*elements*/
        , SQLSMALLINT data_type
        , SQLSMALLINT param_type
        , SQLULEN parameter_size
        , SQLSMALLINT scale)
    {
        RETCODE rc;
        NANODBC_CALL_RC(
            SQLBindParameter
            , rc
            , stmt_ // handle
            , param + 1 // parameter number
            , param_type // input or output type
            , sql_ctype<T>::value // value type
            , data_type // parameter type
            , parameter_size // column size ignored for many types, but needed for strings
            , scale // decimal digits
            , (SQLPOINTER)data // parameter value
            , parameter_size // buffer length
            , bind_len_or_null_[param].data());

        if(!success(rc))
            NANODBC_THROW_DATABASE_ERROR(stmt_, SQL_HANDLE_STMT);
    }

    // handles a single value (possibly a single string value), or multiple non-string values
    template<class T>
    void bind(short param, const T* values, std::size_t elements, param_direction direction);

    // handles multiple string values
    void bind_strings(
        short param
        , const string_type::value_type* values
        , std::size_t /*length*/
        , std::size_t elements
        , param_direction direction)
    {
        bind(param, values, elements, direction);
    }

    // handles multiple null values
    void bind_null(short param, std::size_t elements)
    {
        SQLSMALLINT data_type;
        SQLSMALLINT param_type;
        SQLULEN parameter_size;
        SQLSMALLINT scale;
        prepare_bind(param, elements, PARAM_IN, data_type, param_type, parameter_size, scale);

        RETCODE rc;
        NANODBC_CALL_RC(
            SQLBindParameter
            , rc
            , stmt_
            , param + 1
            , param_type
            , SQL_C_CHAR
            , data_type
            , parameter_size // column size ignored for many types, but needed for strings
            , 0
            , (SQLPOINTER)0 // null value
            , 0 // parameter_size
            , bind_len_or_null_[param].data());
        if(!success(rc))
            NANODBC_THROW_DATABASE_ERROR(stmt_, SQL_HANDLE_STMT);
    }

    // comparator for null sentry values
    template<class T>
    bool equals(const T& lhs, const T& rhs)
    {
        return lhs == rhs;
    }

    // handles multiple non-string values with a null sentry
    template<class T>
    void bind(
        short param
        , const T* values
        , std::size_t elements
        , const bool* nulls
        , const T* null_sentry
        , param_direction direction);

    // handles multiple string values
    void bind_strings(
        short param
        , const string_type::value_type* values
        , std::size_t length
        , std::size_t elements
        , const bool* nulls
        , const string_type::value_type* null_sentry
        , param_direction direction);

private:
    HSTMT stmt_;
    bool open_;
    class connection conn_;
    std::map<short, std::vector<null_type> > bind_len_or_null_;
};

// Supports code like: query.bind(0, std_string.c_str())
// In this case, we need to pass NULL to the final parameter of SQLBindParameter().
template<>
void statement::statement_impl::bind_parameter<string_type::value_type>(
    short param
    , const string_type::value_type* data
    , std::size_t elements
    , SQLSMALLINT data_type
    , SQLSMALLINT param_type
    , SQLULEN parameter_size
    , SQLSMALLINT scale)
{
    RETCODE rc;
    NANODBC_CALL_RC(
        SQLBindParameter
        , rc
        , stmt_ // handle
        , param + 1 // parameter number
        , param_type // input or output type
        , sql_ctype<string_type::value_type>::value // value type
        , data_type // parameter type
        , parameter_size // column size ignored for many types, but needed for strings
        , scale // decimal digits
        , (SQLPOINTER)data // parameter value
        , parameter_size // buffer length
        , (elements <= 1 ? NULL : bind_len_or_null_[param].data()));

    if(!success(rc))
        NANODBC_THROW_DATABASE_ERROR(stmt_, SQL_HANDLE_STMT);
}

template<class T>
void statement::statement_impl::bind(
    short param
    , const T* values
    , std::size_t elements
    , param_direction direction)
{
    SQLSMALLINT data_type;
    SQLSMALLINT param_type;
    SQLULEN parameter_size;
    SQLSMALLINT scale;
    prepare_bind(param, elements, direction, data_type, param_type, parameter_size, scale);

    for(std::size_t i = 0; i < elements; ++i)
        bind_len_or_null_[param][i] = parameter_size;

    bind_parameter(param, values, elements, data_type, param_type, parameter_size, scale);
}

template<class T>
void statement::statement_impl::bind(
    short param
    , const T* values
    , std::size_t elements
    , const bool* nulls
    , const T* null_sentry
    , param_direction direction)
{
    SQLSMALLINT data_type;
    SQLSMALLINT param_type;
    SQLULEN parameter_size;
    SQLSMALLINT scale;
    prepare_bind(param, elements, direction, data_type, param_type, parameter_size, scale);

    for(std::size_t i = 0; i < elements; ++i)
        if((null_sentry && !equals(values[i], *null_sentry)) || (nulls && !nulls[i]))
            bind_len_or_null_[param][i] = parameter_size;

    bind_parameter(param, values, elements, data_type, param_type, parameter_size, scale);
}

void statement::statement_impl::bind_strings(
    short param
    , const string_type::value_type* values
    , std::size_t length
    , std::size_t elements
    , const bool* nulls
    , const string_type::value_type* null_sentry
    , param_direction direction)
{
    SQLSMALLINT data_type;
    SQLSMALLINT param_type;
    SQLULEN parameter_size;
    SQLSMALLINT scale;
    prepare_bind(param, elements, direction, data_type, param_type, parameter_size, scale);

    if(null_sentry)
    {
        const string_type rhs(null_sentry);
        for(std::size_t i = 0; i < elements; ++i)
        {
            const string_type lhs(values + i * length, values + (i + 1) * length);
            if(NANADBC_STRNCMP(lhs.c_str(), rhs.c_str(), length))
                bind_len_or_null_[param][i] = parameter_size;
        }
    }
    else if(nulls)
    {
        for(std::size_t i = 0; i < elements; ++i)
        {
            if(!nulls[i])
                bind_len_or_null_[param][i] = SQL_NTS; // null terminated
        }
    }

    bind_parameter(param, values, elements, data_type, param_type, parameter_size, scale);
}

template<>
bool statement::statement_impl::equals(const date& lhs, const date& rhs)
{
    return lhs.year == rhs.year
        && lhs.month == rhs.month
        && lhs.day == rhs.day;
}

template<>
bool statement::statement_impl::equals(const timestamp& lhs, const timestamp& rhs)
{
    return lhs.year == rhs.year
        && lhs.month == rhs.month
        && lhs.day == rhs.day
        && lhs.hour == rhs.hour
        && lhs.min == rhs.min
        && lhs.sec == rhs.sec
        && lhs.fract == rhs.fract;
}

} // namespace nanodbc

// 8888888b.                            888 888              8888888                        888
// 888   Y88b                           888 888                888                          888
// 888    888                           888 888                888                          888
// 888   d88P .d88b.  .d8888b  888  888 888 888888             888   88888b.d88b.  88888b.  888
// 8888888P" d8P  Y8b 88K      888  888 888 888                888   888 "888 "88b 888 "88b 888
// 888 T88b  88888888 "Y8888b. 888  888 888 888                888   888  888  888 888  888 888
// 888  T88b Y8b.          X88 Y88b 888 888 Y88b.              888   888  888  888 888 d88P 888
// 888   T88b "Y8888   88888P'  "Y88888 888  "Y888           8888888 888  888  888 88888P"  888
//                                                                                 888
//                                                                                 888
//                                                                                 888
// MARK: Result Impl -

namespace nanodbc
{

class result::result_impl
{
public:
    result_impl(const result_impl&) =delete;
    result_impl& operator=(const result_impl&) =delete;

    result_impl(statement stmt, long rowset_size)
    : stmt_(stmt)
    , rowset_size_(rowset_size)
    , row_count_(0)
    , bound_columns_(0)
    , bound_columns_size_(0)
    , rowset_position_(0)
    , bound_columns_by_name_()
    , at_end_(false)
    {
        RETCODE rc;
        NANODBC_CALL_RC(
            SQLSetStmtAttr
            , rc
            , stmt_.native_statement_handle()
            , SQL_ATTR_ROW_ARRAY_SIZE
            , (SQLPOINTER)(std::intptr_t)rowset_size_
            , 0);
        if(!success(rc))
            NANODBC_THROW_DATABASE_ERROR(stmt_.native_statement_handle(), SQL_HANDLE_STMT);

        NANODBC_CALL_RC(
            SQLSetStmtAttr
            , rc
            , stmt_.native_statement_handle()
            , SQL_ATTR_ROWS_FETCHED_PTR
            , &row_count_
            , 0);
        if(!success(rc))
            NANODBC_THROW_DATABASE_ERROR(stmt_.native_statement_handle(), SQL_HANDLE_STMT);

        auto_bind();
    }

    ~result_impl() NANODBC_NOEXCEPT
    {
        cleanup_bound_columns();
    }

    void* native_statement_handle() const
    {
        return stmt_.native_statement_handle();
    }

    long rowset_size() const
    {
        return rowset_size_;
    }

    long affected_rows() const
    {
        return stmt_.affected_rows();
    }

    long rows() const NANODBC_NOEXCEPT
    {
        NANODBC_ASSERT(row_count_ <= static_cast<SQLULEN>(std::numeric_limits<long>::max()));
        return static_cast<long>(row_count_);
    }

    short columns() const
    {
        return stmt_.columns();
    }

    bool first()
    {
        rowset_position_ = 0;
        return fetch(0, SQL_FETCH_FIRST);
    }

    bool last()
    {
        rowset_position_ = 0;
        return fetch(0, SQL_FETCH_LAST);
    }

    bool next()
    {
        if(rows() && ++rowset_position_ < rowset_size_)
            return rowset_position_ < rows();
        rowset_position_ = 0;
        return fetch(0, SQL_FETCH_NEXT);
    }

    bool prior()
    {
        if(rows() && --rowset_position_ >= 0)
            return true;
        rowset_position_ = 0;
        return fetch(0, SQL_FETCH_PRIOR);
    }

    bool move(long row)
    {
        rowset_position_ = 0;
        return fetch(row, SQL_FETCH_ABSOLUTE);
    }

    bool skip(long rows)
    {
        rowset_position_ += rows;
        if(this->rows() && rowset_position_ < rowset_size_)
            return rowset_position_ < this->rows();
        rowset_position_ = 0;
        return fetch(rows, SQL_FETCH_RELATIVE);
    }

    unsigned long position() const
    {
        SQLULEN pos = 0; // necessary to initialize to 0
        RETCODE rc;
        NANODBC_CALL_RC(
            SQLGetStmtAttr
            , rc
            , stmt_.native_statement_handle()
            , SQL_ATTR_ROW_NUMBER
            , &pos
            , SQL_IS_UINTEGER
            , 0);
        if(!success(rc))
            NANODBC_THROW_DATABASE_ERROR(stmt_.native_statement_handle(), SQL_HANDLE_STMT);


        // MSDN (https://msdn.microsoft.com/en-us/library/ms712631.aspx):
        // If the number of the current row cannot be determined or
        // there is no current row, the driver returns 0.
        // Otherwise, valid row number is returned, starting at 1.
        //
        // NOTE: We try to address incorrect implementation in some drivers (e.g. SQLite ODBC)
        // which instead of 0 return SQL_ROW_NUMBER_UNKNOWN(-2) .
        if (pos == 0 || pos == static_cast<SQLULEN>(SQL_ROW_NUMBER_UNKNOWN))
            return 0;

        NANODBC_ASSERT(pos <= static_cast<SQLULEN>(std::numeric_limits<unsigned long>::max()));
        return static_cast<unsigned long>(pos) + rowset_position_;
    }

    bool end() const NANODBC_NOEXCEPT
    {
        if(at_end_)
            return true;
        SQLULEN pos = 0; // necessary to initialize to 0
        RETCODE rc;
        NANODBC_CALL_RC(
            SQLGetStmtAttr
            , rc
            , stmt_.native_statement_handle()
            , SQL_ATTR_ROW_NUMBER
            , &pos
            , SQL_IS_UINTEGER
            , 0);
        return (!success(rc) || rows() < 0 || pos - 1 > static_cast<unsigned long>(rows()));
    }

    bool is_null(short column) const
    {
        if(column >= bound_columns_size_)
            throw index_range_error();
        bound_column& col = bound_columns_[column];
        if(rowset_position_ >= rows())
            throw index_range_error();
        return col.cbdata_[rowset_position_] == SQL_NULL_DATA;
    }

    bool is_null(const string_type& column_name) const
    {
        const short column = this->column(column_name);
        return is_null(column);
    }

    string_type column_name(short column) const
    {
        if(column >= bound_columns_size_)
            throw index_range_error();
        return bound_columns_[column].name_;
    }

    long column_size(short column) const
    {
        if(column >= bound_columns_size_)
            throw index_range_error();
        bound_column& col = bound_columns_[column];
        NANODBC_ASSERT(col.sqlsize_ <= static_cast<SQLULEN>(std::numeric_limits<long>::max()));
        return static_cast<long>(col.sqlsize_);
    }

    short column(const string_type& column_name) const
    {
        typedef std::map<string_type, bound_column*>::const_iterator iter;
        iter i = bound_columns_by_name_.find(column_name);
        if(i == bound_columns_by_name_.end())
            throw index_range_error();
        return i->second->column_;
    }

    int column_datatype(short column) const
    {
        if(column >= bound_columns_size_)
            throw index_range_error();
        bound_column& col = bound_columns_[column];
        return col.sqltype_;
    }

    int column_datatype(const string_type& column_name) const
    {
        const short column = this->column(column_name);
        bound_column& col = bound_columns_[column];
        return col.sqltype_;
    }

    int column_c_datatype(short column) const
    {
        if(column >= bound_columns_size_)
            throw index_range_error();
        bound_column& col = bound_columns_[column];
        return col.ctype_;
    }

    int column_c_datatype(const string_type& column_name) const
    {
        const short column = this->column(column_name);
        bound_column& col = bound_columns_[column];
        return col.ctype_;
    }

    bool next_result()
    {
        RETCODE rc;
        NANODBC_CALL_RC(
            SQLMoreResults
            , rc
            , stmt_.native_statement_handle());
        if(rc == SQL_NO_DATA)
            return false;
        if(!success(rc))
            NANODBC_THROW_DATABASE_ERROR(stmt_.native_statement_handle(), SQL_HANDLE_STMT);
        auto_bind();
        return true;
    }

    template<class T>
    void get_ref(short column, T& result) const
    {
        if(column >= bound_columns_size_)
            throw index_range_error();
        if(is_null(column))
            throw null_access_error();
        get_ref_impl<T>(column, result);
    }

    template<class T>
    void get_ref(short column, const T& fallback, T& result) const
    {
        if(column >= bound_columns_size_)
            throw index_range_error();
        if(is_null(column))
        {
            result = fallback;
            return;
        }
        get_ref_impl<T>(column, result);
    }

    template<class T>
    void get_ref(const string_type& column_name, T& result) const
    {
        const short column = this->column(column_name);
        if(is_null(column))
            throw null_access_error();
        get_ref_impl<T>(column, result);
    }

    template<class T>
    void get_ref(const string_type& column_name, const T& fallback, T& result) const
    {
        const short column = this->column(column_name);
        if(is_null(column))
        {
            result = fallback;
            return;
        }
        get_ref_impl<T>(column, result);
    }

    template<class T>
    T get(short column) const
    {
        T result;
        get_ref(column, result);
        return result;
    }

    template<class T>
    T get(short column, const T& fallback) const
    {
        T result;
        get_ref(column, fallback, result);
        return result;
    }

    template<class T>
    T get(const string_type& column_name) const
    {
        T result;
        get_ref(column_name, result);
        return result;
    }

    template<class T>
    T get(const string_type& column_name, const T& fallback) const
    {
        T result;
        get_ref(column_name, fallback, result);
        return result;
    }

private:
    template<class T>
    void get_ref_impl(short column, T& result) const;

    void before_move() NANODBC_NOEXCEPT
    {
        for(short i = 0; i < bound_columns_size_; ++i)
        {
            bound_column& col = bound_columns_[i];
            for(long j = 0; j < rowset_size_; ++j)
                col.cbdata_[j] = 0;
            if(col.blob_ && col.pdata_)
                release_bound_resources(i);
        }
    }

    void release_bound_resources(short column) NANODBC_NOEXCEPT
    {
        NANODBC_ASSERT(column < bound_columns_size_);
        bound_column& col = bound_columns_[column];
        delete[] col.pdata_;
        col.pdata_ = 0;
        col.clen_ = 0;
    }

    void cleanup_bound_columns() NANODBC_NOEXCEPT
    {
        before_move();
        delete[] bound_columns_;
        bound_columns_ = NULL;
        bound_columns_size_ = 0;
        bound_columns_by_name_.clear();
    }

    bool fetch(long rows, SQLUSMALLINT orientation)
    {
        before_move();
        RETCODE rc;
        NANODBC_CALL_RC(
            SQLFetchScroll
            , rc
            , stmt_.native_statement_handle()
            , orientation
            , rows);
        if(rc == SQL_NO_DATA)
        {
            at_end_ = true;
            return false;
        }
        if(!success(rc))
            NANODBC_THROW_DATABASE_ERROR(stmt_.native_statement_handle(), SQL_HANDLE_STMT);
        return true;
    }

    void auto_bind()
    {
        cleanup_bound_columns();

        const short n_columns = columns();
        if(n_columns < 1)
            return;

        NANODBC_ASSERT(!bound_columns_);
        NANODBC_ASSERT(!bound_columns_size_);
        bound_columns_ = new bound_column[n_columns];
        bound_columns_size_ = n_columns;

        RETCODE rc;
        NANODBC_SQLCHAR column_name[1024];
        SQLSMALLINT sqltype, scale, nullable, len;
        SQLULEN sqlsize;

        for(SQLSMALLINT i = 0; i < n_columns; ++i)
        {
            NANODBC_CALL_RC(
                NANODBC_UNICODE(SQLDescribeCol)
                , rc
                , stmt_.native_statement_handle()
                , i + 1
                , (NANODBC_SQLCHAR*)column_name
                , sizeof(column_name)/sizeof(NANODBC_SQLCHAR)
                , &len
                , &sqltype
                , &sqlsize
                , &scale
                , &nullable);
            if(!success(rc))
                NANODBC_THROW_DATABASE_ERROR(stmt_.native_statement_handle(), SQL_HANDLE_STMT);

            // Adjust the sqlsize parameter in case of "unlimited" data (varchar(max), nvarchar(max)).
            bool is_blob = false;

            if(sqlsize == 0)
            {
                switch (sqltype)
                {
                    case SQL_VARCHAR:
                    case SQL_WVARCHAR:
                    {
                        // Divide in half, due to sqlsize being 32-bit in Win32 (and 64-bit in x64)
                        //sqlsize = std::numeric_limits<int32_t>::max() / 2 - 1;
                        is_blob = true;
                    }
                }
            }

            bound_column& col = bound_columns_[i];
            col.name_ = reinterpret_cast<string_type::value_type*>(column_name);
            col.column_ = i;
            col.sqltype_ = sqltype;
            col.sqlsize_ = sqlsize;
            col.scale_ = scale;
            bound_columns_by_name_[col.name_] = &col;

            using namespace std; // if int64_t is in std namespace (in c++11)
            switch(col.sqltype_)
            {
                case SQL_BIT:
                case SQL_TINYINT:
                case SQL_SMALLINT:
                case SQL_INTEGER:
                    col.ctype_ = SQL_C_LONG;
                    col.clen_ = sizeof(int32_t);
                    break;
                case SQL_BIGINT:
                    col.ctype_ = SQL_C_SBIGINT;
                    col.clen_ = sizeof(int64_t);
                    break;
                case SQL_DOUBLE:
                case SQL_FLOAT:
                case SQL_DECIMAL:
                case SQL_REAL:
                case SQL_NUMERIC:
                    col.ctype_ = SQL_C_DOUBLE;
                    col.clen_ = sizeof(double);
                    break;
                case SQL_DATE:
                case SQL_TYPE_DATE:
                    col.ctype_ = SQL_C_DATE;
                    col.clen_ = sizeof(date);
                    break;
                case SQL_TIMESTAMP:
                case SQL_TYPE_TIMESTAMP:
                    col.ctype_ = SQL_C_TIMESTAMP;
                    col.clen_ = sizeof(timestamp);
                    break;
                case SQL_CHAR:
                case SQL_VARCHAR:
                    col.ctype_ = SQL_C_CHAR;
                    col.clen_ = (col.sqlsize_ + 1) * sizeof(SQLCHAR);
                    if(is_blob)
                    {
                        col.clen_ = 0;
                        col.blob_ = true;
                    }
                    break;
                case SQL_WCHAR:
                case SQL_WVARCHAR:
                    col.ctype_ = SQL_C_WCHAR;
                    col.clen_ = (col.sqlsize_ + 1) * sizeof(SQLWCHAR);
                    if(is_blob)
                    {
                        col.clen_ = 0;
                        col.blob_ = true;
                    }
                    break;
                case SQL_LONGVARCHAR:
                    col.ctype_ = SQL_C_CHAR;
                    col.blob_ = true;
                    col.clen_ = 0;
                    break;
                case SQL_BINARY:
                case SQL_VARBINARY:
                    col.ctype_ = SQL_C_BINARY;
                    col.clen_ = col.sqlsize_ + sizeof(NANODBC_SQLCHAR);
                    break;
                case SQL_LONGVARBINARY:
                    col.ctype_ = SQL_C_BINARY;
                    col.blob_ = true;
                    col.clen_ = 0;
                    break;
                default:
                    col.ctype_ = sql_ctype<string_type>::value;
                    col.clen_ = 128;
                    break;
            }
        }

        for(SQLSMALLINT i = 0; i < n_columns; ++i)
        {
            bound_column& col = bound_columns_[i];
            col.cbdata_ = new null_type[rowset_size_];
            if(col.blob_)
            {
                NANODBC_CALL_RC(
                    SQLBindCol
                    , rc
                    , stmt_.native_statement_handle()
                    , i + 1
                    , col.ctype_
                    , 0
                    , 0
                    , col.cbdata_);
                if(!success(rc))
                    NANODBC_THROW_DATABASE_ERROR(stmt_.native_statement_handle(), SQL_HANDLE_STMT);
            }
            else
            {
                col.pdata_ = new char[rowset_size_ * col.clen_];
                NANODBC_CALL_RC(
                    SQLBindCol
                    , rc
                    , stmt_.native_statement_handle()
                    , i + 1         // ColumnNumber
                    , col.ctype_    // TargetType
                    , col.pdata_    // TargetValuePtr
                    , col.clen_     // BufferLength
                    , col.cbdata_); // StrLen_or_Ind
                if(!success(rc))
                    NANODBC_THROW_DATABASE_ERROR(stmt_.native_statement_handle(), SQL_HANDLE_STMT);
            }
        }
    }

private:
    statement stmt_;
    const long rowset_size_;
    SQLULEN row_count_;
    bound_column* bound_columns_;
    short bound_columns_size_;
    long rowset_position_;
    std::map<string_type, bound_column*> bound_columns_by_name_;
    bool at_end_;
};

template<>
inline void result::result_impl::get_ref_impl<date>(short column, date& result) const
{
    bound_column& col = bound_columns_[column];
    switch(col.ctype_)
    {
        case SQL_C_DATE:
            result = *((date*)(col.pdata_ + rowset_position_ * col.clen_));
            return;
        case SQL_C_TIMESTAMP:
        {
            timestamp stamp = *( (timestamp*)( col.pdata_ + rowset_position_ * col.clen_ ) );
            date d = { stamp.year, stamp.month, stamp.day };
            result = d;
            return;
        }
    }
    throw type_incompatible_error();
}

template<>
inline void result::result_impl::get_ref_impl<timestamp>(short column, timestamp& result) const
{
    bound_column& col = bound_columns_[column];
    switch(col.ctype_)
    {
        case SQL_C_DATE:
        {
            date d = *((date*)(col.pdata_ + rowset_position_ * col.clen_));
            timestamp stamp = { d.year, d.month, d.day, 0, 0, 0, 0 };
            result = stamp;
            return;
        }
        case SQL_C_TIMESTAMP:
            result = *((timestamp*)(col.pdata_ + rowset_position_ * col.clen_));
            return;
    }
    throw type_incompatible_error();
}

template<>
inline void result::result_impl::get_ref_impl<string_type>(short column, string_type& result) const
{
    bound_column& col = bound_columns_[column];
    const SQLULEN column_size = col.sqlsize_;

    switch(col.ctype_)
    {
        case SQL_C_CHAR:
        {
            if(col.blob_)
            {
                // Input is always std::string, while output may be std::string or std::wstring
                std::stringstream ss;
                char buff[1024] = {0};
                std::size_t buff_size = sizeof(buff);
                SQLLEN ValueLenOrInd;
                SQLRETURN rc;
                void* handle = native_statement_handle();
                do
                {
                    NANODBC_CALL_RC(
                        SQLGetData
                        , rc
                        , handle            // StatementHandle
                        , column + 1        // Col_or_Param_Num
                        , SQL_C_CHAR        // TargetType
                        , buff              // TargetValuePtr
                        , buff_size         // BufferLength
                        , &ValueLenOrInd);  // StrLen_or_IndPtr
                    if(ValueLenOrInd > 0)
                        ss << buff;
                    else if(ValueLenOrInd == SQL_NULL_DATA)
                        *col.cbdata_ = (SQLINTEGER) SQL_NULL_DATA;
                } while(rc > 0);
                convert(ss.str(), result);
            }
            else
            {
                const char* s = col.pdata_ + rowset_position_ * col.clen_;
                const std::string::size_type str_size = std::strlen(s);
                result.assign(s, s + str_size);
            }
            return;
        }

        case SQL_C_WCHAR:
        {
            if(col.blob_)
            {
                // Input is always std::wstring, output might be std::string or std::wstring.
                // Use a string builder to build the output string.
                std::wstringstream ss;
                wchar_t buffer[512] = {0};
                std::size_t buffer_size = sizeof(buffer);
                SQLLEN ValueLenOrInd;
                SQLRETURN rc;
                void* handle = native_statement_handle();
                do
                {
                    NANODBC_CALL_RC(
                        SQLGetData
                        , rc
                        , handle            // StatementHandle
                        , column + 1        // Col_or_Param_Num
                        , SQL_C_WCHAR       // TargetType
                        , buffer            // TargetValuePtr
                        , buffer_size       // BufferLength
                        , &ValueLenOrInd);  // StrLen_or_IndPtr
                    if(ValueLenOrInd > 0)
                        ss << buffer;
                    else if(ValueLenOrInd == SQL_NULL_DATA)
                        *col.cbdata_ = (SQLINTEGER) SQL_NULL_DATA;
                } while(rc > 0);
                convert(ss.str(), result);
            }
            else
            {
                // Type is unicode in the database, convert if necessary
                const SQLWCHAR* s = reinterpret_cast<SQLWCHAR*>(col.pdata_ + rowset_position_ * col.clen_);
                const string_type::size_type str_size = *col.cbdata_ / sizeof(SQLWCHAR);
                std::wstring temp(s, s + str_size);
                convert(temp, result);
            }
            return;
        }

        case SQL_C_GUID:
        case SQL_C_BINARY:
        {
            if(col.blob_)
                throw std::runtime_error("blob not implemented yet");
            const char* s = col.pdata_ + rowset_position_ * col.clen_;
            result.assign(s, s + column_size);
            return;
        }

        case SQL_C_LONG:
        {
           result.resize(column_size);
           if(NANODBC_SNPRINTF(
                    const_cast<string_type::value_type*>(result.data())
                    , column_size
                    , NANODBC_TEXT("%d")
                    , *(int32_t*)(col.pdata_ + rowset_position_ * col.clen_)) == -1)
               throw type_incompatible_error();
           result.resize(NANODBC_STRLEN(result.c_str()));
           return;
        }

        case SQL_C_SBIGINT:
        {
            using namespace std; // in case intmax_t is in namespace std
            result.resize(column_size);
            if(NANODBC_SNPRINTF(
                    const_cast<string_type::value_type*>(result.data())
                    , column_size
                    , NANODBC_TEXT("%jd")
                    , (intmax_t) *(int64_t*)(col.pdata_ + rowset_position_ * col.clen_)) == -1)
                throw type_incompatible_error();
            result.resize(NANODBC_STRLEN(result.c_str()));
            return;
        }

        case SQL_C_FLOAT:
        {
            result.resize(column_size);
            if(NANODBC_SNPRINTF(
                    const_cast<string_type::value_type*>(result.data())
                    , column_size
                    , NANODBC_TEXT("%f")
                    , *(float*)(col.pdata_ + rowset_position_ * col.clen_)) == -1)
                throw type_incompatible_error();
            result.resize(NANODBC_STRLEN(result.c_str()));
            return;
        }

        case SQL_C_DOUBLE:
        {
            result.resize(column_size + 2);     // account for decimal mark and sign
            if(NANODBC_SNPRINTF(
                const_cast<string_type::value_type*>(result.data())
                , column_size + 2
                , NANODBC_TEXT("%.*lf")         // restrict the number of digits
                , col.scale_                    // number of digits after the decimal point
                , *(double*)(col.pdata_ + rowset_position_ * col.clen_)) == -1)
            throw type_incompatible_error();
            result.resize(NANODBC_STRLEN(result.c_str()));
            return;
        }

        case SQL_C_DATE:
        {
            date d = *((date*)(col.pdata_ + rowset_position_ * col.clen_));
            std::tm st = { 0 };
            st.tm_year = d.year - 1900;
            st.tm_mon = d.month - 1;
            st.tm_mday = d.day;
            char* old_lc_time = std::setlocale(LC_TIME, NULL);
            std::setlocale(LC_TIME, "");
            string_type::value_type date_str[512];
            NANODBC_STRFTIME(
                date_str
                , sizeof(date_str) / sizeof(string_type::value_type)
                , NANODBC_TEXT("%Y-%m-%d")
                , &st);
            std::setlocale(LC_TIME, old_lc_time);
            result.assign(date_str);
            return;
        }

        case SQL_C_TIMESTAMP:
        {
            timestamp stamp = *((timestamp*)(col.pdata_ + rowset_position_ * col.clen_));
            std::tm st = { 0 };
            st.tm_year = stamp.year - 1900;
            st.tm_mon = stamp.month - 1;
            st.tm_mday = stamp.day;
            st.tm_hour = stamp.hour;
            st.tm_min = stamp.min;
            st.tm_sec = stamp.sec;
            char* old_lc_time = std::setlocale(LC_TIME, NULL);
            std::setlocale(LC_TIME, "");
            string_type::value_type date_str[512];
            NANODBC_STRFTIME(
                date_str
                , sizeof(date_str) / sizeof(string_type::value_type)
                , NANODBC_TEXT("%Y-%m-%d %H:%M:%S %z")
                , &st);
            std::setlocale(LC_TIME, old_lc_time);
           result.assign(date_str);
           return;
        }
    }
    throw type_incompatible_error();
}

template<class T>
void result::result_impl::get_ref_impl(short column, T& result) const
{
    bound_column& col = bound_columns_[column];
    using namespace std; // if int64_t is in std namespace (in c++11)
    const char* s = col.pdata_ + rowset_position_ * col.clen_;
    switch(col.ctype_)
    {
        case SQL_C_CHAR: result = (T)*(char*)(s); return;
        case SQL_C_SSHORT: result = (T)*(short*)(s); return;
        case SQL_C_USHORT: result = (T)*(unsigned short*)(s); return;
        case SQL_C_LONG: result = (T)*(int32_t*)(s); return;
        case SQL_C_SLONG: result = (T)*(int32_t*)(s); return;
        case SQL_C_ULONG: result = (T)*(uint32_t*)(s); return;
        case SQL_C_FLOAT: result = (T)*(float*)(s); return;
        case SQL_C_DOUBLE: result = (T)*(double*)(s); return;
        case SQL_C_SBIGINT: result = (T)*(int64_t*)(s); return;
        case SQL_C_UBIGINT: result = (T)*(uint64_t*)(s); return;
    }
    throw type_incompatible_error();
}

} // namespace nanodbc

// 8888888888                            8888888888                         888    d8b
// 888                                   888                                888    Y8P
// 888                                   888                                888
// 8888888 888d888 .d88b.   .d88b.       8888888 888  888 88888b.   .d8888b 888888 888  .d88b.  88888b.  .d8888b
// 888     888P"  d8P  Y8b d8P  Y8b      888     888  888 888 "88b d88P"    888    888 d88""88b 888 "88b 88K
// 888     888    88888888 88888888      888     888  888 888  888 888      888    888 888  888 888  888 "Y8888b.
// 888     888    Y8b.     Y8b.          888     Y88b 888 888  888 Y88b.    Y88b.  888 Y88..88P 888  888      X88
// 888     888     "Y8888   "Y8888       888      "Y88888 888  888  "Y8888P  "Y888 888  "Y88P"  888  888  88888P'
// MARK: Free Functions -

namespace nanodbc
{

result execute(connection& conn, const string_type& query, long batch_operations, long timeout)
{
    class statement statement;
    return statement.execute_direct(conn, query, batch_operations, timeout);
}

void just_execute(connection& conn, const string_type& query, long batch_operations, long timeout) {
    class statement statement;
    statement.just_execute_direct(conn, query, batch_operations, timeout);
}

result execute(statement& stmt, long batch_operations)
{
    return stmt.execute(batch_operations);
}

void just_execute(statement& stmt, long batch_operations)
{
    return stmt.just_execute(batch_operations);
}

result transact(statement& stmt, long batch_operations)
{
    class transaction transaction(stmt.connection());
    result rvalue = stmt.execute(batch_operations);
    transaction.commit();
    return rvalue;
}

void just_transact(statement& stmt, long batch_operations)
{
    class transaction transaction(stmt.connection());
    stmt.just_execute(batch_operations);
    transaction.commit();
}

void prepare(statement& stmt, const string_type& query, long timeout)
{
    stmt.prepare(stmt.connection(), query, timeout);
}

} // namespace nanodbc

//  .d8888b.                                               888    d8b                             8888888888                 888
// d88P  Y88b                                              888    Y8P                             888                        888
// 888    888                                              888                                    888                        888
// 888         .d88b.  88888b.  88888b.   .d88b.   .d8888b 888888 888  .d88b.  88888b.            8888888 888  888  888  .d88888
// 888        d88""88b 888 "88b 888 "88b d8P  Y8b d88P"    888    888 d88""88b 888 "88b           888     888  888  888 d88" 888
// 888    888 888  888 888  888 888  888 88888888 888      888    888 888  888 888  888           888     888  888  888 888  888
// Y88b  d88P Y88..88P 888  888 888  888 Y8b.     Y88b.    Y88b.  888 Y88..88P 888  888           888     Y88b 888 d88P Y88b 888
//  "Y8888P"   "Y88P"  888  888 888  888  "Y8888   "Y8888P  "Y888 888  "Y88P"  888  888           888      "Y8888888P"   "Y88888
// MARK: Connection Fwd -

namespace nanodbc
{

connection::connection()
: impl_(new connection_impl())
{

}

connection::connection(const connection& rhs)
: impl_(rhs.impl_)
{

}

#ifndef NANODBC_NO_MOVE_CTOR
    connection::connection(connection&& rhs) NANODBC_NOEXCEPT
    : impl_(std::move(rhs.impl_))
    {

    }
#endif

connection& connection::operator=(connection rhs)
{
    swap(rhs);
    return *this;
}

void connection::swap(connection& rhs) NANODBC_NOEXCEPT
{
    using std::swap;
    swap(impl_, rhs.impl_);
}

connection::connection(
    const string_type& dsn
    , const string_type& user
    , const string_type& pass
    , long timeout)
: impl_(new connection_impl(dsn, user, pass, timeout))
{

}

connection::connection(const string_type& connection_string, long timeout)
: impl_(new connection_impl(connection_string, timeout))
{

}

connection::~connection() NANODBC_NOEXCEPT
{

}

void connection::connect(
    const string_type& dsn
    , const string_type& user
    , const string_type& pass
    , long timeout)
{
    impl_->connect(dsn, user, pass, timeout);
}

void connection::connect(const string_type& connection_string, long timeout)
{
    impl_->connect(connection_string, timeout);
}

#ifdef SQL_ATTR_ASYNC_DBC_EVENT
void connection::async_connect(
    const string_type& dsn
    , const string_type& user
    , const string_type& pass
    , void* event_handle
    , long timeout)
{
    impl_->connect(dsn, user, pass, timeout, event_handle);
}

void connection::async_connect(const string_type& connection_string, void* event_handle, long timeout)
{
    impl_->connect(connection_string, timeout, event_handle);
}

void connection::async_complete()
{
    impl_->async_complete();
}
#endif // SQL_ATTR_ASYNC_DBC_EVENT

bool connection::connected() const
{
    return impl_->connected();
}

void connection::disconnect()
{
    impl_->disconnect();
}

std::size_t connection::transactions() const
{
    return impl_->transactions();
}

void* connection::native_dbc_handle() const
{
    return impl_->native_dbc_handle();
}

void* connection::native_env_handle() const
{
    return impl_->native_env_handle();
}

string_type connection::driver_name() const
{
    return impl_->driver_name();
}

string_type connection::database_name() const
{
    return impl_->database_name();
}

string_type connection::catalog_name() const
{
    return impl_->catalog_name();
}

std::size_t connection::ref_transaction()
{
    return impl_->ref_transaction();
}

std::size_t connection::unref_transaction()
{
    return impl_->unref_transaction();
}

bool connection::rollback() const
{
    return impl_->rollback();
}

void connection::rollback(bool onoff)
{
    impl_->rollback(onoff);
}

} // namespace nanodbc

// 88888888888                                                  888    d8b                             8888888888                 888
//     888                                                      888    Y8P                             888                        888
//     888                                                      888                                    888                        888
//     888  888d888 8888b.  88888b.  .d8888b   8888b.   .d8888b 888888 888  .d88b.  88888b.            8888888 888  888  888  .d88888 .d8888b
//     888  888P"      "88b 888 "88b 88K          "88b d88P"    888    888 d88""88b 888 "88b           888     888  888  888 d88" 888 88K
//     888  888    .d888888 888  888 "Y8888b. .d888888 888      888    888 888  888 888  888           888     888  888  888 888  888 "Y8888b.
//     888  888    888  888 888  888      X88 888  888 Y88b.    Y88b.  888 Y88..88P 888  888           888     Y88b 888 d88P Y88b 888      X88
//     888  888    "Y888888 888  888  88888P' "Y888888  "Y8888P  "Y888 888  "Y88P"  888  888           888      "Y8888888P"   "Y88888  88888P'
// MARK: Transaction Fwd -

namespace nanodbc
{

transaction::transaction(const class connection& conn)
: impl_(new transaction_impl(conn))
{

}

transaction::transaction(const transaction& rhs)
: impl_(rhs.impl_)
{

}

#ifndef NANODBC_NO_MOVE_CTOR
    transaction::transaction(transaction&& rhs) NANODBC_NOEXCEPT
    : impl_(std::move(rhs.impl_))
    {

    }
#endif

transaction& transaction::operator=(transaction rhs)
{
    swap(rhs);
    return *this;
}

void transaction::swap(transaction& rhs) NANODBC_NOEXCEPT
{
    using std::swap;
    swap(impl_, rhs.impl_);
}

transaction::~transaction() NANODBC_NOEXCEPT
{

}

void transaction::commit()
{
    impl_->commit();
}

void transaction::rollback() NANODBC_NOEXCEPT
{
    impl_->rollback();
}

class connection& transaction::connection()
{
    return impl_->connection();
}

const class connection& transaction::connection() const
{
    return impl_->connection();
}

transaction::operator class connection&()
{
    return impl_->connection();
}

transaction::operator const class connection&() const
{
    return impl_->connection();
}

} // namespace nanodbc

//  .d8888b.  888             888                                            888              8888888888                 888
// d88P  Y88b 888             888                                            888              888                        888
// Y88b.      888             888                                            888              888                        888
//  "Y888b.   888888  8888b.  888888 .d88b.  88888b.d88b.   .d88b.  88888b.  888888           8888888 888  888  888  .d88888
//     "Y88b. 888        "88b 888   d8P  Y8b 888 "888 "88b d8P  Y8b 888 "88b 888              888     888  888  888 d88" 888
//       "888 888    .d888888 888   88888888 888  888  888 88888888 888  888 888              888     888  888  888 888  888
// Y88b  d88P Y88b.  888  888 Y88b. Y8b.     888  888  888 Y8b.     888  888 Y88b.            888     Y88b 888 d88P Y88b 888
//  "Y8888P"   "Y888 "Y888888  "Y888 "Y8888  888  888  888  "Y8888  888  888  "Y888           888      "Y8888888P"   "Y88888
// MARK: Statement Fwd -

namespace nanodbc
{

statement::statement()
: impl_(new statement_impl())
{

}

statement::statement(class connection& conn)
: impl_(new statement_impl(conn))
{

}

#ifndef NANODBC_NO_MOVE_CTOR
    statement::statement(statement&& rhs) NANODBC_NOEXCEPT
    : impl_(std::move(rhs.impl_))
    {

    }
#endif

statement::statement(class connection& conn, const string_type& query, long timeout)
: impl_(new statement_impl(conn, query, timeout))
{

}

statement::statement(const statement& rhs)
: impl_(rhs.impl_)
{

}

statement& statement::operator=(statement rhs)
{
    swap(rhs);
    return *this;
}

void statement::swap(statement& rhs) NANODBC_NOEXCEPT
{
    using std::swap;
    swap(impl_, rhs.impl_);
}

statement::~statement() NANODBC_NOEXCEPT
{

}

void statement::open(class connection& conn)
{
    impl_->open(conn);
}

bool statement::open() const
{
    return impl_->open();
}

bool statement::connected() const
{
    return impl_->connected();
}

const class connection& statement::connection() const
{
    return impl_->connection();
}

class connection& statement::connection()
{
    return impl_->connection();
}

void* statement::native_statement_handle() const
{
    return impl_->native_statement_handle();
}

void statement::close()
{
    impl_->close();
}

void statement::cancel()
{
    impl_->cancel();
}

void statement::prepare(class connection& conn, const string_type& query, long timeout)
{
    impl_->prepare(conn, query, timeout);
}

void statement::prepare(const string_type& query, long timeout)
{
    impl_->prepare(query, timeout);
}

void statement::timeout(long timeout)
{
    impl_->timeout(timeout);
}

result statement::execute_direct(
    class connection& conn
    , const string_type& query
    , long batch_operations
    , long timeout)
{
    return impl_->execute_direct(conn, query, batch_operations, timeout, *this);
}

#if defined(SQL_ATTR_ASYNC_STMT_EVENT) && defined(SQL_API_SQLCOMPLETEASYNC)
    void statement::async_execute_direct(
        class connection& conn
        , void* event_handle
        , const string_type& query
        , long batch_operations
        , long timeout)
    {
        impl_->async_execute_direct(conn, event_handle, query, batch_operations, timeout, *this);
    }

    result statement::async_complete(long batch_operations)
    {
        return impl_->async_complete(batch_operations, *this);
    }
#endif

void statement::just_execute_direct(
    class connection& conn
    , const string_type& query
    , long batch_operations
    , long timeout)
{
    impl_->just_execute_direct(conn, query, batch_operations, timeout, *this);
}

result statement::execute(long batch_operations, long timeout)
{
    return impl_->execute(batch_operations, timeout, *this);
}

void statement::just_execute(long batch_operations, long timeout)
{
    impl_->just_execute(batch_operations, timeout, *this);
}

result statement::procedure_columns(
    const string_type& catalog
    , const string_type& schema
    , const string_type& procedure
    , const string_type& column)
{
    return impl_->procedure_columns(catalog, schema, procedure, column, *this);
}

long statement::affected_rows() const
{
    return impl_->affected_rows();
}

short statement::columns() const
{
    return impl_->columns();
}

void statement::reset_parameters() NANODBC_NOEXCEPT
{
    impl_->reset_parameters();
}

unsigned long statement::parameter_size(short param) const
{
    return impl_->parameter_size(param);
}

// We need to instantiate each form of bind() for each of our supported data types.
#define NANODBC_INSTANTIATE_BINDS(type)                                                                                 \
    template void statement::bind(short, const type*, param_direction); /* 1-ary */                                     \
    template void statement::bind(short, const type*, std::size_t, param_direction); /* n-ary */                        \
    template void statement::bind(short, const type*, std::size_t, const type*, param_direction); /* n-ary, sentry */   \
    template void statement::bind(short, const type*, std::size_t, const bool*, param_direction) /* n-ary, flags */     \
    /**/

// The following are the only supported instantiations of statement::bind().
NANODBC_INSTANTIATE_BINDS(string_type::value_type);
NANODBC_INSTANTIATE_BINDS(short);
NANODBC_INSTANTIATE_BINDS(unsigned short);
NANODBC_INSTANTIATE_BINDS(int32_t);
NANODBC_INSTANTIATE_BINDS(uint32_t);
NANODBC_INSTANTIATE_BINDS(int64_t);
NANODBC_INSTANTIATE_BINDS(uint64_t);
NANODBC_INSTANTIATE_BINDS(float);
NANODBC_INSTANTIATE_BINDS(double);
NANODBC_INSTANTIATE_BINDS(date);
NANODBC_INSTANTIATE_BINDS(timestamp);

#undef NANODBC_INSTANTIATE_BINDS

template<class T>
void statement::bind(short param, const T* value, param_direction direction)
{
    impl_->bind(param, value, 1, direction);
}

template<class T>
void statement::bind(short param, const T* values, std::size_t elements, param_direction direction)
{
    impl_->bind(param, values, elements, direction);
}

template<class T>
void statement::bind(
    short param
    , const T* values
    , std::size_t elements
    , const T* null_sentry
    , param_direction direction)
{
    impl_->bind(param, values, elements, 0, null_sentry, direction);
}

template<class T>
void statement::bind(
    short param
    , const T* values
    , std::size_t elements
    , const bool* nulls
    , param_direction direction)
{
    impl_->bind(param, values, elements, nulls, (T*)0, direction);
}

void statement::bind_strings(
    short param
    , const string_type::value_type* values
    , std::size_t length
    , std::size_t elements
    , param_direction direction)
{
    impl_->bind_strings(param, values, length, elements, direction);
}

void statement::bind_strings(
    short param
    , const string_type::value_type* values
    , std::size_t length
    , std::size_t elements
    , const string_type::value_type* null_sentry
    , param_direction direction)
{
    impl_->bind_strings(param, values, length, elements, (bool*)0, null_sentry, direction);
}

void statement::bind_strings(
    short param
    , const string_type::value_type* values
    , std::size_t length
    , std::size_t elements
    , const bool* nulls
    , param_direction direction)
{
    impl_->bind_strings(
        param
        , values
        , length
        , elements
        , nulls
        , (string_type::value_type*)0
        , direction);
}

void statement::bind_null(short param, std::size_t elements)
{
    impl_->bind_null(param, elements);
}

} // namespace nanodbc

namespace nanodbc
{

catalog::tables::tables(result& find_result)
: result_(find_result)
{
}

bool catalog::tables::next()
{
    return result_.next();
}

string_type catalog::tables::table_catalog() const
{
    // TABLE_CAT might be NULL
    return result_.get<string_type>(0, string_type());
}

string_type catalog::tables::table_schema() const
{
    // TABLE_SCHEM might be NULL
    return result_.get<string_type>(1, string_type());
}

string_type catalog::tables::table_name() const
{
    // TABLE_NAME column is never NULL
    return result_.get<string_type>(2);
}

string_type catalog::tables::table_type() const
{
    // TABLE_TYPE column is never NULL
    return result_.get<string_type>(3);
}

string_type catalog::tables::table_remarks() const
{
    // REMARKS might be NULL
    return result_.get<string_type>(4, string_type());
}

catalog::primary_keys::primary_keys(result& find_result)
: result_(find_result)
{
}

bool catalog::primary_keys::next()
{
    return result_.next();
}

string_type catalog::primary_keys::table_catalog() const
{
    // TABLE_CAT might be NULL
    return result_.get<string_type>(0, string_type());
}

string_type catalog::primary_keys::table_schema() const
{
    // TABLE_SCHEM might be NULL
    return result_.get<string_type>(1, string_type());
}

string_type catalog::primary_keys::table_name() const
{
    // TABLE_NAME is never NULL
    return result_.get<string_type>(2);
}

string_type catalog::primary_keys::column_name() const
{
    // COLUMN_NAME is never NULL
    return result_.get<string_type>(3);
}

short catalog::primary_keys::column_number() const
{
    // KEY_SEQ is never NULL
    return result_.get<short>(4);
}

string_type catalog::primary_keys::primary_key_name() const
{
    // PK_NAME might be NULL
    return result_.get<string_type>(5);
}

catalog::columns::columns(result& find_result)
: result_(find_result)
{
}

bool catalog::columns::next()
{
    return result_.next();
}

string_type catalog::columns::table_catalog() const
{
    // TABLE_CAT might be NULL
    return result_.get<string_type>(0, string_type());
}

string_type catalog::columns::table_schema() const
{
    // TABLE_SCHEM might be NULL
    return result_.get<string_type>(1, string_type());
}

string_type catalog::columns::table_name() const
{
    // TABLE_NAME is never NULL
    return result_.get<string_type>(2);
}

string_type catalog::columns::column_name() const
{
    // COLUMN_NAME is never NULL
    return result_.get<string_type>(3);
}

short catalog::columns::data_type() const
{
    // DATA_TYPE is never NULL
    return result_.get<short>(4);
}

string_type catalog::columns::type_name() const
{
    // TYPE_NAME is never NULL
    return result_.get<string_type>(5);
}

long catalog::columns::column_size() const
{
    // COLUMN_SIZE
    return result_.get<long>(6);
}

long catalog::columns::buffer_length() const
{
    // BUFFER_LENGTH
    return result_.get<long>(7);
}

short catalog::columns::decimal_digits() const
{
    // DECIMAL_DIGITS might be NULL
    return result_.get<short>(8, 0);
}

short catalog::columns::numeric_precision_radix() const
{
    // NUM_PREC_RADIX might be NULL
    return result_.get<short>(9, 0);
}

short catalog::columns::nullable() const
{
    // NULLABLE is never NULL
    return result_.get<short>(10);
}

string_type catalog::columns::remarks() const
{
    // REMARKS might be NULL
    return result_.get<string_type>(11, string_type());
}

string_type catalog::columns::column_default() const
{
    // COLUMN_DEF might be NULL, if no default value is specified.
    return result_.get<string_type>(12, string_type());
}

short catalog::columns::sql_data_type() const
{
    // SQL_DATA_TYPE is never NULL
    return result_.get<short>(13);
}

short catalog::columns::sql_datetime_subtype() const
{
    // SQL_DATETIME_SUB might be NULL
    return result_.get<short>(14, 0);
}

long catalog::columns::char_octed_length() const
{
    // CHAR_OCTET_LENGTH might be nULL
    return result_.get<long>(15, 0);
}

long catalog::columns::ordinal_position() const
{
    // ORDINAL_POSITION is never NULL
    return result_.get<long>(16);
}

string_type catalog::columns::is_nullable() const
{
    // IS_NULLABLE is never NULL
    return result_.get<string_type>(17);
}

catalog::catalog(connection& conn)
: conn_(conn)
{
}

catalog::tables catalog::find_tables(
    const string_type& table
  , const string_type& type
  , const string_type& schema
  , const string_type& catalog)
{
    statement stmt(conn_);
    RETCODE rc;
    NANODBC_CALL_RC(
        NANODBC_UNICODE(SQLTables)
        , rc
        , stmt.native_statement_handle()
        , (NANODBC_SQLCHAR*)(catalog.empty() ? NULL : catalog.c_str())
        , (catalog.empty() ? 0 : SQL_NTS)
        , (NANODBC_SQLCHAR*)(schema.empty() ? NULL : schema.c_str())
        , (schema.empty() ? 0 : SQL_NTS)
        , (NANODBC_SQLCHAR*)(table.empty() ? NULL : table.c_str())
        , (table.empty() ? 0 : SQL_NTS)
        , (NANODBC_SQLCHAR*)(type.empty() ? NULL : type.c_str())
        , (type.empty() ? 0 : SQL_NTS));
    if(!success(rc))
        NANODBC_THROW_DATABASE_ERROR(stmt.native_statement_handle(), SQL_HANDLE_STMT);

    result find_result(stmt, 1);
    return catalog::tables(find_result);
}

catalog::columns catalog::find_columns(
        const string_type& column
      , const string_type& table
      , const string_type& schema
      , const string_type& catalog)
{
    statement stmt(conn_);
    RETCODE rc;
    NANODBC_CALL_RC(
        NANODBC_UNICODE(SQLColumns)
        , rc
        , stmt.native_statement_handle()
        , (NANODBC_SQLCHAR*)(catalog.empty() ? NULL : catalog.c_str())
        , (catalog.empty() ? 0 : SQL_NTS)
        , (NANODBC_SQLCHAR*)(schema.empty() ? NULL : schema.c_str())
        , (schema.empty() ? 0 : SQL_NTS)
        , (NANODBC_SQLCHAR*)(table.empty() ? NULL : table.c_str())
        , (table.empty() ? 0 : SQL_NTS)
        , (NANODBC_SQLCHAR*)(column.empty() ? NULL : column.c_str())
        , (column.empty() ? 0 : SQL_NTS));
    if(!success(rc))
        NANODBC_THROW_DATABASE_ERROR(stmt.native_statement_handle(), SQL_HANDLE_STMT);

    result find_result(stmt, 1);
    return catalog::columns(find_result);
}

catalog::primary_keys catalog::find_primary_keys(
      const string_type& table
    , const string_type& schema
    , const string_type& catalog)
{
    statement stmt(conn_);
    RETCODE rc;
    NANODBC_CALL_RC(
        NANODBC_UNICODE(SQLPrimaryKeys)
        , rc
        , stmt.native_statement_handle()
        , (NANODBC_SQLCHAR*)(catalog.empty() ? NULL : catalog.c_str())
        , (catalog.empty() ? 0 : SQL_NTS)
        , (NANODBC_SQLCHAR*)(schema.empty() ? NULL : schema.c_str())
        , (schema.empty() ? 0 : SQL_NTS)
        , (NANODBC_SQLCHAR*)(table.empty() ? NULL : table.c_str())
        , (table.empty() ? 0 : SQL_NTS));
    if(!success(rc))
        NANODBC_THROW_DATABASE_ERROR(stmt.native_statement_handle(), SQL_HANDLE_STMT);

    result find_result(stmt, 1);
    return catalog::primary_keys(find_result);
}

} // namespace nanodbc

// 8888888b.                            888 888              8888888888                 888
// 888   Y88b                           888 888              888                        888
// 888    888                           888 888              888                        888
// 888   d88P .d88b.  .d8888b  888  888 888 888888           8888888 888  888  888  .d88888
// 8888888P" d8P  Y8b 88K      888  888 888 888              888     888  888  888 d88" 888
// 888 T88b  88888888 "Y8888b. 888  888 888 888              888     888  888  888 888  888
// 888  T88b Y8b.          X88 Y88b 888 888 Y88b.            888     Y88b 888 d88P Y88b 888
// 888   T88b "Y8888   88888P'  "Y88888 888  "Y888           888      "Y8888888P"   "Y88888
// MARK: Result Fwd -

namespace nanodbc
{

result::result()
: impl_()
{

}

result::~result() NANODBC_NOEXCEPT
{

}

result::result(statement stmt, long rowset_size)
: impl_(new result_impl(stmt, rowset_size))
{

}

#ifndef NANODBC_NO_MOVE_CTOR
    result::result(result&& rhs) NANODBC_NOEXCEPT
    : impl_(std::move(rhs.impl_))
    {

    }
#endif

result::result(const result& rhs)
: impl_(rhs.impl_)
{

}

result& result::operator=(result rhs)
{
    swap(rhs);
    return *this;
}

void result::swap(result& rhs) NANODBC_NOEXCEPT
{
    using std::swap;
    swap(impl_, rhs.impl_);
}

void* result::native_statement_handle() const
{
    return impl_->native_statement_handle();
}

long result::rowset_size() const NANODBC_NOEXCEPT
{
    return impl_->rowset_size();
}

long result::affected_rows() const
{
    return impl_->affected_rows();
}

long result::rows() const NANODBC_NOEXCEPT
{
    return impl_->rows();
}

short result::columns() const
{
    return impl_->columns();
}

bool result::first()
{
    return impl_->first();
}

bool result::last()
{
    return impl_->last();
}

bool result::next()
{
    return impl_->next();
}

bool result::prior()
{
    return impl_->prior();
}

bool result::move(long row)
{
    return impl_->move(row);
}

bool result::skip(long rows)
{
    return impl_->skip(rows);
}

unsigned long result::position() const
{
    return impl_->position();
}

bool result::end() const NANODBC_NOEXCEPT
{
    return impl_->end();
}

bool result::is_null(short column) const
{
    return impl_->is_null(column);
}

bool result::is_null(const string_type& column_name) const
{
    return impl_->is_null(column_name);
}

string_type result::column_name(short column) const
{
    return impl_->column_name(column);
}

long result::column_size(short column) const
{
    return impl_->column_size(column);
}

short result::column(const string_type& column_name) const
{
    return impl_->column(column_name);
}

int result::column_datatype(short column) const
{
    return impl_->column_datatype(column);
}

int result::column_datatype(const string_type& column_name) const
{
    return impl_->column_datatype(column_name);
}

int result::column_c_datatype(short column) const
{
    return impl_->column_c_datatype(column);
}

int result::column_c_datatype(const string_type& column_name) const
{
    return impl_->column_c_datatype(column_name);
}

bool result::next_result()
{
    return impl_->next_result();
}

template<class T>
void result::get_ref(short column, T& result) const
{
    return impl_->get_ref<T>(column, result);
}

template<class T>
void result::get_ref(short column, const T& fallback, T& result) const
{
    return impl_->get_ref<T>(column, fallback, result);
}

template<class T>
void result::get_ref(const string_type& column_name, T& result) const
{
    return impl_->get_ref<T>(column_name, result);
}

template<class T>
void result::get_ref(const string_type& column_name, const T& fallback, T& result) const
{
    return impl_->get_ref<T>(column_name, fallback, result);
}

template<class T>
T result::get(short column) const
{
    return impl_->get<T>(column);
}

template<class T>
T result::get(short column, const T& fallback) const
{
    return impl_->get<T>(column, fallback);
}

template<class T>
T result::get(const string_type& column_name) const
{
    return impl_->get<T>(column_name);
}

template<class T>
T result::get(const string_type& column_name, const T& fallback) const
{
    return impl_->get<T>(column_name, fallback);
}

result::operator bool() const
{
    return static_cast<bool>(impl_);
}

// The following are the only supported instantiations of result::get_ref().
template void result::get_ref(short, string_type::value_type&) const;
template void result::get_ref(short, short&) const;
template void result::get_ref(short, unsigned short&) const;
template void result::get_ref(short, int32_t&) const;
template void result::get_ref(short, uint32_t&) const;
template void result::get_ref(short, int64_t&) const;
template void result::get_ref(short, uint64_t&) const;
template void result::get_ref(short, float&) const;
template void result::get_ref(short, double&) const;
template void result::get_ref(short, string_type&) const;
template void result::get_ref(short, date&) const;
template void result::get_ref(short, timestamp&) const;

template void result::get_ref(const string_type&, string_type::value_type&) const;
template void result::get_ref(const string_type&, short&) const;
template void result::get_ref(const string_type&, unsigned short&) const;
template void result::get_ref(const string_type&, int32_t&) const;
template void result::get_ref(const string_type&, uint32_t&) const;
template void result::get_ref(const string_type&, int64_t&) const;
template void result::get_ref(const string_type&, uint64_t&) const;
template void result::get_ref(const string_type&, float&) const;
template void result::get_ref(const string_type&, double&) const;
template void result::get_ref(const string_type&, string_type&) const;
template void result::get_ref(const string_type&, date&) const;
template void result::get_ref(const string_type&, timestamp&) const;

// The following are the only supported instantiations of result::get_ref() with fallback.
template void result::get_ref(short, const string_type::value_type&, string_type::value_type&) const;
template void result::get_ref(short, const short&, short&) const;
template void result::get_ref(short, const unsigned short&, unsigned short&) const;
template void result::get_ref(short, const int32_t&, int32_t&) const;
template void result::get_ref(short, const uint32_t&, uint32_t&) const;
template void result::get_ref(short, const int64_t&, int64_t&) const;
template void result::get_ref(short, const uint64_t&, uint64_t&) const;
template void result::get_ref(short, const float&, float&) const;
template void result::get_ref(short, const double&, double&) const;
template void result::get_ref(short, const string_type&, string_type&) const;
template void result::get_ref(short, const date&, date&) const;
template void result::get_ref(short, const timestamp&, timestamp&) const;

template void result::get_ref(const string_type&, const string_type::value_type&, string_type::value_type&) const;
template void result::get_ref(const string_type&, const short&, short&) const;
template void result::get_ref(const string_type&, const unsigned short&, unsigned short&) const;
template void result::get_ref(const string_type&, const int32_t&, int32_t&) const;
template void result::get_ref(const string_type&, const uint32_t&, uint32_t&) const;
template void result::get_ref(const string_type&, const int64_t&, int64_t&) const;
template void result::get_ref(const string_type&, const uint64_t&, uint64_t&) const;
template void result::get_ref(const string_type&, const float&, float&) const;
template void result::get_ref(const string_type&, const double&, double&) const;
template void result::get_ref(const string_type&, const string_type&, string_type&) const;
template void result::get_ref(const string_type&, const date&, date&) const;
template void result::get_ref(const string_type&, const timestamp&, timestamp&) const;

// The following are the only supported instantiations of result::get().
template string_type::value_type result::get(short) const;
template short result::get(short) const;
template unsigned short result::get(short) const;
template int32_t result::get(short) const;
template uint32_t result::get(short) const;
template int64_t result::get(short) const;
template uint64_t result::get(short) const;
template float result::get(short) const;
template double result::get(short) const;
template string_type result::get(short) const;
template date result::get(short) const;
template timestamp result::get(short) const;

template string_type::value_type result::get(const string_type&) const;
template short result::get(const string_type&) const;
template unsigned short result::get(const string_type&) const;
template int32_t result::get(const string_type&) const;
template uint32_t result::get(const string_type&) const;
template int64_t result::get(const string_type&) const;
template uint64_t result::get(const string_type&) const;
template float result::get(const string_type&) const;
template double result::get(const string_type&) const;
template string_type result::get(const string_type&) const;
template date result::get(const string_type&) const;
template timestamp result::get(const string_type&) const;

// The following are the only supported instantiations of result::get() with fallback.
template string_type::value_type result::get(short, const string_type::value_type&) const;
template short result::get(short, const short&) const;
template unsigned short result::get(short, const unsigned short&) const;
template int32_t result::get(short, const int32_t&) const;
template uint32_t result::get(short, const uint32_t&) const;
template int64_t result::get(short, const int64_t&) const;
template uint64_t result::get(short, const uint64_t&) const;
template float result::get(short, const float&) const;
template double result::get(short, const double&) const;
template string_type result::get(short, const string_type&) const;
template date result::get(short, const date&) const;
template timestamp result::get(short, const timestamp&) const;

template string_type::value_type result::get(const string_type&, const string_type::value_type&) const;
template short result::get(const string_type&, const short&) const;
template unsigned short result::get(const string_type&, const unsigned short&) const;
template int32_t result::get(const string_type&, const int32_t&) const;
template uint32_t result::get(const string_type&, const uint32_t&) const;
template int64_t result::get(const string_type&, const int64_t&) const;
template uint64_t result::get(const string_type&, const uint64_t&) const;
template float result::get(const string_type&, const float&) const;
template double result::get(const string_type&, const double&) const;
template string_type result::get(const string_type&, const string_type&) const;
template date result::get(const string_type&, const date&) const;
template timestamp result::get(const string_type&, const timestamp&) const;

} // namespace nanodbc

#undef NANODBC_THROW_DATABASE_ERROR
#undef NANODBC_STRINGIZE
#undef NANODBC_STRINGIZE_I
#undef NANODBC_CALL_RC
#undef NANODBC_CALL

#endif // DOXYGEN
