From 93e7ca5fba007e6587a3c9b0eb1ba11b14184af3 Mon Sep 17 00:00:00 2001 From: Lee Houghton Date: Sat, 5 Oct 2013 19:18:41 +0100 Subject: [PATCH 01/26] Initial version of getColumnValue Still need to (a). return the value as a Buffer rather than a string (b) support other types (c). refactor so that it is not duplicating ODBC::GetColumnValue needlessly. --- src/odbc.cpp | 1 + src/odbc.h | 1 + src/odbc_result.cpp | 118 ++++++++++++++++++++++++++++++++++++++++++-- src/odbc_result.h | 14 ++++++ 4 files changed, 130 insertions(+), 4 deletions(-) diff --git a/src/odbc.cpp b/src/odbc.cpp index 73bb50b..fc3e2af 100644 --- a/src/odbc.cpp +++ b/src/odbc.cpp @@ -65,6 +65,7 @@ void ODBC::Init(v8::Handle target) { NODE_DEFINE_CONSTANT(constructor_template, SQL_DESTROY); //SQL_DESTROY is non-standard NODE_DEFINE_CONSTANT(constructor_template, FETCH_ARRAY); NODE_DEFINE_CONSTANT(constructor_template, FETCH_OBJECT); + NODE_DEFINE_CONSTANT(constructor_template, FETCH_NONE); // Prototype Methods NODE_SET_PROTOTYPE_METHOD(constructor_template, "createConnection", CreateConnection); diff --git a/src/odbc.h b/src/odbc.h index 33bdb37..3f773f4 100644 --- a/src/odbc.h +++ b/src/odbc.h @@ -42,6 +42,7 @@ using namespace node; #define MODE_CALLBACK_FOR_EACH 2 #define FETCH_ARRAY 3 #define FETCH_OBJECT 4 +#define FETCH_NONE 5 #define SQL_DESTROY 9999 diff --git a/src/odbc_result.cpp b/src/odbc_result.cpp index c83d367..de59fd2 100644 --- a/src/odbc_result.cpp +++ b/src/odbc_result.cpp @@ -49,6 +49,7 @@ void ODBCResult::Init(v8::Handle target) { // Prototype Methods NODE_SET_PROTOTYPE_METHOD(constructor_template, "fetchAll", FetchAll); NODE_SET_PROTOTYPE_METHOD(constructor_template, "fetch", Fetch); + NODE_SET_PROTOTYPE_METHOD(constructor_template, "getColumnValue", GetColumnValue); NODE_SET_PROTOTYPE_METHOD(constructor_template, "moreResultsSync", MoreResultsSync); NODE_SET_PROTOTYPE_METHOD(constructor_template, "closeSync", CloseSync); @@ -267,7 +268,7 @@ void ODBCResult::UV_AfterFetch(uv_work_t* work_req, int status) { data->objResult->buffer, data->objResult->bufferLength); } - else { + else if (data->fetchMode == FETCH_OBJECT) { args[1] = ODBC::GetRecordTuple( data->objResult->m_hSTMT, data->objResult->columns, @@ -275,6 +276,9 @@ void ODBCResult::UV_AfterFetch(uv_work_t* work_req, int status) { data->objResult->buffer, data->objResult->bufferLength); } + else { + args[1] = Null(); + } TryCatch try_catch; @@ -380,13 +384,15 @@ Handle ODBCResult::FetchSync(const Arguments& args) { objResult->buffer, objResult->bufferLength); } - else { + else if (fetchMode == FETCH_OBJECT) { data = ODBC::GetRecordTuple( objResult->m_hSTMT, objResult->columns, &objResult->colCount, objResult->buffer, objResult->bufferLength); + } else { + data = Null(); } return scope.Close(data); @@ -520,7 +526,7 @@ void ODBCResult::UV_AfterFetchAll(uv_work_t* work_req, int status) { self->bufferLength) ); } - else { + else if (data->fetchMode == FETCH_OBJECT) { data->rows->Set( Integer::New(data->count), ODBC::GetRecordTuple( @@ -647,7 +653,7 @@ Handle ODBCResult::FetchAllSync(const Arguments& args) { self->bufferLength) ); } - else { + else if (fetchMode == FETCH_OBJECT) { rows->Set( Integer::New(count), ODBC::GetRecordTuple( @@ -752,3 +758,107 @@ Handle ODBCResult::GetColumnNamesSync(const Arguments& args) { return scope.Close(cols); } + +/* + * GetColumnValue + */ + +Handle ODBCResult::GetColumnValue(const Arguments& args) { + DEBUG_PRINTF("ODBCResult::GetColumnValue\n"); + + HandleScope scope; + + ODBCResult* objODBCResult = ObjectWrap::Unwrap(args.Holder()); + + uv_work_t* work_req = (uv_work_t *) (calloc(1, sizeof(uv_work_t))); + + get_column_value_work_data* data = (get_column_value_work_data *) calloc(1, sizeof(get_column_value_work_data)); + + if (args.Length() < 2 || args.Length() > 3 || !args[args.Length() - 1]->IsFunction()) { + return ThrowException(Exception::TypeError( + String::New("ODBCResult::GetColumnValue(column, maxBytes, cb): 2 or 3 arguments are required. The last argument must be a callback function.") + )); + } + + data->col = args[0]->IntegerValue(); + if (args.Length() == 3) { + data->bytesRequested = args[1]->IntegerValue(); + } else { + data->bytesRequested = 0; + } + + data->cb = Persistent::New(Local::Cast(args[args.Length() - 1])); + data->objResult = objODBCResult; + work_req->data = data; + + uv_queue_work( + uv_default_loop(), + work_req, + UV_GetColumnValue, + (uv_after_work_cb)UV_AfterGetColumnValue); + + objODBCResult->Ref(); + + return scope.Close(Undefined()); +} + +void ODBCResult::UV_GetColumnValue(uv_work_t* work_req) { + DEBUG_PRINTF("ODBCResult::UV_GetColumnValue\n"); + + get_column_value_work_data* data = (get_column_value_work_data*)(work_req->data); + + SQLLEN bytesRequested = data->bytesRequested; + if (bytesRequested <= 0) + bytesRequested = data->objResult->bufferLength; + + data->result = SQLGetData( + data->objResult->m_hSTMT, + data->col + 1, + SQL_C_TCHAR, + data->objResult->buffer, + bytesRequested, + &data->bytesRead); +} + +void ODBCResult::UV_AfterGetColumnValue(uv_work_t* work_req, int status) { + DEBUG_PRINTF("ODBCResult::UV_AfterGetColumnValue\n"); + + HandleScope scope; + + get_column_value_work_data* data = (get_column_value_work_data*)(work_req->data); + + SQLRETURN ret = data->result; + + DEBUG_PRINTF("ODBCResult::UV_AfterGetColumnValue: ret=%i, bytesRead=%i\n", ret, data->bytesRead); + + Handle args[2]; + + if (ret == SQL_ERROR) { + args[0] = ODBC::GetSQLError(SQL_HANDLE_STMT, data->objResult->m_hSTMT, "[node-odbc] Error in ODBCResult::GetColumnValue"); + args[1] = Null(); + } else if (data->bytesRead == SQL_NULL_DATA || ret == SQL_NO_DATA || data->bytesRead == 0) { + args[0] = Null(); + args[1] = Null(); + } else { + args[0] = Null(); +#ifdef UNICODE + args[1] = String::New((uint16_t*) (data->objResult->buffer)); +#else + args[1] = String::New((char *) (data->objResult->buffer)); +#endif + } + + TryCatch try_catch; + + data->cb->Call(Context::GetCurrent()->Global(), 2, args); + data->cb.Dispose(); + + if (try_catch.HasCaught()) { + FatalException(try_catch); + } + + free(data); + free(work_req); + + data->objResult->Unref(); +} diff --git a/src/odbc_result.h b/src/odbc_result.h index da4a3f8..e7bffa0 100644 --- a/src/odbc_result.h +++ b/src/odbc_result.h @@ -48,6 +48,10 @@ class ODBCResult : public node::ObjectWrap { static Handle FetchAll(const Arguments& args); static void UV_FetchAll(uv_work_t* work_req); static void UV_AfterFetchAll(uv_work_t* work_req, int status); + + static Handle GetColumnValue(const Arguments& args); + static void UV_GetColumnValue(uv_work_t* work_req); + static void UV_AfterGetColumnValue(uv_work_t* work_req, int status); //sync methods static Handle CloseSync(const Arguments& args); @@ -71,6 +75,16 @@ class ODBCResult : public node::ObjectWrap { Persistent rows; Persistent objError; }; + + struct get_column_value_work_data { + Persistent cb; + ODBCResult *objResult; + SQLRETURN result; + + SQLUSMALLINT col; + SQLLEN bytesRequested; + SQLLEN bytesRead; + }; ODBCResult *self(void) { return this; } From 2996ac506dea00aa8b64ac18fa70273faa469f4e Mon Sep 17 00:00:00 2001 From: Lee Houghton Date: Sat, 11 Jan 2014 00:14:53 +0000 Subject: [PATCH 02/26] Various changes (incomplete) * Return Buffer for varbinary columns * Fix SQL_T macro for compile-time string concatenation * Fix DEBUG_PRINTF usage in ODBCConnection::Query not respecting UNICODE flag * Pass 3rd argument to Query callback ("more") * Start work on GetColumnValue returning data types probably (probably should just use ODBC::GetColumnValue somehow - needs refactoring) * Add ODBCResult::GetColumnValueSync --- src/odbc.cpp | 25 ++++++++++ src/odbc.h | 4 +- src/odbc_connection.cpp | 12 +++-- src/odbc_result.cpp | 108 +++++++++++++++++++++++++++++++++++----- src/odbc_result.h | 2 + 5 files changed, 132 insertions(+), 19 deletions(-) diff --git a/src/odbc.cpp b/src/odbc.cpp index fc3e2af..4fa3b19 100644 --- a/src/odbc.cpp +++ b/src/odbc.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -332,6 +333,8 @@ Column* ODBC::GetColumns(SQLHSTMT hStmt, short* colCount) { */ void ODBC::FreeColumns(Column* columns, short* colCount) { + DEBUG_PRINTF("ODBC::FreeColumns(0x%x, %i)\n", columns, (int)*colCount); + for(int i = 0; i < *colCount; i++) { delete [] columns[i].name; } @@ -349,6 +352,7 @@ Handle ODBC::GetColumnValue( SQLHSTMT hStmt, Column column, uint16_t* buffer, int bufferLength) { HandleScope scope; SQLLEN len = 0; + node::Buffer* slowBuffer; //reset the buffer buffer[0] = '\0'; @@ -520,6 +524,27 @@ Handle ODBC::GetColumnValue( SQLHSTMT hStmt, Column column, return scope.Close(Boolean::New(( *buffer == '0') ? false : true )); //return Boolean::New(( *buffer == '0') ? false : true ); } + case SQL_BINARY: case SQL_VARBINARY: + ret = SQLGetData( + hStmt, + column.index, + SQL_C_BINARY, + (void*) buffer, + bufferLength, + &len); + + DEBUG_PRINTF("ODBC::GetColumnValue - Binary: index=%i name=%s type=%i len=%i\n", + column.index, column.name, column.type, len); + + if(len == SQL_NULL_DATA) { + return scope.Close(Null()); + } + else { + slowBuffer = node::Buffer::New(len); + memcpy(node::Buffer::Data(slowBuffer), buffer, len); + return scope.Close(slowBuffer->handle_); + } + default : Local str; int count = 0; diff --git a/src/odbc.h b/src/odbc.h index 3f773f4..2d50a01 100644 --- a/src/odbc.h +++ b/src/odbc.h @@ -147,9 +147,9 @@ struct query_request { }; #ifdef UNICODE - #define SQL_T(x) (L##x) + #define SQL_T(x) L##x #else - #define SQL_T(x) (x) + #define SQL_T(x) x #endif #ifdef DEBUG diff --git a/src/odbc_connection.cpp b/src/odbc_connection.cpp index f3e8a28..c81a02f 100644 --- a/src/odbc_connection.cpp +++ b/src/odbc_connection.cpp @@ -828,9 +828,10 @@ void ODBCConnection::UV_Query(uv_work_t* req) { for (int i = 0; i < data->paramCount; i++) { prm = data->params[i]; - DEBUG_PRINTF( - "ODBCConnection::UV_Query - param[%i]: c_type=%i type=%i " - "buffer_length=%i size=%i length=%i &length=%X\n", i, prm.c_type, prm.type, + wchar_t* x = L"abc" L"def"; + + DEBUG_TPRINTF( + SQL_T("ODBCConnection::UV_Query - param[%i]: c_type=%i type=%i buffer_length=%i size=%i length=%i &length=%X\n"), i, prm.c_type, prm.type, prm.buffer_length, prm.size, prm.length, &data->params[i].length); ret = SQLBindParameter( @@ -906,8 +907,9 @@ void ODBCConnection::UV_AfterQuery(uv_work_t* req, int status) { args[0] = Local::New(Null()); } args[1] = Local::New(js_result); - - data->cb->Call(Context::GetCurrent()->Global(), 2, args); + args[2] = Local::New(True()); + + data->cb->Call(Context::GetCurrent()->Global(), 3, args); } data->conn->Unref(); diff --git a/src/odbc_result.cpp b/src/odbc_result.cpp index de59fd2..c2c25be 100644 --- a/src/odbc_result.cpp +++ b/src/odbc_result.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -56,6 +57,7 @@ void ODBCResult::Init(v8::Handle target) { NODE_SET_PROTOTYPE_METHOD(constructor_template, "fetchSync", FetchSync); NODE_SET_PROTOTYPE_METHOD(constructor_template, "fetchAllSync", FetchAllSync); NODE_SET_PROTOTYPE_METHOD(constructor_template, "getColumnNamesSync", GetColumnNamesSync); + NODE_SET_PROTOTYPE_METHOD(constructor_template, "getColumnValueSync", GetColumnValueSync); // Properties instance_template->SetAccessor(String::New("fetchMode"), FetchModeGetter, FetchModeSetter); @@ -257,7 +259,7 @@ void ODBCResult::UV_AfterFetch(uv_work_t* work_req, int status) { } if (moreWork) { - Handle args[2]; + Handle args[3]; args[0] = Null(); if (data->fetchMode == FETCH_ARRAY) { @@ -280,9 +282,11 @@ void ODBCResult::UV_AfterFetch(uv_work_t* work_req, int status) { args[1] = Null(); } + args[2] = True(); + TryCatch try_catch; - data->cb->Call(Context::GetCurrent()->Global(), 2, args); + data->cb->Call(Context::GetCurrent()->Global(), 3, args); data->cb.Dispose(); if (try_catch.HasCaught()) { @@ -292,7 +296,7 @@ void ODBCResult::UV_AfterFetch(uv_work_t* work_req, int status) { else { ODBC::FreeColumns(data->objResult->columns, &data->objResult->colCount); - Handle args[2]; + Handle args[3]; //if there was an error, pass that as arg[0] otherwise Null if (error) { @@ -303,10 +307,11 @@ void ODBCResult::UV_AfterFetch(uv_work_t* work_req, int status) { } args[1] = Null(); + args[2] = False(); TryCatch try_catch; - data->cb->Call(Context::GetCurrent()->Global(), 2, args); + data->cb->Call(Context::GetCurrent()->Global(), 3, args); data->cb.Dispose(); if (try_catch.HasCaught()) { @@ -808,13 +813,47 @@ void ODBCResult::UV_GetColumnValue(uv_work_t* work_req) { get_column_value_work_data* data = (get_column_value_work_data*)(work_req->data); SQLLEN bytesRequested = data->bytesRequested; - if (bytesRequested <= 0) + if (bytesRequested <= 0 || bytesRequested > data->objResult->bufferLength) bytesRequested = data->objResult->bufferLength; - + + data->result = SQLColAttribute( + data->objResult->m_hSTMT, + data->col + 1, + SQL_DESC_TYPE, + NULL, + 0, + NULL, + &data->type + ); + + if (!SUCCEEDED(data->result)) + return; + + switch(data->type) { + case SQL_INTEGER: case SQL_SMALLINT: case SQL_TINYINT: + data->type = SQL_C_SLONG; + break; + case SQL_NUMERIC: case SQL_DECIMAL: case SQL_BIGINT: case SQL_FLOAT: case SQL_REAL: case SQL_DOUBLE : + data->type = SQL_C_DOUBLE; + break; + case SQL_DATETIME: case SQL_TIMESTAMP: +#if defined(_WIN32) + data->type = SQL_C_CHAR; +#else + data->type = SQL_C_TYPE_TIMESTAMP +#endif + case SQL_BINARY: case SQL_VARBINARY: + data->type = SQL_C_BINARY; + break; + default: // includes SQL_BIT + data->type = SQL_C_TCHAR; + break; + } + data->result = SQLGetData( data->objResult->m_hSTMT, data->col + 1, - SQL_C_TCHAR, + data->type, data->objResult->buffer, bytesRequested, &data->bytesRead); @@ -841,11 +880,30 @@ void ODBCResult::UV_AfterGetColumnValue(uv_work_t* work_req, int status) { args[1] = Null(); } else { args[0] = Null(); -#ifdef UNICODE - args[1] = String::New((uint16_t*) (data->objResult->buffer)); -#else - args[1] = String::New((char *) (data->objResult->buffer)); -#endif + + switch(data->type) { + case SQL_C_BINARY: { + node::Buffer* slowBuffer = node::Buffer::New(data->bytesRead); + memcpy(node::Buffer::Data(slowBuffer), data->objResult->buffer, data->bytesRead); + v8::Local globalObj = v8::Context::GetCurrent()->Global(); + v8::Local bufferConstructor = v8::Local::Cast(globalObj->Get(v8::String::New("Buffer"))); + v8::Handle constructorArgs[1] = { slowBuffer->handle_ }; + v8::Local actualBuffer = bufferConstructor->NewInstance(3, constructorArgs); + args[1] = actualBuffer; + args[1] = slowBuffer->handle_; + + break; + } + + case SQL_C_TCHAR: default: + #ifdef UNICODE + args[1] = String::New((uint16_t*) (data->objResult->buffer)); + #else + args[1] = String::New((char *) (data->objResult->buffer)); + #endif + break; + } + } TryCatch try_catch; @@ -862,3 +920,29 @@ void ODBCResult::UV_AfterGetColumnValue(uv_work_t* work_req, int status) { data->objResult->Unref(); } + +Handle ODBCResult::GetColumnValueSync(const Arguments& args) { + DEBUG_PRINTF("ODBCResult::GetColumnValueSync\n"); + + HandleScope scope; + + ODBCResult* objODBCResult = ObjectWrap::Unwrap(args.Holder()); + + DEBUG_PRINTF("ODBCResult::GetColumnValueSync: columns=0x%x, colCount=%i\n", objODBCResult->columns, objODBCResult->colCount); + + Column c; + c.index = args[0]->Uint32Value() + 1; + c.name = (unsigned char*)"(Name not known (ODBCResult::GetColumnValueSync))\0\0"; + c.type = SQL_VARCHAR; + c.len = 200; + + int i = args[0]->Uint32Value(); + if (i < 0 || i >= objODBCResult->colCount) + return ThrowException(Exception::RangeError( + String::New("ODBCResult::GetColumnValueSync(): The column index requested is invalid.") + )); + + return scope.Close( + ODBC::GetColumnValue(objODBCResult->m_hSTMT, objODBCResult->columns[i], objODBCResult->buffer, objODBCResult->bufferLength) + ); +} \ No newline at end of file diff --git a/src/odbc_result.h b/src/odbc_result.h index e7bffa0..df42403 100644 --- a/src/odbc_result.h +++ b/src/odbc_result.h @@ -59,6 +59,7 @@ class ODBCResult : public node::ObjectWrap { static Handle FetchSync(const Arguments& args); static Handle FetchAllSync(const Arguments& args); static Handle GetColumnNamesSync(const Arguments& args); + static Handle GetColumnValueSync(const Arguments& args); //property getter/setters static Handle FetchModeGetter(Local property, const AccessorInfo &info); @@ -82,6 +83,7 @@ class ODBCResult : public node::ObjectWrap { SQLRETURN result; SQLUSMALLINT col; + SQLLEN type; SQLLEN bytesRequested; SQLLEN bytesRead; }; From 0e8e2f4e207c3ff66f6d0fdc667d66d74c905310 Mon Sep 17 00:00:00 2001 From: Lee Houghton Date: Sat, 11 Jan 2014 19:33:49 +0000 Subject: [PATCH 03/26] Simplify date/time conversion on Win32 Remove strptime, use SQL_TIMESTAMP_STRUCT and mktime/_mkgmtime similar to *nix. --- binding.gyp | 1 - src/odbc.cpp | 66 ++------- src/strptime.c | 389 ------------------------------------------------- src/strptime.h | 47 ------ 4 files changed, 11 insertions(+), 492 deletions(-) delete mode 100644 src/strptime.c delete mode 100644 src/strptime.h diff --git a/binding.gyp b/binding.gyp index cb1e8dd..dd3928a 100644 --- a/binding.gyp +++ b/binding.gyp @@ -29,7 +29,6 @@ }], [ 'OS=="win"', { 'sources' : [ - 'src/strptime.c', 'src/odbc.cpp' ], 'libraries' : [ diff --git a/src/odbc.cpp b/src/odbc.cpp index 44faf1a..92a7c84 100644 --- a/src/odbc.cpp +++ b/src/odbc.cpp @@ -32,10 +32,6 @@ #include "dynodbc.h" #endif -#ifdef _WIN32 -#include "strptime.h" -#endif - using namespace v8; using namespace node; @@ -419,51 +415,7 @@ Handle ODBC::GetColumnValue( SQLHSTMT hStmt, Column column, break; case SQL_DATETIME : case SQL_TIMESTAMP : { - //I am not sure if this is locale-safe or cross database safe, but it - //works for me on MSSQL -#ifdef _WIN32 - struct tm timeInfo = {}; - - ret = SQLGetData( - hStmt, - column.index, - SQL_C_CHAR, - (char *) buffer, - bufferLength, - &len); - - DEBUG_PRINTF("ODBC::GetColumnValue - W32 Timestamp: index=%i name=%s type=%i len=%i\n", - column.index, column.name, column.type, len); - - if(len == SQL_NULL_DATA) { - return scope.Close(Null()); - //return Null(); - } - else { - strptime((char *) buffer, "%Y-%m-%d %H:%M:%S", &timeInfo); - - //a negative value means that mktime() should use timezone information - //and system databases to attempt to determine whether DST is in effect - //at the specified time. - timeInfo.tm_isdst = -1; - - //return Date::New((double(mktime(&timeInfo)) * 1000)); - return scope.Close(Date::New((double(mktime(&timeInfo)) * 1000))); - } -#else - struct tm timeInfo = { - tm_sec : 0 - , tm_min : 0 - , tm_hour : 0 - , tm_mday : 0 - , tm_mon : 0 - , tm_year : 0 - , tm_wday : 0 - , tm_yday : 0 - , tm_isdst : 0 - , tm_gmtoff : 0 - , tm_zone : 0 - }; + struct tm timeInfo = { 0 }; SQL_TIMESTAMP_STRUCT odbcTime; @@ -494,17 +446,21 @@ Handle ODBC::GetColumnValue( SQLHSTMT hStmt, Column column, //and system databases to attempt to determine whether DST is in effect //at the specified time. timeInfo.tm_isdst = -1; -#ifdef TIMEGM + +#if defined(_WIN32) && defined (TIMEGM) + return scope.Close(Date::New((double(_mkgmtime32(&timeInfo)) * 1000) + + (odbcTime.fraction / 1000000.0))); +#elif defined(WIN32) + return scope.Close(Date::New((double(mktime(&timeInfo)) * 1000) + + (odbcTime.fraction / 1000000.0))); +#elif defined(TIMEGM) return scope.Close(Date::New((double(timegm(&timeInfo)) * 1000) - + (odbcTime.fraction / 1000000))); + + (odbcTime.fraction / 1000000.0))); #else return scope.Close(Date::New((double(timelocal(&timeInfo)) * 1000) - + (odbcTime.fraction / 1000000))); + + (odbcTime.fraction / 1000000.0))); #endif - //return Date::New((double(timegm(&timeInfo)) * 1000) - // + (odbcTime.fraction / 1000000)); } -#endif } break; case SQL_BIT : //again, i'm not sure if this is cross database safe, but it works for diff --git a/src/strptime.c b/src/strptime.c deleted file mode 100644 index cca482f..0000000 --- a/src/strptime.c +++ /dev/null @@ -1,389 +0,0 @@ -/*- - * Copyright (c) 1997, 1998, 2005, 2008 The NetBSD Foundation, Inc. - * All rights reserved. - * - * This code was contributed to The NetBSD Foundation by Klaus Klein. - * Heavily optimised by David Laight - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS - * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS - * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -/* - * Inclusion in node-odbc note: - * - * This code was found here: http://social.msdn.microsoft.com/forums/en-US/vcgeneral/thread/25a654f9-b6b6-490a-8f36-c87483bb36b7 - * One user posted what looks to be a scaled down version of the NetBSD code - * but did not include any header with their work. Since it seems pretty obvious - * that the user took much of the code from NetBSD, that is why the NetBSD header - * is displayed above. - */ - -#include "strptime.h" - -static int conv_num(const char **, int *, int, int); -static int strncasecmp(char *s1, char *s2, size_t n); - -char * strptime(const char *buf, const char *fmt, struct tm *tm) -{ - char c; - const char *bp; - size_t len = 0; - int alt_format, i, split_year = 0; - - bp = buf; - - while ((c = *fmt) != '\0') - { - /* Clear `alternate' modifier prior to new conversion. */ - alt_format = 0; - - /* Eat up white-space. */ - if (isspace(c)) - { - while (isspace(*bp)) - bp++; - - fmt++; - continue; - } - - if ((c = *fmt++) != '%') - goto literal; - - -again: switch (c = *fmt++) - { - case '%': /* "%%" is converted to "%". */ - literal: - if (c != *bp++) - return (0); - break; - - /* - * "Alternative" modifiers. Just set the appropriate flag - * and start over again. - */ - case 'E': /* "%E?" alternative conversion modifier. */ - LEGAL_ALT(0); - alt_format |= ALT_E; - goto again; - - case 'O': /* "%O?" alternative conversion modifier. */ - LEGAL_ALT(0); - alt_format |= ALT_O; - goto again; - - /* - * "Complex" conversion rules, implemented through recursion. - */ - case 'c': /* Date and time, using the locale's format. */ - LEGAL_ALT(ALT_E); - if (!(bp = strptime(bp, "%x %X", tm))) - return (0); - break; - - case 'D': /* The date as "%m/%d/%y". */ - LEGAL_ALT(0); - if (!(bp = strptime(bp, "%m/%d/%y", tm))) - return (0); - break; - - case 'R': /* The time as "%H:%M". */ - LEGAL_ALT(0); - if (!(bp = strptime(bp, "%H:%M", tm))) - return (0); - break; - - case 'r': /* The time in 12-hour clock representation. */ - LEGAL_ALT(0); - if (!(bp = strptime(bp, "%I:%M:%S %p", tm))) - return (0); - break; - - case 'T': /* The time as "%H:%M:%S". */ - LEGAL_ALT(0); - if (!(bp = strptime(bp, "%H:%M:%S", tm))) - return (0); - break; - - case 'X': /* The time, using the locale's format. */ - LEGAL_ALT(ALT_E); - if (!(bp = strptime(bp, "%H:%M:%S", tm))) - return (0); - break; - - case 'x': /* The date, using the locale's format. */ - LEGAL_ALT(ALT_E); - if (!(bp = strptime(bp, "%m/%d/%y", tm))) - return (0); - break; - - /* - * "Elementary" conversion rules. - */ - case 'A': /* The day of week, using the locale's form. */ - case 'a': - LEGAL_ALT(0); - for (i = 0; i < 7; i++) - { - /* Full name. */ - len = strlen(day[i]); - if (strncasecmp((char *)(day[i]), (char *)bp, len) == 0) - break; - - /* Abbreviated name. */ - len = strlen(abday[i]); - if (strncasecmp((char *)(abday[i]), (char *)bp, len) == 0) - break; - } - - /* Nothing matched. */ - if (i == 7) - return (0); - - tm->tm_wday = i; - bp += len; - break; - - case 'B': /* The month, using the locale's form. */ - case 'b': - case 'h': - LEGAL_ALT(0); - for (i = 0; i < 12; i++) - { - /* Full name. */ - - len = strlen(mon[i]); - if (strncasecmp((char *)(mon[i]), (char *)bp, len) == 0) - break; - - /* Abbreviated name. */ - len = strlen(abmon[i]); - if (strncasecmp((char *)(abmon[i]),(char *) bp, len) == 0) - break; - } - - /* Nothing matched. */ - if (i == 12) - return (0); - - tm->tm_mon = i; - bp += len; - break; - - case 'C': /* The century number. */ - LEGAL_ALT(ALT_E); - if (!(conv_num(&bp, &i, 0, 99))) - return (0); - - if (split_year) - { - tm->tm_year = (tm->tm_year % 100) + (i * 100); - } else { - tm->tm_year = i * 100; - split_year = 1; - } - break; - - case 'd': /* The day of month. */ - case 'e': - LEGAL_ALT(ALT_O); - if (!(conv_num(&bp, &tm->tm_mday, 1, 31))) - return (0); - break; - - case 'k': /* The hour (24-hour clock representation). */ - LEGAL_ALT(0); - /* FALLTHROUGH */ - case 'H': - LEGAL_ALT(ALT_O); - if (!(conv_num(&bp, &tm->tm_hour, 0, 23))) - return (0); - break; - - case 'l': /* The hour (12-hour clock representation). */ - LEGAL_ALT(0); - /* FALLTHROUGH */ - case 'I': - LEGAL_ALT(ALT_O); - if (!(conv_num(&bp, &tm->tm_hour, 1, 12))) - return (0); - if (tm->tm_hour == 12) - tm->tm_hour = 0; - break; - - case 'j': /* The day of year. */ - LEGAL_ALT(0); - if (!(conv_num(&bp, &i, 1, 366))) - return (0); - tm->tm_yday = i - 1; - break; - - case 'M': /* The minute. */ - LEGAL_ALT(ALT_O); - if (!(conv_num(&bp, &tm->tm_min, 0, 59))) - return (0); - break; - - case 'm': /* The month. */ - LEGAL_ALT(ALT_O); - if (!(conv_num(&bp, &i, 1, 12))) - return (0); - tm->tm_mon = i - 1; - break; - -// case 'p': /* The locale's equivalent of AM/PM. */ -// LEGAL_ALT(0); -// /* AM? */ -// if (strcasecmp(am_pm[0], bp) == 0) -// { -// if (tm->tm_hour > 11) -// return (0); -// -// bp += strlen(am_pm[0]); -// break; -// } -// /* PM? */ -// else if (strcasecmp(am_pm[1], bp) == 0) -// { -// if (tm->tm_hour > 11) -// return (0); -// -// tm->tm_hour += 12; -// bp += strlen(am_pm[1]); -// break; -// } -// -// /* Nothing matched. */ -// return (0); - - case 'S': /* The seconds. */ - LEGAL_ALT(ALT_O); - if (!(conv_num(&bp, &tm->tm_sec, 0, 61))) - return (0); - break; - - case 'U': /* The week of year, beginning on sunday. */ - case 'W': /* The week of year, beginning on monday. */ - LEGAL_ALT(ALT_O); - /* - * XXX This is bogus, as we can not assume any valid - * information present in the tm structure at this - * point to calculate a real value, so just check the - * range for now. - */ - if (!(conv_num(&bp, &i, 0, 53))) - return (0); - break; - - case 'w': /* The day of week, beginning on sunday. */ - LEGAL_ALT(ALT_O); - if (!(conv_num(&bp, &tm->tm_wday, 0, 6))) - return (0); - break; - - case 'Y': /* The year. */ - LEGAL_ALT(ALT_E); - if (!(conv_num(&bp, &i, 0, 9999))) - return (0); - - tm->tm_year = i - TM_YEAR_BASE; - break; - - case 'y': /* The year within 100 years of the epoch. */ - LEGAL_ALT(ALT_E | ALT_O); - if (!(conv_num(&bp, &i, 0, 99))) - return (0); - - if (split_year) - { - tm->tm_year = ((tm->tm_year / 100) * 100) + i; - break; - } - split_year = 1; - if (i <= 68) - tm->tm_year = i + 2000 - TM_YEAR_BASE; - else - tm->tm_year = i + 1900 - TM_YEAR_BASE; - break; - - /* - * Miscellaneous conversions. - */ - case 'n': /* Any kind of white-space. */ - case 't': - LEGAL_ALT(0); - while (isspace(*bp)) - bp++; - break; - - - default: /* Unknown/unsupported conversion. */ - return (0); - } - - - } - - /* LINTED functional specification */ - return ((char *)bp); -} - - -static int conv_num(const char **buf, int *dest, int llim, int ulim) -{ - int result = 0; - - /* The limit also determines the number of valid digits. */ - int rulim = ulim; - - if (**buf < '0' || **buf > '9') - return (0); - - do { - result *= 10; - result += *(*buf)++ - '0'; - rulim /= 10; - } while ((result * 10 <= ulim) && rulim && **buf >= '0' && **buf <= '9'); - - if (result < llim || result > ulim) - return (0); - - *dest = result; - return (1); -} - -int strncasecmp(char *s1, char *s2, size_t n) -{ - if (n == 0) - return 0; - - while (n-- != 0 && tolower(*s1) == tolower(*s2)) - { - if (n == 0 || *s1 == '\0' || *s2 == '\0') - break; - s1++; - s2++; - } - - return tolower(*(unsigned char *) s1) - tolower(*(unsigned char *) s2); -} diff --git a/src/strptime.h b/src/strptime.h deleted file mode 100644 index ce0cbfa..0000000 --- a/src/strptime.h +++ /dev/null @@ -1,47 +0,0 @@ -#ifndef _STRPTIME_H -#define _STRPTIME_H - -#define ALT_E 0x01 -#define ALT_O 0x02 -//#define LEGAL_ALT(x) { if (alt_format & ~(x)) return (0); } -#define LEGAL_ALT(x) { ; } -#define TM_YEAR_BASE (1900) - -#include -#include -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -char * strptime(const char *buf, const char *fmt, struct tm *tm); - -#ifdef __cplusplus -} -#endif - -static const char *day[7] = { - "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", - "Friday", "Saturday" -}; - -static const char *abday[7] = { - "Sun","Mon","Tue","Wed","Thu","Fri","Sat" -}; - -static const char *mon[12] = { - "January", "February", "March", "April", "May", "June", "July", - "August", "September", "October", "November", "December" -}; - -static const char *abmon[12] = { - "Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" -}; -static const char *am_pm[2] = { - "AM", "PM" -}; - -#endif \ No newline at end of file From b79c5f7642818835530f276c55c98a40986cc745 Mon Sep 17 00:00:00 2001 From: Lee Houghton Date: Sat, 11 Jan 2014 19:34:39 +0000 Subject: [PATCH 04/26] Fix test-bad-connection-string on Windows The Windows ODBC Driver Manager returns SQLSTATE 01S00 whereas unixODBC returns SQLSTATE IM002. --- test/test-bad-connection-string.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/test-bad-connection-string.js b/test/test-bad-connection-string.js index e742a40..47e9557 100644 --- a/test/test-bad-connection-string.js +++ b/test/test-bad-connection-string.js @@ -13,11 +13,8 @@ assert.equal(db.connected, false); db.open("this is wrong", function(err) { console.log(err); - assert.deepEqual(err, { - error: '[node-odbc] SQL_ERROR', - message: '[unixODBC][Driver Manager]Data source name not found, and no default driver specified', - state: 'IM002' - }); + // unixODBC may return IM002, but ODBC on Windows gives you 01S00 (invalid connection string attribute) + assert(err.state == 'IM002' || err.state == '01S00'); assert.equal(db.connected, false); }); From 42336ef983b5d5cc80d29d5db1b15b078ea26a6a Mon Sep 17 00:00:00 2001 From: Lee Houghton Date: Sat, 11 Jan 2014 19:35:54 +0000 Subject: [PATCH 05/26] Fix test-date on MSSQL MSSQL's datetime type has only 3 millisecond precision (datetime2 supports better precision, but is unlikely to exist elsewhere) --- test/test-date.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/test-date.js b/test/test-date.js index aafb909..08f0cbe 100644 --- a/test/test-date.js +++ b/test/test-date.js @@ -25,7 +25,11 @@ db.open(common.connectionString, function(err) { //test selected data after the connection //is closed, in case the assertion fails assert.equal(data[0].DT1.constructor.name, "Date", "DT1 is not an instance of a Date object"); - assert.equal(data[0].DT1.getTime(), dt.getTime()); + + // Not all databases have sufficient precision in their datetime type to go down to a millisecond, + // e.g. MSSQL. (MSSQL provides datetime2 for better accuracy.) + var delta = Math.abs(data[0].DT1.getTime() - dt.getTime()); + assert(delta < 10, "Times differ by more than 10 milliseconds"); }); }); }); From 3fce14703f1b0b9edcf038ade88fbecb57c45899 Mon Sep 17 00:00:00 2001 From: Lee Houghton Date: Sat, 11 Jan 2014 20:06:52 +0000 Subject: [PATCH 06/26] Fix test-param-select-with-numbers-only on MSSQL Precision again. --- test/test-param-select-with-numbers-only.js | 23 ++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/test/test-param-select-with-numbers-only.js b/test/test-param-select-with-numbers-only.js index ad500d5..da4f24e 100644 --- a/test/test-param-select-with-numbers-only.js +++ b/test/test-param-select-with-numbers-only.js @@ -11,14 +11,23 @@ db.open(common.connectionString, function (err) { , [5, 3, 1, 1.23456789012345, 12345.000] , function (err, data, more) { db.close(function () { + console.log(data); assert.equal(err, null); - assert.deepEqual(data, [{ - INTCOL1: 5, - INTCOL2: 3, - INTCOL3: 1, - FLOATCOL4 : 1.23456789012345, - FLOATYINT : 12345 - }]); + assert(Array.isArray(data), 'data is not an array') + assert(typeof data[0] === 'object', 'data[0] is not an object'); + assert.strictEqual(data[0].INTCOL1, 5, 'INTCOL1 is wrong'); + assert.strictEqual(data[0].INTCOL2, 3, 'INTCOL2 is wrong'); + assert.strictEqual(data[0].INTCOL3, 1, 'INTCOL3 is wrong'); + assert.equal(Math.round(data[0].FLOATCOL4 * 1000000) / 1000000, 1.234568); // In case the database's precision is not stellar... + assert.strictEqual(data[0].FLOATYINT, 12345, 'FLOATYINT is wrong'); + + //assert.deepEqual(data, [{ + // INTCOL1: 5, + // INTCOL2: 3, + // INTCOL3: 1, + // FLOATCOL4 : 1.23456789012345, + // FLOATYINT : 12345 + //}]); }); }); }); From c0cbf24d5dc1ccf62314e949532acc530cbe45b0 Mon Sep 17 00:00:00 2001 From: Lee Houghton Date: Sat, 11 Jan 2014 20:25:00 +0000 Subject: [PATCH 07/26] Fix test-query-select-unicode on MSSQL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MSSQL requires N'☯ąčęėįšųūž☎áäàéêèóöòüßÄÖÜ€ шчябы Ⅲ ❤' but sqlite doesn't support national character literals. Relying on default collation being unicode-capable is also non-standard, however... --- test/test-query-select-unicode.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test-query-select-unicode.js b/test/test-query-select-unicode.js index 751b464..86d79fc 100644 --- a/test/test-query-select-unicode.js +++ b/test/test-query-select-unicode.js @@ -6,7 +6,7 @@ var common = require("./common") db.openSync(common.connectionString); -db.query("select '☯ąčęėįšųūž☎áäàéêèóöòüßÄÖÜ€ шчябы Ⅲ ❤' as UNICODETEXT", function (err, data) { +db.query("select ? as UNICODETEXT", ['☯ąčęėįšųūž☎áäàéêèóöòüßÄÖÜ€ шчябы Ⅲ ❤'], function (err, data) { db.closeSync(); console.log(data); assert.equal(err, null); From 7f136a5c87aa3323f62b3db6cda364d6296826b4 Mon Sep 17 00:00:00 2001 From: Lee Houghton Date: Sat, 11 Jan 2014 20:29:00 +0000 Subject: [PATCH 08/26] Fix test-querySync-select-unicode on MSSQL See commit #c0cbf24d5dc1ccf62314e949532acc530cbe45b0 --- test/test-querySync-select-unicode.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test-querySync-select-unicode.js b/test/test-querySync-select-unicode.js index bcd4166..1b34ab5 100644 --- a/test/test-querySync-select-unicode.js +++ b/test/test-querySync-select-unicode.js @@ -8,7 +8,7 @@ db.openSync(common.connectionString); var data; try { - data = db.querySync("select 'ꜨꜢ' as UNICODETEXT"); + data = db.querySync("select ? as UNICODETEXT", ['ꜨꜢ']); } catch (e) { console.log(e); From f8e8604622294a8efbfd1a8a448db45ac5ba17c4 Mon Sep 17 00:00:00 2001 From: Lee Houghton Date: Sun, 12 Jan 2014 18:13:13 +0000 Subject: [PATCH 09/26] Fix crash in BindSync when DEBUG turned on Was printing a not-necessarily-null terminated buffer with %s. --- src/odbc_statement.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/odbc_statement.cpp b/src/odbc_statement.cpp index 42b702d..bcd4e7e 100644 --- a/src/odbc_statement.cpp +++ b/src/odbc_statement.cpp @@ -800,7 +800,7 @@ Handle ODBCStatement::BindSync(const Arguments& args) { DEBUG_PRINTF( "ODBCStatement::BindSync - param[%i]: c_type=%i type=%i " - "buffer_length=%i size=%i length=%i &length=%X decimals=%i value=%s\n", + "buffer_length=%i size=%i length=%i &length=%X decimals=%i value=%x\n", i, prm.c_type, prm.type, prm.buffer_length, prm.size, prm.length, &stmt->params[i].length, prm.decimals, prm.buffer ); From e8c130b6f28376ba42eeea1eb97c9b9dd5f8d2b5 Mon Sep 17 00:00:00 2001 From: Lee Houghton Date: Sun, 12 Jan 2014 18:14:39 +0000 Subject: [PATCH 10/26] Properly print query SQL in debug mode --- src/odbc_connection.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/odbc_connection.cpp b/src/odbc_connection.cpp index 4f111e0..a606f27 100644 --- a/src/odbc_connection.cpp +++ b/src/odbc_connection.cpp @@ -771,14 +771,17 @@ Handle ODBCConnection::Query(const Arguments& args) { data->sqlSize = (data->sqlLen * sizeof(uint16_t)) + sizeof(uint16_t); data->sql = (uint16_t *) malloc(data->sqlSize); sql->Write((uint16_t *) data->sql); + + DEBUG_TPRINTF(L"ODBCConnection::Query : sqlLen=%i, sqlSize=%i, sql=%s\n", + data->sqlLen, data->sqlSize, (uint16_t*) data->sql); #else data->sqlSize = sql->Utf8Length() + 1; data->sql = (char *) malloc(data->sqlSize); sql->WriteUtf8((char *) data->sql); -#endif DEBUG_PRINTF("ODBCConnection::Query : sqlLen=%i, sqlSize=%i, sql=%s\n", data->sqlLen, data->sqlSize, (char*) data->sql); +#endif data->conn = conn; work_req->data = data; From 5acce2a957c4ecbc3d95cd2548c7ae3fdd6580d2 Mon Sep 17 00:00:00 2001 From: Lee Houghton Date: Sun, 12 Jan 2014 18:22:04 +0000 Subject: [PATCH 11/26] Refactor ODBC::GetColumnValue Split into GetCColumnType, GetColumnData, and ConvertColumnValue to allow ODBC::GetColumnValue (async) to do the data-getting on the worker thread and the conversion in the callback on the V8 thread. Returns a Buffer if the data type of the column is SQL_BINARY or SQL_VARBINARY. Converts the 'bit' data type into true/false using the SQL_C_BIT data type rather than SQL_C_TCHAR. --- src/odbc.cpp | 393 +++++++++++++++++++++----------------------- src/odbc.h | 14 +- src/odbc_result.cpp | 144 +++++++--------- src/odbc_result.h | 4 +- 4 files changed, 256 insertions(+), 299 deletions(-) diff --git a/src/odbc.cpp b/src/odbc.cpp index 92a7c84..e96604c 100644 --- a/src/odbc.cpp +++ b/src/odbc.cpp @@ -307,7 +307,7 @@ Column* ODBC::GetColumns(SQLHSTMT hStmt, short* colCount) { (SQLSMALLINT) MAX_FIELD_SIZE, (SQLSMALLINT *) &buflen, NULL); - + //store the len attribute columns[i].len = buflen; @@ -344,97 +344,75 @@ void ODBC::FreeColumns(Column* columns, short* colCount) { * GetColumnValue */ -Handle ODBC::GetColumnValue( SQLHSTMT hStmt, Column column, - uint16_t* buffer, int bufferLength) { - HandleScope scope; - SQLLEN len = 0; - node::Buffer* slowBuffer; +SQLSMALLINT ODBC::GetCColumnType(const Column& column) { + switch((int) column.type) { + case SQL_INTEGER: case SQL_SMALLINT: case SQL_TINYINT: + return SQL_C_SLONG; + + case SQL_NUMERIC: case SQL_DECIMAL: case SQL_BIGINT: + case SQL_FLOAT: case SQL_REAL: case SQL_DOUBLE: + return SQL_C_DOUBLE; - //reset the buffer - buffer[0] = '\0'; + case SQL_DATETIME: case SQL_TIMESTAMP: + return SQL_C_TYPE_TIMESTAMP; - //TODO: SQLGetData can supposedly return multiple chunks, need to do this to - //retrieve large fields - int ret; - - switch ((int) column.type) { - case SQL_INTEGER : - case SQL_SMALLINT : - case SQL_TINYINT : { - long value; - - ret = SQLGetData( - hStmt, - column.index, - SQL_C_SLONG, - &value, - sizeof(value), - &len); - - DEBUG_PRINTF("ODBC::GetColumnValue - Integer: index=%i name=%s type=%i len=%i ret=%i\n", - column.index, column.name, column.type, len, ret); - - if (len == SQL_NULL_DATA) { - return scope.Close(Null()); - //return Null(); - } - else { - return scope.Close(Integer::New(value)); - //return Integer::New(value); - } - } + case SQL_BINARY: case SQL_VARBINARY: case SQL_LONGVARBINARY: + return SQL_C_BINARY; + + case SQL_BIT: + return SQL_C_BIT; + + default: + return SQL_C_TCHAR; + } +} + +SQLRETURN ODBC::GetColumnData( SQLHSTMT hStmt, const Column& column, + void* buffer, int bufferLength, SQLSMALLINT& cType, SQLINTEGER& len) { + + cType = GetCColumnType(column); + + switch(cType) { + case SQL_C_SLONG: + bufferLength = sizeof(long); break; - case SQL_NUMERIC : - case SQL_DECIMAL : - case SQL_BIGINT : - case SQL_FLOAT : - case SQL_REAL : - case SQL_DOUBLE : { - double value; - - ret = SQLGetData( - hStmt, - column.index, - SQL_C_DOUBLE, - &value, - sizeof(value), - &len); - - DEBUG_PRINTF("ODBC::GetColumnValue - Number: index=%i name=%s type=%i len=%i ret=%i val=%f\n", - column.index, column.name, column.type, len, ret, value); - - if(len == SQL_NULL_DATA) { - return scope.Close(Null()); - //return Null(); - } - else { - return scope.Close(Number::New(value)); - //return Number::New(value); - } - } + + case SQL_C_DOUBLE: + bufferLength = sizeof(double); break; - case SQL_DATETIME : - case SQL_TIMESTAMP : { - struct tm timeInfo = { 0 }; + } - SQL_TIMESTAMP_STRUCT odbcTime; - - ret = SQLGetData( - hStmt, - column.index, - SQL_C_TYPE_TIMESTAMP, - &odbcTime, - bufferLength, - &len); - - DEBUG_PRINTF("ODBC::GetColumnValue - Unix Timestamp: index=%i name=%s type=%i len=%i\n", - column.index, column.name, column.type, len); - - if(len == SQL_NULL_DATA) { - return scope.Close(Null()); - //return Null(); - } - else { + return SQLGetData( + hStmt, + column.index, + cType, + buffer, + bufferLength, + &len); +} + +Handle ODBC::ConvertColumnValue( SQLSMALLINT cType, + uint16_t* buffer, SQLINTEGER bytesInBuffer, + Buffer* resultBuffer, size_t& resultBufferOffset) { + + HandleScope scope; + + switch(cType) { + case SQL_C_SLONG: + assert(bytesInBuffer >= sizeof (long)); + // XXX Integer::New will truncate here if sizeof(long) > 4, it expects 32-bit numbers + return scope.Close(Integer::New(*reinterpret_cast(buffer))); + break; + + case SQL_C_DOUBLE: + assert(bytesInBuffer >= sizeof (double)); + return scope.Close(Number::New(*reinterpret_cast(buffer))); + break; + + case SQL_C_TYPE_TIMESTAMP: { + assert(bytesInBuffer >= sizeof (SQL_TIMESTAMP_STRUCT)); + SQL_TIMESTAMP_STRUCT& odbcTime = *reinterpret_cast(buffer); + struct tm timeInfo = { 0 }; timeInfo.tm_year = odbcTime.year - 1900; timeInfo.tm_mon = odbcTime.month - 1; timeInfo.tm_mday = odbcTime.day; @@ -447,135 +425,136 @@ Handle ODBC::GetColumnValue( SQLHSTMT hStmt, Column column, //at the specified time. timeInfo.tm_isdst = -1; -#if defined(_WIN32) && defined (TIMEGM) + #if defined(_WIN32) && defined (TIMEGM) return scope.Close(Date::New((double(_mkgmtime32(&timeInfo)) * 1000) + (odbcTime.fraction / 1000000.0))); -#elif defined(WIN32) + #elif defined(WIN32) return scope.Close(Date::New((double(mktime(&timeInfo)) * 1000) + (odbcTime.fraction / 1000000.0))); -#elif defined(TIMEGM) + #elif defined(TIMEGM) return scope.Close(Date::New((double(timegm(&timeInfo)) * 1000) + (odbcTime.fraction / 1000000.0))); -#else + #else return scope.Close(Date::New((double(timelocal(&timeInfo)) * 1000) + (odbcTime.fraction / 1000000.0))); -#endif - } - } break; - case SQL_BIT : - //again, i'm not sure if this is cross database safe, but it works for - //MSSQL - ret = SQLGetData( - hStmt, - column.index, - SQL_C_CHAR, - (char *) buffer, - bufferLength, - &len); - - DEBUG_PRINTF("ODBC::GetColumnValue - Bit: index=%i name=%s type=%i len=%i\n", - column.index, column.name, column.type, len); - - if(len == SQL_NULL_DATA) { - return scope.Close(Null()); - //return Null(); - } - else { - return scope.Close(Boolean::New(( *buffer == '0') ? false : true )); - //return Boolean::New(( *buffer == '0') ? false : true ); - } - case SQL_BINARY: case SQL_VARBINARY: - ret = SQLGetData( - hStmt, - column.index, - SQL_C_BINARY, - (void*) buffer, - bufferLength, - &len); - - DEBUG_PRINTF("ODBC::GetColumnValue - Binary: index=%i name=%s type=%i len=%i\n", - column.index, column.name, column.type, len); - - if(len == SQL_NULL_DATA) { - return scope.Close(Null()); - } - else { - slowBuffer = node::Buffer::New(len); - memcpy(node::Buffer::Data(slowBuffer), buffer, len); - return scope.Close(slowBuffer->handle_); + #endif + break; } - - default : - Local str; - int count = 0; - - do { - ret = SQLGetData( - hStmt, - column.index, - SQL_C_TCHAR, - (char *) buffer, - bufferLength, - &len); - - DEBUG_PRINTF("ODBC::GetColumnValue - String: index=%i name=%s type=%i len=%i value=%s ret=%i bufferLength=%i\n", - column.index, column.name, column.type, len,(char *) buffer, ret, bufferLength); - - if(len == SQL_NULL_DATA) { - return scope.Close(Null()); - //return Null(); - } - - if (SQL_NO_DATA == ret) { - //we have captured all of the data - break; - } - else if (SQL_SUCCEEDED(ret)) { - //we have not captured all of the data yet - - if (count == 0) { - //no concatenation required, this is our first pass -#ifdef UNICODE - str = String::New((uint16_t*) buffer); -#else - str = String::New((char *) buffer); -#endif - } - else { - //we need to concatenate -#ifdef UNICODE - str = String::Concat(str, String::New((uint16_t*) buffer)); -#else - str = String::Concat(str, String::New((char *) buffer)); -#endif - } - - count += 1; + + case SQL_C_BIT: + assert(bytesInBuffer >= sizeof(SQLCHAR)); + return scope.Close(Boolean::New(!!*reinterpret_cast(buffer))); + + case SQL_C_BINARY: case SQL_C_TCHAR: + if (resultBufferOffset + bytesInBuffer > Buffer::Length(resultBuffer)) + return ThrowException(Exception::Error(String::New("ODBC::GetColumnValue: Buffer overflow (binary)"))); + + memcpy(Buffer::Data(resultBuffer) + resultBufferOffset, buffer, bytesInBuffer); + resultBufferOffset += bytesInBuffer; + + return scope.Close(Undefined()); + break; + + default: + return ThrowException(Exception::Error(String::New("ODBC::GetColumnValue: Internal error (unexpected C type)"))); + } +} + +Handle ODBC::GetColumnValue( SQLHSTMT hStmt, Column column, + uint16_t* buffer, int bufferLength, + bool partial, bool fetch) { + HandleScope scope; + SQLLEN len = 0; + Buffer* slowBuffer = 0; + size_t offset = 0; + + //reset the buffer + buffer[0] = '\0'; + + if (!fetch) + partial = true; + + DEBUG_PRINTF("ODBC::GetColumnValue: column=%s, type=%i, buffer=%x, bufferLength=%i, partial=%i, fetch=%i\n", column.name, column.type, buffer, bufferLength, partial?1:0, fetch?1:0); + + while (true) { + SQLINTEGER len = 0; + SQLSMALLINT cType = GetCColumnType(column); + SQLRETURN ret; + + if (fetch) { + // TODO For fixed-length columns, the driver will just assume the buffer is big enough + // Ensure this won't break anything. + + if (cType == SQL_C_BINARY || cType == SQL_C_TCHAR) { + if (slowBuffer) { + // Just use the node::Buffer we have to avoid memcpy()ing + ret = GetColumnData(hStmt, column, Buffer::Data(slowBuffer) + offset, Buffer::Length(slowBuffer) - offset, cType, len); + if (len > bufferLength) + len = bufferLength; + } else { + // We don't know how much data there is to get back yet... + // Use our buffer for now for the first SQLGetData call, which will tell us the full length. + ret = GetColumnData(hStmt, column, buffer, bufferLength, cType, len); + if (len == SQL_NULL_DATA) + return scope.Close(Null()); + if (partial && len > bufferLength) + len = bufferLength; + slowBuffer = Buffer::New(len); + if (len > bufferLength) + len = bufferLength; + memcpy(Buffer::Data(slowBuffer), buffer, len); } - else { - //an error has occured - //possible values for ret are SQL_ERROR (-1) and SQL_INVALID_HANDLE (-2) - - //If we have an invalid handle, then stuff is way bad and we should abort - //immediately. Memory errors are bound to follow as we must be in an - //inconsisant state. - assert(ret != SQL_INVALID_HANDLE); - - //Not sure if throwing here will work out well for us but we can try - //since we should have a valid handle and the error is something we - //can look into - return ThrowException(ODBC::GetSQLError( - SQL_HANDLE_STMT, - hStmt, - (char *) "[node-odbc] Error in ODBC::GetColumnValue" - )); - - break; + } else { + // Use the ODBCResult's buffer + ret = GetColumnData(hStmt, column, buffer, bufferLength, cType, len); + } + } else { + ret = SQL_SUCCESS; + } + + // Not an error, as such. + if (ret == SQL_NO_DATA) + return scope.Close(Undefined()); + + if (!SQL_SUCCEEDED(ret)) + return ThrowException(ODBC::GetSQLError(SQL_HANDLE_STMT, hStmt, "ODBC::GetColumnValue: Error getting data for result column")); + + if (len == SQL_NULL_DATA) + return scope.Close(Null()); + + Handle result = ConvertColumnValue(cType, buffer, len, slowBuffer, offset); + + if (!result->IsUndefined()) + return scope.Close(result); + + // SQLGetData returns SQL_SUCCESS on the last chunk, SQL_SUCCESS_WITH_INFO and SQLSTATE 01004 on the preceding chunks. + if (partial || ret == SQL_SUCCESS) { + switch (cType) { + case SQL_C_BINARY: { + // XXX Can we trust the global Buffer object? What if the caller has set it to something non-Function? Crash? + Local bufferConstructor = Local::Cast(Context::GetCurrent()->Global()->Get(String::New("Buffer"))); + Handle constructorArgs[1] = { slowBuffer->handle_ }; + return scope.Close(bufferConstructor->NewInstance(1, constructorArgs)); } - } while (true); - - return scope.Close(str); - //return str; + + // TODO when UNICODE is not defined, assumes that the character encoding of the data received is UTF-8 + case SQL_C_TCHAR: + #ifdef UNICODE + assert (offset % 2 == 0); + return scope.Close(String::New(reinterpret_cast(Buffer::Data(slowBuffer)), offset / 2)); + #else + return scope.Close(String::New(Buffer::Data(slowBuffer), offset)); + #endif + + default: + return ThrowException(Exception::Error(String::New("ODBC::GetColumnValue: Unexpected C type"))); + } + } else { // SQL_SUCCESS_WITH_INFO (more data) + offset += len; + + if (slowBuffer && offset >= Buffer::Length(slowBuffer)) + return ThrowException(Exception::Error(String::New("ODBC::GetColumnValue: result buffer not big enough"))); + } } } @@ -786,7 +765,7 @@ Local ODBC::GetSQLError (SQLSMALLINT handleType, SQLHANDLE handle, char* Local objError = Object::New(); - SQLINTEGER i = 0; + SQLSMALLINT i = 0; SQLINTEGER native; SQLSMALLINT len; @@ -805,7 +784,7 @@ Local ODBC::GetSQLError (SQLSMALLINT handleType, SQLHANDLE handle, char* &len); // Windows seems to define SQLINTEGER as long int, unixodbc as just int... %i should cover both - DEBUG_PRINTF("ODBC::GetSQLError : called SQLGetDiagField; ret=%i\n", ret); + DEBUG_PRINTF("ODBC::GetSQLError : called SQLGetDiagField; ret=%i, numfields=%i\n", ret, numfields); for (i = 0; i < numfields; i++){ DEBUG_PRINTF("ODBC::GetSQLError : calling SQLGetDiagRec; i=%i, numfields=%i\n", i, numfields); diff --git a/src/odbc.h b/src/odbc.h index 2d50a01..dbf29e5 100644 --- a/src/odbc.h +++ b/src/odbc.h @@ -20,6 +20,7 @@ #include #include +#include #include #include @@ -45,7 +46,6 @@ using namespace node; #define FETCH_NONE 5 #define SQL_DESTROY 9999 - typedef struct { unsigned char *name; unsigned int len; @@ -72,7 +72,10 @@ class ODBC : public node::ObjectWrap { static void Init(v8::Handle target); static Column* GetColumns(SQLHSTMT hStmt, short* colCount); static void FreeColumns(Column* columns, short* colCount); - static Handle GetColumnValue(SQLHSTMT hStmt, Column column, uint16_t* buffer, int bufferLength); + static SQLRETURN GetCColumnType(const Column& column); + static SQLRETURN GetColumnData(SQLHSTMT hStmt, const Column& column, void* buffer, int bufferLength, SQLSMALLINT& cType, SQLINTEGER& len); + static Handle ConvertColumnValue(SQLSMALLINT cType, uint16_t* buffer, SQLINTEGER bytesInBuffer, node::Buffer* resultBuffer, size_t& resultBufferOffset); + static Handle GetColumnValue(SQLHSTMT hStmt, Column column, uint16_t* buffer, int bufferLength, bool partial = false, bool fetch = true); static Local GetRecordTuple (SQLHSTMT hStmt, Column* columns, short* colCount, uint16_t* buffer, int bufferLength); static Handle GetRecordArray (SQLHSTMT hStmt, Column* columns, short* colCount, uint16_t* buffer, int bufferLength); static Handle CallbackSQLError (SQLSMALLINT handleType, SQLHANDLE handle, Persistent cb); @@ -169,6 +172,13 @@ struct query_request { return ThrowException(Exception::TypeError( \ String::New("Expected " #N "arguments"))); +//Require Integer Argument +#define REQ_INT32_ARG(I, VAR) \ + if (args.Length() <= (I) || !args[I]->IsInt32()) \ + return ThrowException(Exception::TypeError( \ + String::New("Argument " #I " must be an integer"))); \ + int VAR(args[I]->Int32Value()); + //Require String Argument; Save String as Utf8 #define REQ_STR_ARG(I, VAR) \ if (args.Length() <= (I) || !args[I]->IsString()) \ diff --git a/src/odbc_result.cpp b/src/odbc_result.cpp index c2c25be..3298243 100644 --- a/src/odbc_result.cpp +++ b/src/odbc_result.cpp @@ -779,20 +779,13 @@ Handle ODBCResult::GetColumnValue(const Arguments& args) { get_column_value_work_data* data = (get_column_value_work_data *) calloc(1, sizeof(get_column_value_work_data)); - if (args.Length() < 2 || args.Length() > 3 || !args[args.Length() - 1]->IsFunction()) { - return ThrowException(Exception::TypeError( - String::New("ODBCResult::GetColumnValue(column, maxBytes, cb): 2 or 3 arguments are required. The last argument must be a callback function.") - )); - } - - data->col = args[0]->IntegerValue(); - if (args.Length() == 3) { - data->bytesRequested = args[1]->IntegerValue(); - } else { - data->bytesRequested = 0; - } + REQ_INT32_ARG(0, col); + REQ_INT32_ARG(1, maxBytes); + REQ_FUN_ARG(2, cb); - data->cb = Persistent::New(Local::Cast(args[args.Length() - 1])); + data->col = col; + data->bytesRequested = maxBytes; + data->cb = Persistent::New(cb); data->objResult = objODBCResult; work_req->data = data; @@ -815,48 +808,18 @@ void ODBCResult::UV_GetColumnValue(uv_work_t* work_req) { SQLLEN bytesRequested = data->bytesRequested; if (bytesRequested <= 0 || bytesRequested > data->objResult->bufferLength) bytesRequested = data->objResult->bufferLength; - - data->result = SQLColAttribute( - data->objResult->m_hSTMT, - data->col + 1, - SQL_DESC_TYPE, - NULL, - 0, - NULL, - &data->type - ); - - if (!SUCCEEDED(data->result)) - return; - - switch(data->type) { - case SQL_INTEGER: case SQL_SMALLINT: case SQL_TINYINT: - data->type = SQL_C_SLONG; - break; - case SQL_NUMERIC: case SQL_DECIMAL: case SQL_BIGINT: case SQL_FLOAT: case SQL_REAL: case SQL_DOUBLE : - data->type = SQL_C_DOUBLE; - break; - case SQL_DATETIME: case SQL_TIMESTAMP: -#if defined(_WIN32) - data->type = SQL_C_CHAR; -#else - data->type = SQL_C_TYPE_TIMESTAMP -#endif - case SQL_BINARY: case SQL_VARBINARY: - data->type = SQL_C_BINARY; - break; - default: // includes SQL_BIT - data->type = SQL_C_TCHAR; - break; - } - - data->result = SQLGetData( + + data->result = ODBC::GetColumnData( data->objResult->m_hSTMT, - data->col + 1, - data->type, - data->objResult->buffer, - bytesRequested, - &data->bytesRead); + data->objResult->columns[data->col], + data->objResult->buffer, + bytesRequested, + data->cType, + data->bytesRead); + + // GetColumnData actually returns the full length of the column in the final parameter + if (data->bytesRead > data->bytesRequested) + data->bytesRead = data->bytesRequested; } void ODBCResult::UV_AfterGetColumnValue(uv_work_t* work_req, int status) { @@ -866,6 +829,8 @@ void ODBCResult::UV_AfterGetColumnValue(uv_work_t* work_req, int status) { get_column_value_work_data* data = (get_column_value_work_data*)(work_req->data); + TryCatch tc; + SQLRETURN ret = data->result; DEBUG_PRINTF("ODBCResult::UV_AfterGetColumnValue: ret=%i, bytesRead=%i\n", ret, data->bytesRead); @@ -875,35 +840,42 @@ void ODBCResult::UV_AfterGetColumnValue(uv_work_t* work_req, int status) { if (ret == SQL_ERROR) { args[0] = ODBC::GetSQLError(SQL_HANDLE_STMT, data->objResult->m_hSTMT, "[node-odbc] Error in ODBCResult::GetColumnValue"); args[1] = Null(); - } else if (data->bytesRead == SQL_NULL_DATA || ret == SQL_NO_DATA || data->bytesRead == 0) { + } else if (data->bytesRead == SQL_NULL_DATA) { args[0] = Null(); args[1] = Null(); } else { args[0] = Null(); - - switch(data->type) { - case SQL_C_BINARY: { - node::Buffer* slowBuffer = node::Buffer::New(data->bytesRead); - memcpy(node::Buffer::Data(slowBuffer), data->objResult->buffer, data->bytesRead); - v8::Local globalObj = v8::Context::GetCurrent()->Global(); - v8::Local bufferConstructor = v8::Local::Cast(globalObj->Get(v8::String::New("Buffer"))); - v8::Handle constructorArgs[1] = { slowBuffer->handle_ }; - v8::Local actualBuffer = bufferConstructor->NewInstance(3, constructorArgs); - args[1] = actualBuffer; - args[1] = slowBuffer->handle_; - break; - } - - case SQL_C_TCHAR: default: - #ifdef UNICODE - args[1] = String::New((uint16_t*) (data->objResult->buffer)); - #else - args[1] = String::New((char *) (data->objResult->buffer)); - #endif - break; + if (data->cType == SQL_C_TCHAR) { +#ifdef UNICODE + assert(data->bytesRead % 2 == 0); + args[1] = String::New(reinterpret_cast(data->objResult->buffer), data->bytesRead / 2); +#else + // XXX Expects UTF8, could really be anything... if the chunk finishes in the middle of a multi-byte character, that's bad. + args[1] = String::New(reinterpret_cast(data->objResult->buffer), data->bytesRead); +#endif + } else if(data->cType == SQL_C_BINARY) { + Buffer* slowBuffer = Buffer::New(data->bytesRead); + memcpy(Buffer::Data(slowBuffer), data->objResult->buffer, data->bytesRead); + Local bufferConstructor = v8::Local::Cast(Context::GetCurrent()->Global()->Get(String::New("Buffer"))); + Handle constructorArgs[1] = { slowBuffer->handle_ }; + args[1] = bufferConstructor->NewInstance(1, constructorArgs); + } else { + TryCatch try_catch; + + size_t offset = 0; // Only used for SQL_C_BINARY/SQL_C_TCHAR, which are already handled + + args[1] = ODBC::ConvertColumnValue( + data->cType, + data->objResult->buffer, + data->bytesRead, + 0, offset); + + if (try_catch.HasCaught()) { + args[0] = try_catch.Exception(); + args[1] = Undefined(); + } } - } TryCatch try_catch; @@ -927,22 +899,18 @@ Handle ODBCResult::GetColumnValueSync(const Arguments& args) { HandleScope scope; ODBCResult* objODBCResult = ObjectWrap::Unwrap(args.Holder()); - - DEBUG_PRINTF("ODBCResult::GetColumnValueSync: columns=0x%x, colCount=%i\n", objODBCResult->columns, objODBCResult->colCount); - - Column c; - c.index = args[0]->Uint32Value() + 1; - c.name = (unsigned char*)"(Name not known (ODBCResult::GetColumnValueSync))\0\0"; - c.type = SQL_VARCHAR; - c.len = 200; - int i = args[0]->Uint32Value(); - if (i < 0 || i >= objODBCResult->colCount) + REQ_INT32_ARG(0, col); + if (col < 0 || col >= objODBCResult->colCount) return ThrowException(Exception::RangeError( String::New("ODBCResult::GetColumnValueSync(): The column index requested is invalid.") )); + OPT_INT_ARG(1, maxBytes, objODBCResult->bufferLength); + + DEBUG_PRINTF("ODBCResult::GetColumnValueSync: columns=0x%x, colCount=%i, column=%i, maxBytes=%i\n", objODBCResult->columns, objODBCResult->colCount, col, maxBytes); + return scope.Close( - ODBC::GetColumnValue(objODBCResult->m_hSTMT, objODBCResult->columns[i], objODBCResult->buffer, objODBCResult->bufferLength) + ODBC::GetColumnValue(objODBCResult->m_hSTMT, objODBCResult->columns[col], objODBCResult->buffer, maxBytes, true) ); } \ No newline at end of file diff --git a/src/odbc_result.h b/src/odbc_result.h index df42403..2e4e200 100644 --- a/src/odbc_result.h +++ b/src/odbc_result.h @@ -83,9 +83,9 @@ class ODBCResult : public node::ObjectWrap { SQLRETURN result; SQLUSMALLINT col; - SQLLEN type; SQLLEN bytesRequested; - SQLLEN bytesRead; + SQLINTEGER bytesRead; + SQLSMALLINT cType; }; ODBCResult *self(void) { return this; } From 924b26b760581e2a35c40f7a05475cfa570c3e51 Mon Sep 17 00:00:00 2001 From: Lee Houghton Date: Sun, 12 Jan 2014 21:15:58 +0000 Subject: [PATCH 12/26] Fix getting long strings Handle null terminators got from SQLGetData with SQL_C_TCHAR Also fix exception handling in some functions --- src/odbc.cpp | 92 +++++++++++++++++++++++++++------------------ src/odbc.h | 10 ++++- src/odbc_result.cpp | 24 +++++------- 3 files changed, 74 insertions(+), 52 deletions(-) diff --git a/src/odbc.cpp b/src/odbc.cpp index e96604c..bfa41a9 100644 --- a/src/odbc.cpp +++ b/src/odbc.cpp @@ -393,7 +393,7 @@ SQLRETURN ODBC::GetColumnData( SQLHSTMT hStmt, const Column& column, Handle ODBC::ConvertColumnValue( SQLSMALLINT cType, uint16_t* buffer, SQLINTEGER bytesInBuffer, - Buffer* resultBuffer, size_t& resultBufferOffset) { + Buffer* resultBuffer, size_t resultBufferOffset) { HandleScope scope; @@ -446,17 +446,15 @@ Handle ODBC::ConvertColumnValue( SQLSMALLINT cType, return scope.Close(Boolean::New(!!*reinterpret_cast(buffer))); case SQL_C_BINARY: case SQL_C_TCHAR: - if (resultBufferOffset + bytesInBuffer > Buffer::Length(resultBuffer)) - return ThrowException(Exception::Error(String::New("ODBC::GetColumnValue: Buffer overflow (binary)"))); + DEBUG_PRINTF("%i %i += %i <= %i\n", resultBufferOffset, bytesInBuffer, resultBufferOffset + bytesInBuffer, Buffer::Length(resultBuffer)); + assert (resultBufferOffset + bytesInBuffer <= Buffer::Length(resultBuffer)); memcpy(Buffer::Data(resultBuffer) + resultBufferOffset, buffer, bytesInBuffer); - resultBufferOffset += bytesInBuffer; return scope.Close(Undefined()); - break; default: - return ThrowException(Exception::Error(String::New("ODBC::GetColumnValue: Internal error (unexpected C type)"))); + assert(!"ODBC::ConvertColumnValue: Internal error (unexpected C type)"); } } @@ -464,7 +462,6 @@ Handle ODBC::GetColumnValue( SQLHSTMT hStmt, Column column, uint16_t* buffer, int bufferLength, bool partial, bool fetch) { HandleScope scope; - SQLLEN len = 0; Buffer* slowBuffer = 0; size_t offset = 0; @@ -477,7 +474,7 @@ Handle ODBC::GetColumnValue( SQLHSTMT hStmt, Column column, DEBUG_PRINTF("ODBC::GetColumnValue: column=%s, type=%i, buffer=%x, bufferLength=%i, partial=%i, fetch=%i\n", column.name, column.type, buffer, bufferLength, partial?1:0, fetch?1:0); while (true) { - SQLINTEGER len = 0; + SQLINTEGER bytesAvailable = 0, bytesRead = 0; SQLSMALLINT cType = GetCColumnType(column); SQLRETURN ret; @@ -488,25 +485,50 @@ Handle ODBC::GetColumnValue( SQLHSTMT hStmt, Column column, if (cType == SQL_C_BINARY || cType == SQL_C_TCHAR) { if (slowBuffer) { // Just use the node::Buffer we have to avoid memcpy()ing - ret = GetColumnData(hStmt, column, Buffer::Data(slowBuffer) + offset, Buffer::Length(slowBuffer) - offset, cType, len); - if (len > bufferLength) - len = bufferLength; + int remainingBuffer = Buffer::Length(slowBuffer) - offset; + ret = GetColumnData(hStmt, column, Buffer::Data(slowBuffer) + offset, remainingBuffer, cType, bytesAvailable); + if (bytesAvailable == SQL_NULL_DATA) + return scope.Close(Null()); + + bytesRead = bytesAvailable > remainingBuffer ? remainingBuffer : bytesAvailable; + + if (cType == SQL_C_TCHAR && bytesRead == remainingBuffer) + bytesRead -= sizeof(TCHAR); // The last byte or two bytes of the buffer is actually a null terminator (gee, thanks, ODBC). + + offset += bytesRead; + + DEBUG_PRINTF(" *** SQLGetData(slowBuffer, bufferLength=%i): returned %i with %i bytes available\n", remainingBuffer, ret, bytesAvailable); } else { // We don't know how much data there is to get back yet... // Use our buffer for now for the first SQLGetData call, which will tell us the full length. - ret = GetColumnData(hStmt, column, buffer, bufferLength, cType, len); - if (len == SQL_NULL_DATA) + ret = GetColumnData(hStmt, column, buffer, bufferLength, cType, bytesAvailable); + if (bytesAvailable == SQL_NULL_DATA) return scope.Close(Null()); - if (partial && len > bufferLength) - len = bufferLength; - slowBuffer = Buffer::New(len); - if (len > bufferLength) - len = bufferLength; - memcpy(Buffer::Data(slowBuffer), buffer, len); + + bytesRead = bytesAvailable > bufferLength ? bufferLength : bytesAvailable; + + if (cType == SQL_C_TCHAR && bytesRead == bufferLength) + bytesRead -= sizeof(TCHAR); // The last byte or two bytes of the buffer is actually a null terminator (gee, thanks, ODBC). + + offset += bytesRead; + + DEBUG_PRINTF(" *** SQLGetData(internalBuffer, bufferLength=%i): returned %i with %i bytes available\n", bufferLength, ret, bytesAvailable); + + if (bytesAvailable == SQL_NULL_DATA) + return scope.Close(Null()); + + slowBuffer = Buffer::New(bytesAvailable + (cType == SQL_C_TCHAR ? sizeof(TCHAR) : 0)); // Account for possible null terminator... + memcpy(Buffer::Data(slowBuffer), buffer, bytesRead); + DEBUG_PRINTF(" *** Created new SlowBuffer (length: %i), copied %i bytes from internal buffer\n", Buffer::Length(slowBuffer), bytesRead); } } else { // Use the ODBCResult's buffer - ret = GetColumnData(hStmt, column, buffer, bufferLength, cType, len); + ret = GetColumnData(hStmt, column, buffer, bufferLength, cType, bytesAvailable); + if (bytesAvailable == SQL_NULL_DATA) + return scope.Close(Null()); + + bytesRead = bytesAvailable > bufferLength ? bufferLength : bytesAvailable; + DEBUG_PRINTF(" *** SQLGetData(internalBuffer, bufferLength=%i) for fixed type: returned %i with %i bytes available\n", bufferLength, ret, bytesAvailable); } } else { ret = SQL_SUCCESS; @@ -518,15 +540,15 @@ Handle ODBC::GetColumnValue( SQLHSTMT hStmt, Column column, if (!SQL_SUCCEEDED(ret)) return ThrowException(ODBC::GetSQLError(SQL_HANDLE_STMT, hStmt, "ODBC::GetColumnValue: Error getting data for result column")); - - if (len == SQL_NULL_DATA) - return scope.Close(Null()); - Handle result = ConvertColumnValue(cType, buffer, len, slowBuffer, offset); - - if (!result->IsUndefined()) - return scope.Close(result); + Handle result; + if (!slowBuffer) { + result = ConvertColumnValue(cType, buffer, bytesRead, 0, 0); + if (!result->IsUndefined()) + return scope.Close(result); + } + // SQLGetData returns SQL_SUCCESS on the last chunk, SQL_SUCCESS_WITH_INFO and SQLSTATE 01004 on the preceding chunks. if (partial || ret == SQL_SUCCESS) { switch (cType) { @@ -550,10 +572,7 @@ Handle ODBC::GetColumnValue( SQLHSTMT hStmt, Column column, return ThrowException(Exception::Error(String::New("ODBC::GetColumnValue: Unexpected C type"))); } } else { // SQL_SUCCESS_WITH_INFO (more data) - offset += len; - - if (slowBuffer && offset >= Buffer::Length(slowBuffer)) - return ThrowException(Exception::Error(String::New("ODBC::GetColumnValue: result buffer not big enough"))); + assert(!slowBuffer || offset <= Buffer::Length(slowBuffer)); } } } @@ -570,12 +589,11 @@ Local ODBC::GetRecordTuple ( SQLHSTMT hStmt, Column* columns, Local tuple = Object::New(); for(int i = 0; i < *colCount; i++) { + Handle value = GetColumnValue( hStmt, columns[i], buffer, bufferLength ); #ifdef UNICODE - tuple->Set( String::New((uint16_t *) columns[i].name), - GetColumnValue( hStmt, columns[i], buffer, bufferLength)); + tuple->Set( String::New((uint16_t *) columns[i].name), value ); #else - tuple->Set( String::New((const char *) columns[i].name), - GetColumnValue( hStmt, columns[i], buffer, bufferLength)); + tuple->Set( String::New((const char *) columns[i].name), value ); #endif } @@ -595,8 +613,8 @@ Handle ODBC::GetRecordArray ( SQLHSTMT hStmt, Column* columns, Local array = Array::New(); for(int i = 0; i < *colCount; i++) { - array->Set( Integer::New(i), - GetColumnValue( hStmt, columns[i], buffer, bufferLength)); + Handle value = GetColumnValue( hStmt, columns[i], buffer, bufferLength ); + array->Set( Integer::New(i), value ); } //return array; diff --git a/src/odbc.h b/src/odbc.h index dbf29e5..f68f081 100644 --- a/src/odbc.h +++ b/src/odbc.h @@ -74,7 +74,7 @@ class ODBC : public node::ObjectWrap { static void FreeColumns(Column* columns, short* colCount); static SQLRETURN GetCColumnType(const Column& column); static SQLRETURN GetColumnData(SQLHSTMT hStmt, const Column& column, void* buffer, int bufferLength, SQLSMALLINT& cType, SQLINTEGER& len); - static Handle ConvertColumnValue(SQLSMALLINT cType, uint16_t* buffer, SQLINTEGER bytesInBuffer, node::Buffer* resultBuffer, size_t& resultBufferOffset); + static Handle ConvertColumnValue(SQLSMALLINT cType, uint16_t* buffer, SQLINTEGER bytesInBuffer, node::Buffer* resultBuffer, size_t resultBufferOffset); static Handle GetColumnValue(SQLHSTMT hStmt, Column column, uint16_t* buffer, int bufferLength, bool partial = false, bool fetch = true); static Local GetRecordTuple (SQLHSTMT hStmt, Column* columns, short* colCount, uint16_t* buffer, int bufferLength); static Handle GetRecordArray (SQLHSTMT hStmt, Column* columns, short* colCount, uint16_t* buffer, int bufferLength); @@ -250,5 +250,13 @@ struct query_request { String::New("Argument " #I " must be an integer"))); \ } +// For non-Windows? +#ifndef TCHAR +#ifdef UNICODE +#define TCHAR wchar_t +#else +#define TCHAR char +#endif +#endif #endif diff --git a/src/odbc_result.cpp b/src/odbc_result.cpp index 3298243..bb5e8db 100644 --- a/src/odbc_result.cpp +++ b/src/odbc_result.cpp @@ -121,7 +121,7 @@ Handle ODBCResult::New(const Arguments& args) { delete canFreeHandle; //specify the buffer length - objODBCResult->bufferLength = MAX_VALUE_SIZE - 1; + objODBCResult->bufferLength = MAX_VALUE_SIZE + (MAX_VALUE_SIZE % 2); // Ensure even so GetColumnValue doesn't break //initialze a buffer for this object objODBCResult->buffer = (uint16_t *) malloc(objODBCResult->bufferLength + 1); @@ -647,28 +647,24 @@ Handle ODBCResult::FetchAllSync(const Arguments& args) { break; } - if (fetchMode == FETCH_ARRAY) { - rows->Set( - Integer::New(count), + Handle value; + if (fetchMode == FETCH_ARRAY) + value = ODBC::GetRecordArray( self->m_hSTMT, self->columns, &self->colCount, self->buffer, - self->bufferLength) - ); - } - else if (fetchMode == FETCH_OBJECT) { - rows->Set( - Integer::New(count), - ODBC::GetRecordTuple( + self->bufferLength); + else if (fetchMode == FETCH_OBJECT) + value = ODBC::GetRecordTuple( self->m_hSTMT, self->columns, &self->colCount, self->buffer, - self->bufferLength) - ); - } + self->bufferLength); + + rows->Set(Integer::New(count), value); count++; } } From ddc41d9a9863bfb4405f5d1d8a86ac1a936f6e6f Mon Sep 17 00:00:00 2001 From: Lee Houghton Date: Mon, 13 Jan 2014 04:57:29 +0000 Subject: [PATCH 13/26] Improve GetColumnValue Allow fetching entire value in async mode (loops via uv_queue_work) - refactored ODBC::GetColumnValue again to make this possible without unduly duplicating code. Allow not passing maxBytes to result.getColumnValue. --- src/odbc.cpp | 231 ++++++++++++++++++++++++++------------------ src/odbc.h | 18 +++- src/odbc_result.cpp | 126 ++++++++++++++---------- src/odbc_result.h | 11 ++- 4 files changed, 238 insertions(+), 148 deletions(-) diff --git a/src/odbc.cpp b/src/odbc.cpp index bfa41a9..c4eb078 100644 --- a/src/odbc.cpp +++ b/src/odbc.cpp @@ -445,135 +445,178 @@ Handle ODBC::ConvertColumnValue( SQLSMALLINT cType, assert(bytesInBuffer >= sizeof(SQLCHAR)); return scope.Close(Boolean::New(!!*reinterpret_cast(buffer))); - case SQL_C_BINARY: case SQL_C_TCHAR: - DEBUG_PRINTF("%i %i += %i <= %i\n", resultBufferOffset, bytesInBuffer, resultBufferOffset + bytesInBuffer, Buffer::Length(resultBuffer)); - assert (resultBufferOffset + bytesInBuffer <= Buffer::Length(resultBuffer)); - - memcpy(Buffer::Data(resultBuffer) + resultBufferOffset, buffer, bytesInBuffer); - - return scope.Close(Undefined()); - default: assert(!"ODBC::ConvertColumnValue: Internal error (unexpected C type)"); } } +SQLRETURN ODBC::FetchMoreData( SQLHSTMT hStmt, const Column& column, SQLSMALLINT cType, + SQLINTEGER& bytesAvailable, SQLINTEGER& bytesRead, + void* internalBuffer, SQLINTEGER internalBufferLength, + void* resultBuffer, size_t& offset, int resultBufferLength ) { + + bytesRead = 0; + SQLRETURN ret; + + if (resultBuffer) { + // Just use the node::Buffer we have to avoid memcpy()ing + SQLINTEGER remainingBuffer = resultBufferLength - offset; + ret = GetColumnData(hStmt, column, (char*)resultBuffer + offset, remainingBuffer, cType, bytesAvailable); + if (!SQL_SUCCEEDED(ret) || bytesAvailable == SQL_NULL_DATA) + return ret; + + bytesRead = min(bytesAvailable, remainingBuffer); + + if (cType == SQL_C_TCHAR && bytesRead == remainingBuffer) + bytesRead -= sizeof(TCHAR); // The last byte or two bytes of the buffer is actually a null terminator (gee, thanks, ODBC). + + offset += bytesRead; + + DEBUG_PRINTF("ODBC::FetchMoreData: SQLGetData(slowBuffer, bufferLength=%i): returned %i with %i bytes available\n", + remainingBuffer, ret, bytesAvailable); + } else { + // We don't know how much data there is to get back yet... + // Use our buffer for now for the first SQLGetData call, which will tell us the full length. + ret = GetColumnData(hStmt, column, internalBuffer, internalBufferLength, cType, bytesAvailable); + if (!SQL_SUCCEEDED(ret) || bytesAvailable == SQL_NULL_DATA) + return ret; + + bytesRead = min(bytesAvailable, internalBufferLength); + + if (cType == SQL_C_TCHAR && bytesRead == internalBufferLength) + bytesRead -= sizeof(TCHAR); // The last byte or two bytes of the buffer is actually a null terminator (gee, thanks, ODBC). + + offset += bytesRead; + + DEBUG_PRINTF("ODBC::FetchMoreData: SQLGetData(internalBuffer, bufferLength=%i): returned %i with %i bytes available\n", + internalBufferLength, ret, bytesAvailable); + + // Now would be a good time to create the result buffer, but we can't call + // Buffer::New here if we are in a worker function + } + + offset += bytesRead; + return ret; +} + Handle ODBC::GetColumnValue( SQLHSTMT hStmt, Column column, uint16_t* buffer, int bufferLength, bool partial, bool fetch) { HandleScope scope; - Buffer* slowBuffer = 0; - size_t offset = 0; - //reset the buffer + // Reset the buffer buffer[0] = '\0'; if (!fetch) partial = true; - DEBUG_PRINTF("ODBC::GetColumnValue: column=%s, type=%i, buffer=%x, bufferLength=%i, partial=%i, fetch=%i\n", column.name, column.type, buffer, bufferLength, partial?1:0, fetch?1:0); + DEBUG_PRINTF("ODBC::GetColumnValue: column=%s, type=%i, buffer=%x, bufferLength=%i, partial=%i, fetch=%i\n", + column.name, column.type, buffer, bufferLength, partial?1:0, fetch?1:0); - while (true) { + SQLSMALLINT cType = GetCColumnType(column); + + // Fixed length column + if (cType != SQL_C_BINARY && cType != SQL_C_TCHAR) { SQLINTEGER bytesAvailable = 0, bytesRead = 0; - SQLSMALLINT cType = GetCColumnType(column); - SQLRETURN ret; if (fetch) { - // TODO For fixed-length columns, the driver will just assume the buffer is big enough - // Ensure this won't break anything. - - if (cType == SQL_C_BINARY || cType == SQL_C_TCHAR) { - if (slowBuffer) { - // Just use the node::Buffer we have to avoid memcpy()ing - int remainingBuffer = Buffer::Length(slowBuffer) - offset; - ret = GetColumnData(hStmt, column, Buffer::Data(slowBuffer) + offset, remainingBuffer, cType, bytesAvailable); - if (bytesAvailable == SQL_NULL_DATA) - return scope.Close(Null()); - - bytesRead = bytesAvailable > remainingBuffer ? remainingBuffer : bytesAvailable; - - if (cType == SQL_C_TCHAR && bytesRead == remainingBuffer) - bytesRead -= sizeof(TCHAR); // The last byte or two bytes of the buffer is actually a null terminator (gee, thanks, ODBC). - - offset += bytesRead; - - DEBUG_PRINTF(" *** SQLGetData(slowBuffer, bufferLength=%i): returned %i with %i bytes available\n", remainingBuffer, ret, bytesAvailable); - } else { - // We don't know how much data there is to get back yet... - // Use our buffer for now for the first SQLGetData call, which will tell us the full length. - ret = GetColumnData(hStmt, column, buffer, bufferLength, cType, bytesAvailable); - if (bytesAvailable == SQL_NULL_DATA) - return scope.Close(Null()); - - bytesRead = bytesAvailable > bufferLength ? bufferLength : bytesAvailable; - - if (cType == SQL_C_TCHAR && bytesRead == bufferLength) - bytesRead -= sizeof(TCHAR); // The last byte or two bytes of the buffer is actually a null terminator (gee, thanks, ODBC). + // Use the ODBCResult's buffer + // TODO For fixed-length columns, the driver will just assume the buffer is big enough + // Ensure this won't break anything. - offset += bytesRead; + SQLRETURN ret = GetColumnData(hStmt, column, buffer, bufferLength, cType, bytesAvailable); + if (!SQL_SUCCEEDED(ret)) + return ThrowException(ODBC::GetSQLError(SQL_HANDLE_STMT, hStmt)); - DEBUG_PRINTF(" *** SQLGetData(internalBuffer, bufferLength=%i): returned %i with %i bytes available\n", bufferLength, ret, bytesAvailable); - - if (bytesAvailable == SQL_NULL_DATA) - return scope.Close(Null()); - - slowBuffer = Buffer::New(bytesAvailable + (cType == SQL_C_TCHAR ? sizeof(TCHAR) : 0)); // Account for possible null terminator... - memcpy(Buffer::Data(slowBuffer), buffer, bytesRead); - DEBUG_PRINTF(" *** Created new SlowBuffer (length: %i), copied %i bytes from internal buffer\n", Buffer::Length(slowBuffer), bytesRead); - } - } else { - // Use the ODBCResult's buffer - ret = GetColumnData(hStmt, column, buffer, bufferLength, cType, bytesAvailable); if (bytesAvailable == SQL_NULL_DATA) return scope.Close(Null()); bytesRead = bytesAvailable > bufferLength ? bufferLength : bytesAvailable; DEBUG_PRINTF(" *** SQLGetData(internalBuffer, bufferLength=%i) for fixed type: returned %i with %i bytes available\n", bufferLength, ret, bytesAvailable); - } + + return scope.Close(ConvertColumnValue(cType, buffer, bytesRead, 0, 0)); + } else { + return scope.Close(ConvertColumnValue(cType, buffer, bufferLength, 0, 0)); + } + } + + // Varying length column - fetch piece by piece (in as few pieces as possible - fetch the first piece into the + // ODBCResult's buffer to find out how big the column is, then allocate a node::Buffer to hold the rest of it) + Buffer* slowBuffer = 0; + size_t offset = 0; + + SQLRETURN ret = 0; + SQLINTEGER bytesAvailable = 0, bytesRead = 0; + + do { + if (fetch) { + ret = FetchMoreData( + hStmt, column, cType, + bytesAvailable, bytesRead, + buffer, bufferLength, + slowBuffer ? Buffer::Data(slowBuffer) : 0, offset, slowBuffer ? Buffer::Length(slowBuffer) : 0); } else { ret = SQL_SUCCESS; + bytesAvailable = bufferLength; + bytesRead = bufferLength; } - // Not an error, as such. + // Maybe this should be an error? if (ret == SQL_NO_DATA) return scope.Close(Undefined()); if (!SQL_SUCCEEDED(ret)) return ThrowException(ODBC::GetSQLError(SQL_HANDLE_STMT, hStmt, "ODBC::GetColumnValue: Error getting data for result column")); + + if (slowBuffer) + assert(offset <= Buffer::Length(slowBuffer)); + + // The SlowBuffer is needed when + // (a) the result type is Binary + // (b) the result is a string longer than our internal buffer (and we hopefully know the length). + // Move data from internal buffer to node::Buffer now. + if (!slowBuffer && (ret == SQL_SUCCESS_WITH_INFO || cType == SQL_C_BINARY)) { + slowBuffer = Buffer::New(bytesAvailable + (cType == SQL_C_TCHAR ? sizeof(TCHAR) : 0)); // Allow space for ODBC to add an unnecessary null terminator + memcpy(Buffer::Data(slowBuffer), buffer, bytesRead); + DEBUG_PRINTF(" *** Created new SlowBuffer (length: %i), copied %i bytes from internal buffer\n", Buffer::Length(slowBuffer), bytesRead); + } - Handle result; - if (!slowBuffer) { - result = ConvertColumnValue(cType, buffer, bytesRead, 0, 0); + // SQLGetData returns SQL_SUCCESS on the last chunk, SQL_SUCCESS_WITH_INFO and SQLSTATE 01004 on the preceding chunks. + } while (!partial && ret == SQL_SUCCESS_WITH_INFO); - if (!result->IsUndefined()) - return scope.Close(result); - } - - // SQLGetData returns SQL_SUCCESS on the last chunk, SQL_SUCCESS_WITH_INFO and SQLSTATE 01004 on the preceding chunks. - if (partial || ret == SQL_SUCCESS) { - switch (cType) { - case SQL_C_BINARY: { - // XXX Can we trust the global Buffer object? What if the caller has set it to something non-Function? Crash? - Local bufferConstructor = Local::Cast(Context::GetCurrent()->Global()->Get(String::New("Buffer"))); - Handle constructorArgs[1] = { slowBuffer->handle_ }; - return scope.Close(bufferConstructor->NewInstance(1, constructorArgs)); - } - - // TODO when UNICODE is not defined, assumes that the character encoding of the data received is UTF-8 - case SQL_C_TCHAR: - #ifdef UNICODE - assert (offset % 2 == 0); - return scope.Close(String::New(reinterpret_cast(Buffer::Data(slowBuffer)), offset / 2)); - #else - return scope.Close(String::New(Buffer::Data(slowBuffer), offset)); - #endif + return scope.Close(InterpretBuffers(cType, buffer, bytesRead, slowBuffer->handle_, slowBuffer, offset)); +} - default: - return ThrowException(Exception::Error(String::New("ODBC::GetColumnValue: Unexpected C type"))); - } - } else { // SQL_SUCCESS_WITH_INFO (more data) - assert(!slowBuffer || offset <= Buffer::Length(slowBuffer)); +Handle ODBC::InterpretBuffers( SQLSMALLINT cType, + void* internalBuffer, SQLINTEGER bytesRead, + Persistent resultBufferHandle, + void* resultBuffer, size_t resultBufferOffset) { + HandleScope scope; + + switch (cType) { + case SQL_C_BINARY: { + // XXX Can we trust the global Buffer object? What if the caller has set it to something non-Function? Crash? + Local bufferConstructor = Local::Cast(Context::GetCurrent()->Global()->Get(String::New("Buffer"))); + Handle constructorArgs[1] = { resultBufferHandle }; + return scope.Close(bufferConstructor->NewInstance(1, constructorArgs)); } + + // TODO when UNICODE is not defined, assumes that the character encoding of the data received is UTF-8 + case SQL_C_TCHAR: default: +#ifdef UNICODE + // slowBuffer may or may not exist, depending on whether or not the data had to be split + if (resultBuffer) + assert (resultBufferOffset % 2 == 0); + if (resultBuffer) + return scope.Close(String::New(reinterpret_cast(resultBuffer), resultBufferOffset / 2)); + else + return scope.Close(String::New(reinterpret_cast(internalBuffer), bytesRead / 2)); +#else + if (resultBuffer) + return scope.Close(String::New(reinterpret_cast(resultBuffer), resultBufferOffset)); + else + return scope.Close(String::New(reinterpret_cast(internalBuffer), bytesRead)); +#endif } } @@ -582,14 +625,16 @@ Handle ODBC::GetColumnValue( SQLHSTMT hStmt, Column column, */ Local ODBC::GetRecordTuple ( SQLHSTMT hStmt, Column* columns, - short* colCount, uint16_t* buffer, - int bufferLength) { + short* colCount, uint16_t* buffer, + int bufferLength) { HandleScope scope; Local tuple = Object::New(); for(int i = 0; i < *colCount; i++) { Handle value = GetColumnValue( hStmt, columns[i], buffer, bufferLength ); + if (value->IsUndefined()) + return scope.Close(Local()); #ifdef UNICODE tuple->Set( String::New((uint16_t *) columns[i].name), value ); #else @@ -614,6 +659,8 @@ Handle ODBC::GetRecordArray ( SQLHSTMT hStmt, Column* columns, for(int i = 0; i < *colCount; i++) { Handle value = GetColumnValue( hStmt, columns[i], buffer, bufferLength ); + if (value->IsUndefined()) + return scope.Close(Handle()); array->Set( Integer::New(i), value ); } diff --git a/src/odbc.h b/src/odbc.h index f68f081..d4b1dfb 100644 --- a/src/odbc.h +++ b/src/odbc.h @@ -75,6 +75,8 @@ class ODBC : public node::ObjectWrap { static SQLRETURN GetCColumnType(const Column& column); static SQLRETURN GetColumnData(SQLHSTMT hStmt, const Column& column, void* buffer, int bufferLength, SQLSMALLINT& cType, SQLINTEGER& len); static Handle ConvertColumnValue(SQLSMALLINT cType, uint16_t* buffer, SQLINTEGER bytesInBuffer, node::Buffer* resultBuffer, size_t resultBufferOffset); + static SQLRETURN FetchMoreData(SQLHSTMT hStmt, const Column& column, SQLSMALLINT cType, SQLINTEGER& bytesAvailable, SQLINTEGER& bytesRead, void* internalBuffer, SQLINTEGER internalBufferLength, void* resultBuffer, size_t& offset, int resultBufferLength); + static Handle InterpretBuffers(SQLSMALLINT cType, void* internalBuffer, SQLINTEGER bytesRead, Persistent resultBufferHandle, void* resultBuffer, size_t resultBufferOffset); static Handle GetColumnValue(SQLHSTMT hStmt, Column column, uint16_t* buffer, int bufferLength, bool partial = false, bool fetch = true); static Local GetRecordTuple (SQLHSTMT hStmt, Column* columns, short* colCount, uint16_t* buffer, int bufferLength); static Handle GetRecordArray (SQLHSTMT hStmt, Column* columns, short* colCount, uint16_t* buffer, int bufferLength); @@ -156,11 +158,11 @@ struct query_request { #endif #ifdef DEBUG - #define DEBUG_PRINTF(...) fprintf(stdout, __VA_ARGS__) + #define DEBUG_PRINTF(...) fprintf(stderr, __VA_ARGS__) #ifdef UNICODE - #define DEBUG_TPRINTF(...) fwprintf(stdout, __VA_ARGS__) + #define DEBUG_TPRINTF(...) fwprintf(stderr, __VA_ARGS__) #else - #define DEBUG_TPRINTF(...) fprintf(stdout, __VA_ARGS__) + #define DEBUG_TPRINTF(...) fprintf(stderr, __VA_ARGS__) #endif #else #define DEBUG_PRINTF(...) (void)0 @@ -259,4 +261,14 @@ struct query_request { #endif #endif +template +inline const T& min(const T& x, const T& y) { + return !(y < x) ? x : y; +} + +template +inline const T& max(const T& x, const T& y) { + return x < y ? y : x; +} + #endif diff --git a/src/odbc_result.cpp b/src/odbc_result.cpp index bb5e8db..824d392 100644 --- a/src/odbc_result.cpp +++ b/src/odbc_result.cpp @@ -774,15 +774,27 @@ Handle ODBCResult::GetColumnValue(const Arguments& args) { uv_work_t* work_req = (uv_work_t *) (calloc(1, sizeof(uv_work_t))); get_column_value_work_data* data = (get_column_value_work_data *) calloc(1, sizeof(get_column_value_work_data)); - + REQ_INT32_ARG(0, col); - REQ_INT32_ARG(1, maxBytes); - REQ_FUN_ARG(2, cb); + + int maxBytes; + Local cb; + + if (args.Length() == 3) { + REQ_INT32_ARG(1, tmpMaxBytes); maxBytes = tmpMaxBytes; + REQ_FUN_ARG(2, tmpCB); cb = tmpCB; + } else if (args.Length() == 2) { + REQ_FUN_ARG(3, tmpCB); cb = tmpCB; + maxBytes = 0; // Not a partial request + } data->col = col; data->bytesRequested = maxBytes; data->cb = Persistent::New(cb); data->objResult = objODBCResult; + data->resultBufferContents = 0; + data->resultBufferOffset = 0; + data->resultBufferLength = 0; work_req->data = data; uv_queue_work( @@ -790,7 +802,7 @@ Handle ODBCResult::GetColumnValue(const Arguments& args) { work_req, UV_GetColumnValue, (uv_after_work_cb)UV_AfterGetColumnValue); - + objODBCResult->Ref(); return scope.Close(Undefined()); @@ -805,17 +817,16 @@ void ODBCResult::UV_GetColumnValue(uv_work_t* work_req) { if (bytesRequested <= 0 || bytesRequested > data->objResult->bufferLength) bytesRequested = data->objResult->bufferLength; - data->result = ODBC::GetColumnData( + data->cType = ODBC::GetCColumnType(data->objResult->columns[data->col]); + + data->result = ODBC::FetchMoreData( data->objResult->m_hSTMT, - data->objResult->columns[data->col], - data->objResult->buffer, - bytesRequested, - data->cType, - data->bytesRead); - - // GetColumnData actually returns the full length of the column in the final parameter - if (data->bytesRead > data->bytesRequested) - data->bytesRead = data->bytesRequested; + data->objResult->columns[data->col], + data->cType, + data->bytesAvailable, + data->bytesRead, + data->objResult->buffer, bytesRequested, + data->resultBufferContents, data->resultBufferOffset, data->resultBufferLength); } void ODBCResult::UV_AfterGetColumnValue(uv_work_t* work_req, int status) { @@ -825,59 +836,65 @@ void ODBCResult::UV_AfterGetColumnValue(uv_work_t* work_req, int status) { get_column_value_work_data* data = (get_column_value_work_data*)(work_req->data); - TryCatch tc; - SQLRETURN ret = data->result; - DEBUG_PRINTF("ODBCResult::UV_AfterGetColumnValue: ret=%i, bytesRead=%i\n", ret, data->bytesRead); + DEBUG_PRINTF("ODBCResult::UV_AfterGetColumnValue: ret=%i, cType=%i, bytesRequested=%i, bytesRead=%i\n", ret, data->cType, data->bytesRequested, data->bytesRead); - Handle args[2]; + Handle args[3]; + int argc = 3; - if (ret == SQL_ERROR) { + if (!SQL_SUCCEEDED(ret)) { args[0] = ODBC::GetSQLError(SQL_HANDLE_STMT, data->objResult->m_hSTMT, "[node-odbc] Error in ODBCResult::GetColumnValue"); - args[1] = Null(); + argc = 1; } else if (data->bytesRead == SQL_NULL_DATA) { args[0] = Null(); args[1] = Null(); - } else { + args[2] = False(); + } else if (data->cType != SQL_C_BINARY && data->cType != SQL_C_TCHAR) { args[0] = Null(); + args[1] = ODBC::ConvertColumnValue(data->cType, data->objResult->buffer, data->bytesRead, 0, 0); + args[2] = False(); + } else if (data->bytesRequested == 0 && data->result == SQL_SUCCESS_WITH_INFO) { + // Not partial request, and there is more data to fetch ... queue some more work + + if (!data->resultBufferContents) { + Buffer* b = Buffer::New(data->bytesAvailable + (data->cType == SQL_C_TCHAR ? sizeof(TCHAR) : 0)); // Allow space for ODBC to add an unnecessary null terminator + memcpy(Buffer::Data(b), data->objResult->buffer, data->bytesRead); + data->resultBuffer = b->handle_; + data->resultBufferOffset = data->bytesRead; + data->resultBufferContents = Buffer::Data(b); + data->resultBufferLength = Buffer::Length(b); + DEBUG_PRINTF(" *** Created new SlowBuffer (length: %i), copied %i bytes from internal buffer\n", Buffer::Length(b), data->bytesRead); + } - if (data->cType == SQL_C_TCHAR) { -#ifdef UNICODE - assert(data->bytesRead % 2 == 0); - args[1] = String::New(reinterpret_cast(data->objResult->buffer), data->bytesRead / 2); -#else - // XXX Expects UTF8, could really be anything... if the chunk finishes in the middle of a multi-byte character, that's bad. - args[1] = String::New(reinterpret_cast(data->objResult->buffer), data->bytesRead); -#endif - } else if(data->cType == SQL_C_BINARY) { - Buffer* slowBuffer = Buffer::New(data->bytesRead); - memcpy(Buffer::Data(slowBuffer), data->objResult->buffer, data->bytesRead); - Local bufferConstructor = v8::Local::Cast(Context::GetCurrent()->Global()->Get(String::New("Buffer"))); - Handle constructorArgs[1] = { slowBuffer->handle_ }; - args[1] = bufferConstructor->NewInstance(1, constructorArgs); - } else { - TryCatch try_catch; - - size_t offset = 0; // Only used for SQL_C_BINARY/SQL_C_TCHAR, which are already handled - - args[1] = ODBC::ConvertColumnValue( - data->cType, - data->objResult->buffer, - data->bytesRead, - 0, offset); - - if (try_catch.HasCaught()) { - args[0] = try_catch.Exception(); - args[1] = Undefined(); - } + uv_queue_work( + uv_default_loop(), + work_req, + UV_GetColumnValue, + (uv_after_work_cb)UV_AfterGetColumnValue); + + return; + } else { + if (!data->resultBufferContents && data->cType == SQL_C_BINARY) { + Buffer* b = Buffer::New((const char*)data->objResult->buffer, (size_t)data->bytesRead); + data->resultBuffer = b->handle_; + data->resultBufferOffset = data->bytesRead; + data->resultBufferContents = Buffer::Data(b); + data->resultBufferLength = Buffer::Length(b); + DEBUG_PRINTF(" *** Created new SlowBuffer (length: %i), copied %i bytes from internal buffer\n", Buffer::Length(b), data->bytesRead); } + + args[0] = Null(); + args[1] = ODBC::InterpretBuffers(data->cType, data->objResult->buffer, data->bytesRead, data->resultBuffer, data->resultBufferContents, data->resultBufferOffset); + args[2] = ret == SQL_SUCCESS_WITH_INFO ? True() : False(); } TryCatch try_catch; - data->cb->Call(Context::GetCurrent()->Global(), 2, args); + data->cb->Call(Context::GetCurrent()->Global(), argc, args); data->cb.Dispose(); + if (data->resultBufferContents) + data->resultBuffer.Dispose(); if (try_catch.HasCaught()) { FatalException(try_catch); @@ -902,7 +919,12 @@ Handle ODBCResult::GetColumnValueSync(const Arguments& args) { String::New("ODBCResult::GetColumnValueSync(): The column index requested is invalid.") )); - OPT_INT_ARG(1, maxBytes, objODBCResult->bufferLength); + bool partial = true; + OPT_INT_ARG(1, maxBytes, 0); + if (maxBytes == 0) { + partial = false; + maxBytes = objODBCResult->bufferLength; + } DEBUG_PRINTF("ODBCResult::GetColumnValueSync: columns=0x%x, colCount=%i, column=%i, maxBytes=%i\n", objODBCResult->columns, objODBCResult->colCount, col, maxBytes); diff --git a/src/odbc_result.h b/src/odbc_result.h index 2e4e200..3c2395e 100644 --- a/src/odbc_result.h +++ b/src/odbc_result.h @@ -84,8 +84,17 @@ class ODBCResult : public node::ObjectWrap { SQLUSMALLINT col; SQLLEN bytesRequested; - SQLINTEGER bytesRead; SQLSMALLINT cType; + + // State + Persistent resultBuffer; // Used for keeping the buffer alive + void* resultBufferContents; + size_t resultBufferOffset; + size_t resultBufferLength; + + SQLINTEGER bytesAvailable; + SQLINTEGER bytesRead; + }; ODBCResult *self(void) { return this; } From 7a2312acf99efa03de5af451a09e00e1c24fda02 Mon Sep 17 00:00:00 2001 From: Lee Houghton Date: Thu, 16 Jan 2014 22:00:02 +0000 Subject: [PATCH 14/26] Better parameter and state validation in GetColumnValue --- binding.gyp | 82 +- src/odbc_result.cpp | 1875 ++++++++++++++++++++++--------------------- 2 files changed, 983 insertions(+), 974 deletions(-) diff --git a/binding.gyp b/binding.gyp index dd3928a..acce441 100644 --- a/binding.gyp +++ b/binding.gyp @@ -1,41 +1,41 @@ -{ - 'targets' : [ - { - 'target_name' : 'odbc_bindings', - 'sources' : [ - 'src/odbc.cpp', - 'src/odbc_connection.cpp', - 'src/odbc_statement.cpp', - 'src/odbc_result.cpp', - 'src/dynodbc.cpp' - ], - 'defines' : [ - 'UNICODE' - ], - 'conditions' : [ - [ 'OS == "linux"', { - 'libraries' : [ - '-lodbc' - ], - 'cflags' : [ - '-g' - ] - }], - [ 'OS == "mac"', { - 'libraries' : [ - '-L/usr/local/lib', - '-lodbc' - ] - }], - [ 'OS=="win"', { - 'sources' : [ - 'src/odbc.cpp' - ], - 'libraries' : [ - '-lodbccp32.lib' - ] - }] - ] - } - ] -} +{ + 'targets' : [ + { + 'target_name' : 'odbc_bindings', + 'sources' : [ + 'src/odbc.cpp', + 'src/odbc_connection.cpp', + 'src/odbc_statement.cpp', + 'src/odbc_result.cpp', + 'src/dynodbc.cpp' + ], + 'defines' : [ + 'UNICODE', 'DEBUG' + ], + 'conditions' : [ + [ 'OS == "linux"', { + 'libraries' : [ + '-lodbc' + ], + 'cflags' : [ + '-g' + ] + }], + [ 'OS == "mac"', { + 'libraries' : [ + '-L/usr/local/lib', + '-lodbc' + ] + }], + [ 'OS=="win"', { + 'sources' : [ + 'src/odbc.cpp' + ], + 'libraries' : [ + '-lodbccp32.lib' + ] + }] + ] + } + ] +} diff --git a/src/odbc_result.cpp b/src/odbc_result.cpp index 824d392..a917955 100644 --- a/src/odbc_result.cpp +++ b/src/odbc_result.cpp @@ -1,934 +1,943 @@ -/* - Copyright (c) 2013, Dan VerWeire - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ - -#include -#include -#include -#include -#include -#include -#include - -#include "odbc.h" -#include "odbc_connection.h" -#include "odbc_result.h" -#include "odbc_statement.h" - -using namespace v8; -using namespace node; - -Persistent ODBCResult::constructor_template; -Persistent ODBCResult::OPTION_FETCH_MODE = Persistent::New(String::New("fetchMode")); - -void ODBCResult::Init(v8::Handle target) { - DEBUG_PRINTF("ODBCResult::Init\n"); - HandleScope scope; - - Local t = FunctionTemplate::New(New); - - // Constructor Template - constructor_template = Persistent::New(t); - constructor_template->SetClassName(String::NewSymbol("ODBCResult")); - - // Reserve space for one Handle - Local instance_template = constructor_template->InstanceTemplate(); - instance_template->SetInternalFieldCount(1); - - // Prototype Methods - NODE_SET_PROTOTYPE_METHOD(constructor_template, "fetchAll", FetchAll); - NODE_SET_PROTOTYPE_METHOD(constructor_template, "fetch", Fetch); - NODE_SET_PROTOTYPE_METHOD(constructor_template, "getColumnValue", GetColumnValue); - - NODE_SET_PROTOTYPE_METHOD(constructor_template, "moreResultsSync", MoreResultsSync); - NODE_SET_PROTOTYPE_METHOD(constructor_template, "closeSync", CloseSync); - NODE_SET_PROTOTYPE_METHOD(constructor_template, "fetchSync", FetchSync); - NODE_SET_PROTOTYPE_METHOD(constructor_template, "fetchAllSync", FetchAllSync); - NODE_SET_PROTOTYPE_METHOD(constructor_template, "getColumnNamesSync", GetColumnNamesSync); - NODE_SET_PROTOTYPE_METHOD(constructor_template, "getColumnValueSync", GetColumnValueSync); - - // Properties - instance_template->SetAccessor(String::New("fetchMode"), FetchModeGetter, FetchModeSetter); - - // Attach the Database Constructor to the target object - target->Set( v8::String::NewSymbol("ODBCResult"), - constructor_template->GetFunction()); - - scope.Close(Undefined()); -} - -ODBCResult::~ODBCResult() { - this->Free(); -} - -void ODBCResult::Free() { - DEBUG_PRINTF("ODBCResult::Free m_hSTMT=%X m_canFreeHandle=%X\n", m_hSTMT, m_canFreeHandle); - - if (m_hSTMT && m_canFreeHandle) { - uv_mutex_lock(&ODBC::g_odbcMutex); - - SQLFreeHandle( SQL_HANDLE_STMT, m_hSTMT); - - m_hSTMT = NULL; - - uv_mutex_unlock(&ODBC::g_odbcMutex); - } - - if (bufferLength > 0) { - bufferLength = 0; - free(buffer); - } -} - -Handle ODBCResult::New(const Arguments& args) { - DEBUG_PRINTF("ODBCResult::New\n"); - - HandleScope scope; - - REQ_EXT_ARG(0, js_henv); - REQ_EXT_ARG(1, js_hdbc); - REQ_EXT_ARG(2, js_hstmt); - REQ_EXT_ARG(3, js_canFreeHandle); - - HENV hENV = static_cast(js_henv->Value()); - HDBC hDBC = static_cast(js_hdbc->Value()); - HSTMT hSTMT = static_cast(js_hstmt->Value()); - bool* canFreeHandle = static_cast(js_canFreeHandle->Value()); - - //create a new OBCResult object - ODBCResult* objODBCResult = new ODBCResult(hENV, hDBC, hSTMT, *canFreeHandle); - - DEBUG_PRINTF("ODBCResult::New m_hDBC=%X m_hDBC=%X m_hSTMT=%X canFreeHandle=%X\n", - objODBCResult->m_hENV, - objODBCResult->m_hDBC, - objODBCResult->m_hSTMT, - objODBCResult->m_canFreeHandle - ); - - //free the pointer to canFreeHandle - delete canFreeHandle; - - //specify the buffer length - objODBCResult->bufferLength = MAX_VALUE_SIZE + (MAX_VALUE_SIZE % 2); // Ensure even so GetColumnValue doesn't break - - //initialze a buffer for this object - objODBCResult->buffer = (uint16_t *) malloc(objODBCResult->bufferLength + 1); - //TODO: make sure the malloc succeeded - - //set the initial colCount to 0 - objODBCResult->colCount = 0; - - //default fetchMode to FETCH_OBJECT - objODBCResult->m_fetchMode = FETCH_OBJECT; - - objODBCResult->Wrap(args.Holder()); - - return scope.Close(args.Holder()); -} - -Handle ODBCResult::FetchModeGetter(Local property, const AccessorInfo &info) { - HandleScope scope; - - ODBCResult *obj = ObjectWrap::Unwrap(info.Holder()); - - return scope.Close(Integer::New(obj->m_fetchMode)); -} - -void ODBCResult::FetchModeSetter(Local property, Local value, const AccessorInfo &info) { - HandleScope scope; - - ODBCResult *obj = ObjectWrap::Unwrap(info.Holder()); - - if (value->IsNumber()) { - obj->m_fetchMode = value->Int32Value(); - } -} - -/* - * Fetch - */ - -Handle ODBCResult::Fetch(const Arguments& args) { - DEBUG_PRINTF("ODBCResult::Fetch\n"); - - HandleScope scope; - - ODBCResult* objODBCResult = ObjectWrap::Unwrap(args.Holder()); - - uv_work_t* work_req = (uv_work_t *) (calloc(1, sizeof(uv_work_t))); - - fetch_work_data* data = (fetch_work_data *) calloc(1, sizeof(fetch_work_data)); - - Local cb; - - //set the fetch mode to the default of this instance - data->fetchMode = objODBCResult->m_fetchMode; - - if (args.Length() == 1 && args[0]->IsFunction()) { - cb = Local::Cast(args[0]); - } - else if (args.Length() == 2 && args[0]->IsObject() && args[1]->IsFunction()) { - cb = Local::Cast(args[1]); - - Local obj = args[0]->ToObject(); - - if (obj->Has(OPTION_FETCH_MODE) && obj->Get(OPTION_FETCH_MODE)->IsInt32()) { - data->fetchMode = obj->Get(OPTION_FETCH_MODE)->ToInt32()->Value(); - } - } - else { - return ThrowException(Exception::TypeError( - String::New("ODBCResult::Fetch(): 1 or 2 arguments are required. The last argument must be a callback function.") - )); - } - - data->cb = Persistent::New(cb); - - data->objResult = objODBCResult; - work_req->data = data; - - uv_queue_work( - uv_default_loop(), - work_req, - UV_Fetch, - (uv_after_work_cb)UV_AfterFetch); - - objODBCResult->Ref(); - - return scope.Close(Undefined()); -} - -void ODBCResult::UV_Fetch(uv_work_t* work_req) { - DEBUG_PRINTF("ODBCResult::UV_Fetch\n"); - - fetch_work_data* data = (fetch_work_data *)(work_req->data); - - data->result = SQLFetch(data->objResult->m_hSTMT); -} - -void ODBCResult::UV_AfterFetch(uv_work_t* work_req, int status) { - DEBUG_PRINTF("ODBCResult::UV_AfterFetch\n"); - - HandleScope scope; - - fetch_work_data* data = (fetch_work_data *)(work_req->data); - - SQLRETURN ret = data->result; - //TODO: we should probably define this on the work data so we - //don't have to keep creating it? - Local objError; - bool moreWork = true; - bool error = false; - - if (data->objResult->colCount == 0) { - data->objResult->columns = ODBC::GetColumns( - data->objResult->m_hSTMT, - &data->objResult->colCount); - } - - //check to see if the result has no columns - if (data->objResult->colCount == 0) { - //this means - moreWork = false; - } - //check to see if there was an error - else if (ret == SQL_ERROR) { - moreWork = false; - error = true; - - objError = ODBC::GetSQLError( - SQL_HANDLE_STMT, - data->objResult->m_hSTMT, - (char *) "Error in ODBCResult::UV_AfterFetch"); - } - //check to see if we are at the end of the recordset - else if (ret == SQL_NO_DATA) { - moreWork = false; - } - - if (moreWork) { - Handle args[3]; - - args[0] = Null(); - if (data->fetchMode == FETCH_ARRAY) { - args[1] = ODBC::GetRecordArray( - data->objResult->m_hSTMT, - data->objResult->columns, - &data->objResult->colCount, - data->objResult->buffer, - data->objResult->bufferLength); - } - else if (data->fetchMode == FETCH_OBJECT) { - args[1] = ODBC::GetRecordTuple( - data->objResult->m_hSTMT, - data->objResult->columns, - &data->objResult->colCount, - data->objResult->buffer, - data->objResult->bufferLength); - } - else { - args[1] = Null(); - } - - args[2] = True(); - - TryCatch try_catch; - - data->cb->Call(Context::GetCurrent()->Global(), 3, args); - data->cb.Dispose(); - - if (try_catch.HasCaught()) { - FatalException(try_catch); - } - } - else { - ODBC::FreeColumns(data->objResult->columns, &data->objResult->colCount); - - Handle args[3]; - - //if there was an error, pass that as arg[0] otherwise Null - if (error) { - args[0] = objError; - } - else { - args[0] = Null(); - } - - args[1] = Null(); - args[2] = False(); - - TryCatch try_catch; - - data->cb->Call(Context::GetCurrent()->Global(), 3, args); - data->cb.Dispose(); - - if (try_catch.HasCaught()) { - FatalException(try_catch); - } - } - - free(data); - free(work_req); - - data->objResult->Unref(); - - return; -} - -/* - * FetchSync - */ - -Handle ODBCResult::FetchSync(const Arguments& args) { - DEBUG_PRINTF("ODBCResult::FetchSync\n"); - - HandleScope scope; - - ODBCResult* objResult = ObjectWrap::Unwrap(args.Holder()); - - Local objError; - bool moreWork = true; - bool error = false; - int fetchMode = objResult->m_fetchMode; - - if (args.Length() == 1 && args[0]->IsObject()) { - Local obj = args[0]->ToObject(); - - if (obj->Has(OPTION_FETCH_MODE) && obj->Get(OPTION_FETCH_MODE)->IsInt32()) { - fetchMode = obj->Get(OPTION_FETCH_MODE)->ToInt32()->Value(); - } - } - - SQLRETURN ret = SQLFetch(objResult->m_hSTMT); - - if (objResult->colCount == 0) { - objResult->columns = ODBC::GetColumns( - objResult->m_hSTMT, - &objResult->colCount); - } - - //check to see if the result has no columns - if (objResult->colCount == 0) { - moreWork = false; - } - //check to see if there was an error - else if (ret == SQL_ERROR) { - moreWork = false; - error = true; - - objError = ODBC::GetSQLError( - SQL_HANDLE_STMT, - objResult->m_hSTMT, - (char *) "Error in ODBCResult::UV_AfterFetch"); - } - //check to see if we are at the end of the recordset - else if (ret == SQL_NO_DATA) { - moreWork = false; - } - - if (moreWork) { - Handle data; - - if (fetchMode == FETCH_ARRAY) { - data = ODBC::GetRecordArray( - objResult->m_hSTMT, - objResult->columns, - &objResult->colCount, - objResult->buffer, - objResult->bufferLength); - } - else if (fetchMode == FETCH_OBJECT) { - data = ODBC::GetRecordTuple( - objResult->m_hSTMT, - objResult->columns, - &objResult->colCount, - objResult->buffer, - objResult->bufferLength); - } else { - data = Null(); - } - - return scope.Close(data); - } - else { - ODBC::FreeColumns(objResult->columns, &objResult->colCount); - - //if there was an error, pass that as arg[0] otherwise Null - if (error) { - ThrowException(objError); - - return scope.Close(Null()); - } - else { - return scope.Close(Null()); - } - } -} - -/* - * FetchAll - */ - -Handle ODBCResult::FetchAll(const Arguments& args) { - DEBUG_PRINTF("ODBCResult::FetchAll\n"); - - HandleScope scope; - - ODBCResult* objODBCResult = ObjectWrap::Unwrap(args.Holder()); - - uv_work_t* work_req = (uv_work_t *) (calloc(1, sizeof(uv_work_t))); - - fetch_work_data* data = (fetch_work_data *) calloc(1, sizeof(fetch_work_data)); - - Local cb; - - data->fetchMode = objODBCResult->m_fetchMode; - - if (args.Length() == 1 && args[0]->IsFunction()) { - cb = Local::Cast(args[0]); - } - else if (args.Length() == 2 && args[0]->IsObject() && args[1]->IsFunction()) { - cb = Local::Cast(args[1]); - - Local obj = args[0]->ToObject(); - - if (obj->Has(OPTION_FETCH_MODE) && obj->Get(OPTION_FETCH_MODE)->IsInt32()) { - data->fetchMode = obj->Get(OPTION_FETCH_MODE)->ToInt32()->Value(); - } - } - else { - return ThrowException(Exception::TypeError( - String::New("ODBCResult::FetchAll(): 1 or 2 arguments are required. The last argument must be a callback function.") - )); - } - - data->rows = Persistent::New(Array::New()); - data->errorCount = 0; - data->count = 0; - data->objError = Persistent::New(Object::New()); - - data->cb = Persistent::New(cb); - data->objResult = objODBCResult; - - work_req->data = data; - - uv_queue_work(uv_default_loop(), - work_req, - UV_FetchAll, - (uv_after_work_cb)UV_AfterFetchAll); - - data->objResult->Ref(); - - return scope.Close(Undefined()); -} - -void ODBCResult::UV_FetchAll(uv_work_t* work_req) { - DEBUG_PRINTF("ODBCResult::UV_FetchAll\n"); - - fetch_work_data* data = (fetch_work_data *)(work_req->data); - - data->result = SQLFetch(data->objResult->m_hSTMT); - } - -void ODBCResult::UV_AfterFetchAll(uv_work_t* work_req, int status) { - DEBUG_PRINTF("ODBCResult::UV_AfterFetchAll\n"); - - HandleScope scope; - - fetch_work_data* data = (fetch_work_data *)(work_req->data); - - ODBCResult* self = data->objResult->self(); - - bool doMoreWork = true; - - if (self->colCount == 0) { - self->columns = ODBC::GetColumns(self->m_hSTMT, &self->colCount); - } - - //check to see if the result set has columns - if (self->colCount == 0) { - //this most likely means that the query was something like - //'insert into ....' - doMoreWork = false; - } - //check to see if there was an error - else if (data->result == SQL_ERROR) { - data->errorCount++; - - data->objError = Persistent::New(ODBC::GetSQLError( - SQL_HANDLE_STMT, - self->m_hSTMT, - (char *) "[node-odbc] Error in ODBCResult::UV_AfterFetchAll" - )); - - doMoreWork = false; - } - //check to see if we are at the end of the recordset - else if (data->result == SQL_NO_DATA) { - doMoreWork = false; - } - else { - if (data->fetchMode == FETCH_ARRAY) { - data->rows->Set( - Integer::New(data->count), - ODBC::GetRecordArray( - self->m_hSTMT, - self->columns, - &self->colCount, - self->buffer, - self->bufferLength) - ); - } - else if (data->fetchMode == FETCH_OBJECT) { - data->rows->Set( - Integer::New(data->count), - ODBC::GetRecordTuple( - self->m_hSTMT, - self->columns, - &self->colCount, - self->buffer, - self->bufferLength) - ); - } - data->count++; - } - - if (doMoreWork) { - //Go back to the thread pool and fetch more data! - uv_queue_work( - uv_default_loop(), - work_req, - UV_FetchAll, - (uv_after_work_cb)UV_AfterFetchAll); - } - else { - ODBC::FreeColumns(self->columns, &self->colCount); - - Handle args[2]; - - if (data->errorCount > 0) { - args[0] = Local::New(data->objError); - } - else { - args[0] = Null(); - } - - args[1] = Local::New(data->rows); - - TryCatch try_catch; - - data->cb->Call(Context::GetCurrent()->Global(), 2, args); - data->cb.Dispose(); - data->rows.Dispose(); - data->objError.Dispose(); - - if (try_catch.HasCaught()) { - FatalException(try_catch); - } - - //TODO: Do we need to free self->rows somehow? - free(data); - free(work_req); - - self->Unref(); - } - - scope.Close(Undefined()); -} - -/* - * FetchAllSync - */ - -Handle ODBCResult::FetchAllSync(const Arguments& args) { - DEBUG_PRINTF("ODBCResult::FetchAllSync\n"); - - HandleScope scope; - - ODBCResult* self = ObjectWrap::Unwrap(args.Holder()); - - Local objError = Object::New(); - - SQLRETURN ret; - int count = 0; - int errorCount = 0; - int fetchMode = self->m_fetchMode; - - if (args.Length() == 1 && args[0]->IsObject()) { - Local obj = args[0]->ToObject(); - - if (obj->Has(OPTION_FETCH_MODE) && obj->Get(OPTION_FETCH_MODE)->IsInt32()) { - fetchMode = obj->Get(OPTION_FETCH_MODE)->ToInt32()->Value(); - } - } - - if (self->colCount == 0) { - self->columns = ODBC::GetColumns(self->m_hSTMT, &self->colCount); - } - - Local rows = Array::New(); - - //Only loop through the recordset if there are columns - if (self->colCount > 0) { - //loop through all records - while (true) { - ret = SQLFetch(self->m_hSTMT); - - //check to see if there was an error - if (ret == SQL_ERROR) { - errorCount++; - - objError = ODBC::GetSQLError( - SQL_HANDLE_STMT, - self->m_hSTMT, - (char *) "[node-odbc] Error in ODBCResult::UV_AfterFetchAll; probably" - " your query did not have a result set." - ); - - break; - } - - //check to see if we are at the end of the recordset - if (ret == SQL_NO_DATA) { - ODBC::FreeColumns(self->columns, &self->colCount); - - break; - } - - Handle value; - if (fetchMode == FETCH_ARRAY) - value = - ODBC::GetRecordArray( - self->m_hSTMT, - self->columns, - &self->colCount, - self->buffer, - self->bufferLength); - else if (fetchMode == FETCH_OBJECT) - value = ODBC::GetRecordTuple( - self->m_hSTMT, - self->columns, - &self->colCount, - self->buffer, - self->bufferLength); - - rows->Set(Integer::New(count), value); - count++; - } - } - else { - ODBC::FreeColumns(self->columns, &self->colCount); - } - - //throw the error object if there were errors - if (errorCount > 0) { - ThrowException(objError); - } - - return scope.Close(rows); -} - -/* - * CloseSync - * - */ - -Handle ODBCResult::CloseSync(const Arguments& args) { - DEBUG_PRINTF("ODBCResult::CloseSync\n"); - - HandleScope scope; - - OPT_INT_ARG(0, closeOption, SQL_DESTROY); - - ODBCResult* result = ObjectWrap::Unwrap(args.Holder()); - - DEBUG_PRINTF("ODBCResult::CloseSync closeOption=%i m_canFreeHandle=%i\n", - closeOption, result->m_canFreeHandle); - - if (closeOption == SQL_DESTROY && result->m_canFreeHandle) { - result->Free(); - } - else if (closeOption == SQL_DESTROY && !result->m_canFreeHandle) { - //We technically can't free the handle so, we'll SQL_CLOSE - uv_mutex_lock(&ODBC::g_odbcMutex); - - SQLFreeStmt(result->m_hSTMT, SQL_CLOSE); - - uv_mutex_unlock(&ODBC::g_odbcMutex); - } - else { - uv_mutex_lock(&ODBC::g_odbcMutex); - - SQLFreeStmt(result->m_hSTMT, closeOption); - - uv_mutex_unlock(&ODBC::g_odbcMutex); - } - - return scope.Close(True()); -} - -Handle ODBCResult::MoreResultsSync(const Arguments& args) { - DEBUG_PRINTF("ODBCResult::MoreResultsSync\n"); - - HandleScope scope; - - ODBCResult* result = ObjectWrap::Unwrap(args.Holder()); - - SQLRETURN ret = SQLMoreResults(result->m_hSTMT); - - if (ret == SQL_ERROR) { - ThrowException(ODBC::GetSQLError(SQL_HANDLE_STMT, result->m_hSTMT, (char *)"[node-odbc] Error in ODBCResult::MoreResultsSync")); - } - - return scope.Close(SQL_SUCCEEDED(ret) || ret == SQL_ERROR ? True() : False()); -} - -/* - * GetColumnNamesSync - */ - -Handle ODBCResult::GetColumnNamesSync(const Arguments& args) { - DEBUG_PRINTF("ODBCResult::GetColumnNamesSync\n"); - - HandleScope scope; - - ODBCResult* self = ObjectWrap::Unwrap(args.Holder()); - - Local cols = Array::New(); - - if (self->colCount == 0) { - self->columns = ODBC::GetColumns(self->m_hSTMT, &self->colCount); - } - - for (int i = 0; i < self->colCount; i++) { - cols->Set(Integer::New(i), - String::New((const char *) self->columns[i].name)); - } - - return scope.Close(cols); -} - -/* - * GetColumnValue - */ - -Handle ODBCResult::GetColumnValue(const Arguments& args) { - DEBUG_PRINTF("ODBCResult::GetColumnValue\n"); - - HandleScope scope; - - ODBCResult* objODBCResult = ObjectWrap::Unwrap(args.Holder()); - - uv_work_t* work_req = (uv_work_t *) (calloc(1, sizeof(uv_work_t))); - - get_column_value_work_data* data = (get_column_value_work_data *) calloc(1, sizeof(get_column_value_work_data)); - - REQ_INT32_ARG(0, col); - - int maxBytes; - Local cb; - - if (args.Length() == 3) { - REQ_INT32_ARG(1, tmpMaxBytes); maxBytes = tmpMaxBytes; - REQ_FUN_ARG(2, tmpCB); cb = tmpCB; - } else if (args.Length() == 2) { - REQ_FUN_ARG(3, tmpCB); cb = tmpCB; - maxBytes = 0; // Not a partial request - } - - data->col = col; - data->bytesRequested = maxBytes; - data->cb = Persistent::New(cb); - data->objResult = objODBCResult; - data->resultBufferContents = 0; - data->resultBufferOffset = 0; - data->resultBufferLength = 0; - work_req->data = data; - - uv_queue_work( - uv_default_loop(), - work_req, - UV_GetColumnValue, - (uv_after_work_cb)UV_AfterGetColumnValue); - - objODBCResult->Ref(); - - return scope.Close(Undefined()); -} - -void ODBCResult::UV_GetColumnValue(uv_work_t* work_req) { - DEBUG_PRINTF("ODBCResult::UV_GetColumnValue\n"); - - get_column_value_work_data* data = (get_column_value_work_data*)(work_req->data); - - SQLLEN bytesRequested = data->bytesRequested; - if (bytesRequested <= 0 || bytesRequested > data->objResult->bufferLength) - bytesRequested = data->objResult->bufferLength; - - data->cType = ODBC::GetCColumnType(data->objResult->columns[data->col]); - - data->result = ODBC::FetchMoreData( - data->objResult->m_hSTMT, - data->objResult->columns[data->col], - data->cType, - data->bytesAvailable, - data->bytesRead, - data->objResult->buffer, bytesRequested, - data->resultBufferContents, data->resultBufferOffset, data->resultBufferLength); -} - -void ODBCResult::UV_AfterGetColumnValue(uv_work_t* work_req, int status) { - DEBUG_PRINTF("ODBCResult::UV_AfterGetColumnValue\n"); - - HandleScope scope; - - get_column_value_work_data* data = (get_column_value_work_data*)(work_req->data); - - SQLRETURN ret = data->result; - - DEBUG_PRINTF("ODBCResult::UV_AfterGetColumnValue: ret=%i, cType=%i, bytesRequested=%i, bytesRead=%i\n", ret, data->cType, data->bytesRequested, data->bytesRead); - - Handle args[3]; - int argc = 3; - - if (!SQL_SUCCEEDED(ret)) { - args[0] = ODBC::GetSQLError(SQL_HANDLE_STMT, data->objResult->m_hSTMT, "[node-odbc] Error in ODBCResult::GetColumnValue"); - argc = 1; - } else if (data->bytesRead == SQL_NULL_DATA) { - args[0] = Null(); - args[1] = Null(); - args[2] = False(); - } else if (data->cType != SQL_C_BINARY && data->cType != SQL_C_TCHAR) { - args[0] = Null(); - args[1] = ODBC::ConvertColumnValue(data->cType, data->objResult->buffer, data->bytesRead, 0, 0); - args[2] = False(); - } else if (data->bytesRequested == 0 && data->result == SQL_SUCCESS_WITH_INFO) { - // Not partial request, and there is more data to fetch ... queue some more work - - if (!data->resultBufferContents) { - Buffer* b = Buffer::New(data->bytesAvailable + (data->cType == SQL_C_TCHAR ? sizeof(TCHAR) : 0)); // Allow space for ODBC to add an unnecessary null terminator - memcpy(Buffer::Data(b), data->objResult->buffer, data->bytesRead); - data->resultBuffer = b->handle_; - data->resultBufferOffset = data->bytesRead; - data->resultBufferContents = Buffer::Data(b); - data->resultBufferLength = Buffer::Length(b); - DEBUG_PRINTF(" *** Created new SlowBuffer (length: %i), copied %i bytes from internal buffer\n", Buffer::Length(b), data->bytesRead); - } - - uv_queue_work( - uv_default_loop(), - work_req, - UV_GetColumnValue, - (uv_after_work_cb)UV_AfterGetColumnValue); - - return; - } else { - if (!data->resultBufferContents && data->cType == SQL_C_BINARY) { - Buffer* b = Buffer::New((const char*)data->objResult->buffer, (size_t)data->bytesRead); - data->resultBuffer = b->handle_; - data->resultBufferOffset = data->bytesRead; - data->resultBufferContents = Buffer::Data(b); - data->resultBufferLength = Buffer::Length(b); - DEBUG_PRINTF(" *** Created new SlowBuffer (length: %i), copied %i bytes from internal buffer\n", Buffer::Length(b), data->bytesRead); - } - - args[0] = Null(); - args[1] = ODBC::InterpretBuffers(data->cType, data->objResult->buffer, data->bytesRead, data->resultBuffer, data->resultBufferContents, data->resultBufferOffset); - args[2] = ret == SQL_SUCCESS_WITH_INFO ? True() : False(); - } - - TryCatch try_catch; - - data->cb->Call(Context::GetCurrent()->Global(), argc, args); - data->cb.Dispose(); - if (data->resultBufferContents) - data->resultBuffer.Dispose(); - - if (try_catch.HasCaught()) { - FatalException(try_catch); - } - - free(data); - free(work_req); - - data->objResult->Unref(); -} - -Handle ODBCResult::GetColumnValueSync(const Arguments& args) { - DEBUG_PRINTF("ODBCResult::GetColumnValueSync\n"); - - HandleScope scope; - - ODBCResult* objODBCResult = ObjectWrap::Unwrap(args.Holder()); - - REQ_INT32_ARG(0, col); - if (col < 0 || col >= objODBCResult->colCount) - return ThrowException(Exception::RangeError( - String::New("ODBCResult::GetColumnValueSync(): The column index requested is invalid.") - )); - - bool partial = true; - OPT_INT_ARG(1, maxBytes, 0); - if (maxBytes == 0) { - partial = false; - maxBytes = objODBCResult->bufferLength; - } - - DEBUG_PRINTF("ODBCResult::GetColumnValueSync: columns=0x%x, colCount=%i, column=%i, maxBytes=%i\n", objODBCResult->columns, objODBCResult->colCount, col, maxBytes); - - return scope.Close( - ODBC::GetColumnValue(objODBCResult->m_hSTMT, objODBCResult->columns[col], objODBCResult->buffer, maxBytes, true) - ); +/* + Copyright (c) 2013, Dan VerWeire + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +#include +#include +#include +#include +#include +#include +#include + +#include "odbc.h" +#include "odbc_connection.h" +#include "odbc_result.h" +#include "odbc_statement.h" + +using namespace v8; +using namespace node; + +Persistent ODBCResult::constructor_template; +Persistent ODBCResult::OPTION_FETCH_MODE = Persistent::New(String::New("fetchMode")); + +void ODBCResult::Init(v8::Handle target) { + DEBUG_PRINTF("ODBCResult::Init\n"); + HandleScope scope; + + Local t = FunctionTemplate::New(New); + + // Constructor Template + constructor_template = Persistent::New(t); + constructor_template->SetClassName(String::NewSymbol("ODBCResult")); + + // Reserve space for one Handle + Local instance_template = constructor_template->InstanceTemplate(); + instance_template->SetInternalFieldCount(1); + + // Prototype Methods + NODE_SET_PROTOTYPE_METHOD(constructor_template, "fetchAll", FetchAll); + NODE_SET_PROTOTYPE_METHOD(constructor_template, "fetch", Fetch); + NODE_SET_PROTOTYPE_METHOD(constructor_template, "getColumnValue", GetColumnValue); + + NODE_SET_PROTOTYPE_METHOD(constructor_template, "moreResultsSync", MoreResultsSync); + NODE_SET_PROTOTYPE_METHOD(constructor_template, "closeSync", CloseSync); + NODE_SET_PROTOTYPE_METHOD(constructor_template, "fetchSync", FetchSync); + NODE_SET_PROTOTYPE_METHOD(constructor_template, "fetchAllSync", FetchAllSync); + NODE_SET_PROTOTYPE_METHOD(constructor_template, "getColumnNamesSync", GetColumnNamesSync); + NODE_SET_PROTOTYPE_METHOD(constructor_template, "getColumnValueSync", GetColumnValueSync); + + // Properties + instance_template->SetAccessor(String::New("fetchMode"), FetchModeGetter, FetchModeSetter); + + // Attach the Database Constructor to the target object + target->Set( v8::String::NewSymbol("ODBCResult"), + constructor_template->GetFunction()); + + scope.Close(Undefined()); +} + +ODBCResult::~ODBCResult() { + this->Free(); +} + +void ODBCResult::Free() { + DEBUG_PRINTF("ODBCResult::Free m_hSTMT=%X m_canFreeHandle=%X\n", m_hSTMT, m_canFreeHandle); + + if (m_hSTMT && m_canFreeHandle) { + uv_mutex_lock(&ODBC::g_odbcMutex); + + SQLFreeHandle( SQL_HANDLE_STMT, m_hSTMT); + + m_hSTMT = NULL; + + uv_mutex_unlock(&ODBC::g_odbcMutex); + } + + if (bufferLength > 0) { + bufferLength = 0; + free(buffer); + } +} + +Handle ODBCResult::New(const Arguments& args) { + DEBUG_PRINTF("ODBCResult::New\n"); + + HandleScope scope; + + REQ_EXT_ARG(0, js_henv); + REQ_EXT_ARG(1, js_hdbc); + REQ_EXT_ARG(2, js_hstmt); + REQ_EXT_ARG(3, js_canFreeHandle); + + HENV hENV = static_cast(js_henv->Value()); + HDBC hDBC = static_cast(js_hdbc->Value()); + HSTMT hSTMT = static_cast(js_hstmt->Value()); + bool* canFreeHandle = static_cast(js_canFreeHandle->Value()); + + //create a new OBCResult object + ODBCResult* objODBCResult = new ODBCResult(hENV, hDBC, hSTMT, *canFreeHandle); + + DEBUG_PRINTF("ODBCResult::New m_hDBC=%X m_hDBC=%X m_hSTMT=%X canFreeHandle=%X\n", + objODBCResult->m_hENV, + objODBCResult->m_hDBC, + objODBCResult->m_hSTMT, + objODBCResult->m_canFreeHandle + ); + + //free the pointer to canFreeHandle + delete canFreeHandle; + + //specify the buffer length + objODBCResult->bufferLength = MAX_VALUE_SIZE + (MAX_VALUE_SIZE % 2); // Ensure even so GetColumnValue doesn't break + + //initialze a buffer for this object + objODBCResult->buffer = (uint16_t *) malloc(objODBCResult->bufferLength + 1); + //TODO: make sure the malloc succeeded + + //set the initial colCount to 0 + objODBCResult->colCount = 0; + + //default fetchMode to FETCH_OBJECT + objODBCResult->m_fetchMode = FETCH_OBJECT; + + objODBCResult->Wrap(args.Holder()); + + return scope.Close(args.Holder()); +} + +Handle ODBCResult::FetchModeGetter(Local property, const AccessorInfo &info) { + HandleScope scope; + + ODBCResult *obj = ObjectWrap::Unwrap(info.Holder()); + + return scope.Close(Integer::New(obj->m_fetchMode)); +} + +void ODBCResult::FetchModeSetter(Local property, Local value, const AccessorInfo &info) { + HandleScope scope; + + ODBCResult *obj = ObjectWrap::Unwrap(info.Holder()); + + if (value->IsNumber()) { + obj->m_fetchMode = value->Int32Value(); + } +} + +/* + * Fetch + */ + +Handle ODBCResult::Fetch(const Arguments& args) { + DEBUG_PRINTF("ODBCResult::Fetch\n"); + + HandleScope scope; + + ODBCResult* objODBCResult = ObjectWrap::Unwrap(args.Holder()); + + uv_work_t* work_req = (uv_work_t *) (calloc(1, sizeof(uv_work_t))); + + fetch_work_data* data = (fetch_work_data *) calloc(1, sizeof(fetch_work_data)); + + Local cb; + + //set the fetch mode to the default of this instance + data->fetchMode = objODBCResult->m_fetchMode; + + if (args.Length() == 1 && args[0]->IsFunction()) { + cb = Local::Cast(args[0]); + } + else if (args.Length() == 2 && args[0]->IsObject() && args[1]->IsFunction()) { + cb = Local::Cast(args[1]); + + Local obj = args[0]->ToObject(); + + if (obj->Has(OPTION_FETCH_MODE) && obj->Get(OPTION_FETCH_MODE)->IsInt32()) { + data->fetchMode = obj->Get(OPTION_FETCH_MODE)->ToInt32()->Value(); + } + } + else { + return ThrowException(Exception::TypeError( + String::New("ODBCResult::Fetch(): 1 or 2 arguments are required. The last argument must be a callback function.") + )); + } + + data->cb = Persistent::New(cb); + + data->objResult = objODBCResult; + work_req->data = data; + + uv_queue_work( + uv_default_loop(), + work_req, + UV_Fetch, + (uv_after_work_cb)UV_AfterFetch); + + objODBCResult->Ref(); + + return scope.Close(Undefined()); +} + +void ODBCResult::UV_Fetch(uv_work_t* work_req) { + DEBUG_PRINTF("ODBCResult::UV_Fetch\n"); + + fetch_work_data* data = (fetch_work_data *)(work_req->data); + + data->result = SQLFetch(data->objResult->m_hSTMT); +} + +void ODBCResult::UV_AfterFetch(uv_work_t* work_req, int status) { + DEBUG_PRINTF("ODBCResult::UV_AfterFetch\n"); + + HandleScope scope; + + fetch_work_data* data = (fetch_work_data *)(work_req->data); + + SQLRETURN ret = data->result; + //TODO: we should probably define this on the work data so we + //don't have to keep creating it? + Local objError; + bool moreWork = true; + bool error = false; + + if (data->objResult->colCount == 0) { + data->objResult->columns = ODBC::GetColumns( + data->objResult->m_hSTMT, + &data->objResult->colCount); + } + + //check to see if the result has no columns + if (data->objResult->colCount == 0) { + //this means + moreWork = false; + } + //check to see if there was an error + else if (ret == SQL_ERROR) { + moreWork = false; + error = true; + + objError = ODBC::GetSQLError( + SQL_HANDLE_STMT, + data->objResult->m_hSTMT, + (char *) "Error in ODBCResult::UV_AfterFetch"); + } + //check to see if we are at the end of the recordset + else if (ret == SQL_NO_DATA) { + moreWork = false; + } + + if (moreWork) { + Handle args[3]; + + args[0] = Null(); + if (data->fetchMode == FETCH_ARRAY) { + args[1] = ODBC::GetRecordArray( + data->objResult->m_hSTMT, + data->objResult->columns, + &data->objResult->colCount, + data->objResult->buffer, + data->objResult->bufferLength); + } + else if (data->fetchMode == FETCH_OBJECT) { + args[1] = ODBC::GetRecordTuple( + data->objResult->m_hSTMT, + data->objResult->columns, + &data->objResult->colCount, + data->objResult->buffer, + data->objResult->bufferLength); + } + else { + args[1] = Null(); + } + + args[2] = True(); + + TryCatch try_catch; + + data->cb->Call(Context::GetCurrent()->Global(), 3, args); + data->cb.Dispose(); + + if (try_catch.HasCaught()) { + FatalException(try_catch); + } + } + else { + ODBC::FreeColumns(data->objResult->columns, &data->objResult->colCount); + + Handle args[3]; + + //if there was an error, pass that as arg[0] otherwise Null + if (error) { + args[0] = objError; + } + else { + args[0] = Null(); + } + + args[1] = Null(); + args[2] = False(); + + TryCatch try_catch; + + data->cb->Call(Context::GetCurrent()->Global(), 3, args); + data->cb.Dispose(); + + if (try_catch.HasCaught()) { + FatalException(try_catch); + } + } + + free(data); + free(work_req); + + data->objResult->Unref(); + + return; +} + +/* + * FetchSync + */ + +Handle ODBCResult::FetchSync(const Arguments& args) { + DEBUG_PRINTF("ODBCResult::FetchSync\n"); + + HandleScope scope; + + ODBCResult* objResult = ObjectWrap::Unwrap(args.Holder()); + + Local objError; + bool moreWork = true; + bool error = false; + int fetchMode = objResult->m_fetchMode; + + if (args.Length() == 1 && args[0]->IsObject()) { + Local obj = args[0]->ToObject(); + + if (obj->Has(OPTION_FETCH_MODE) && obj->Get(OPTION_FETCH_MODE)->IsInt32()) { + fetchMode = obj->Get(OPTION_FETCH_MODE)->ToInt32()->Value(); + } + } + + SQLRETURN ret = SQLFetch(objResult->m_hSTMT); + + if (objResult->colCount == 0) { + objResult->columns = ODBC::GetColumns( + objResult->m_hSTMT, + &objResult->colCount); + } + + //check to see if the result has no columns + if (objResult->colCount == 0) { + moreWork = false; + } + //check to see if there was an error + else if (ret == SQL_ERROR) { + moreWork = false; + error = true; + + objError = ODBC::GetSQLError( + SQL_HANDLE_STMT, + objResult->m_hSTMT, + (char *) "Error in ODBCResult::UV_AfterFetch"); + } + //check to see if we are at the end of the recordset + else if (ret == SQL_NO_DATA) { + moreWork = false; + } + + if (moreWork) { + Handle data; + + if (fetchMode == FETCH_ARRAY) { + data = ODBC::GetRecordArray( + objResult->m_hSTMT, + objResult->columns, + &objResult->colCount, + objResult->buffer, + objResult->bufferLength); + } + else if (fetchMode == FETCH_OBJECT) { + data = ODBC::GetRecordTuple( + objResult->m_hSTMT, + objResult->columns, + &objResult->colCount, + objResult->buffer, + objResult->bufferLength); + } else { + data = Null(); + } + + return scope.Close(data); + } + else { + ODBC::FreeColumns(objResult->columns, &objResult->colCount); + + //if there was an error, pass that as arg[0] otherwise Null + if (error) { + ThrowException(objError); + + return scope.Close(Null()); + } + else { + return scope.Close(Null()); + } + } +} + +/* + * FetchAll + */ + +Handle ODBCResult::FetchAll(const Arguments& args) { + DEBUG_PRINTF("ODBCResult::FetchAll\n"); + + HandleScope scope; + + ODBCResult* objODBCResult = ObjectWrap::Unwrap(args.Holder()); + + uv_work_t* work_req = (uv_work_t *) (calloc(1, sizeof(uv_work_t))); + + fetch_work_data* data = (fetch_work_data *) calloc(1, sizeof(fetch_work_data)); + + Local cb; + + data->fetchMode = objODBCResult->m_fetchMode; + + if (args.Length() == 1 && args[0]->IsFunction()) { + cb = Local::Cast(args[0]); + } + else if (args.Length() == 2 && args[0]->IsObject() && args[1]->IsFunction()) { + cb = Local::Cast(args[1]); + + Local obj = args[0]->ToObject(); + + if (obj->Has(OPTION_FETCH_MODE) && obj->Get(OPTION_FETCH_MODE)->IsInt32()) { + data->fetchMode = obj->Get(OPTION_FETCH_MODE)->ToInt32()->Value(); + } + } + else { + return ThrowException(Exception::TypeError( + String::New("ODBCResult::FetchAll(): 1 or 2 arguments are required. The last argument must be a callback function.") + )); + } + + data->rows = Persistent::New(Array::New()); + data->errorCount = 0; + data->count = 0; + data->objError = Persistent::New(Object::New()); + + data->cb = Persistent::New(cb); + data->objResult = objODBCResult; + + work_req->data = data; + + uv_queue_work(uv_default_loop(), + work_req, + UV_FetchAll, + (uv_after_work_cb)UV_AfterFetchAll); + + data->objResult->Ref(); + + return scope.Close(Undefined()); +} + +void ODBCResult::UV_FetchAll(uv_work_t* work_req) { + DEBUG_PRINTF("ODBCResult::UV_FetchAll\n"); + + fetch_work_data* data = (fetch_work_data *)(work_req->data); + + data->result = SQLFetch(data->objResult->m_hSTMT); + } + +void ODBCResult::UV_AfterFetchAll(uv_work_t* work_req, int status) { + DEBUG_PRINTF("ODBCResult::UV_AfterFetchAll\n"); + + HandleScope scope; + + fetch_work_data* data = (fetch_work_data *)(work_req->data); + + ODBCResult* self = data->objResult->self(); + + bool doMoreWork = true; + + if (self->colCount == 0) { + self->columns = ODBC::GetColumns(self->m_hSTMT, &self->colCount); + } + + //check to see if the result set has columns + if (self->colCount == 0) { + //this most likely means that the query was something like + //'insert into ....' + doMoreWork = false; + } + //check to see if there was an error + else if (data->result == SQL_ERROR) { + data->errorCount++; + + data->objError = Persistent::New(ODBC::GetSQLError( + SQL_HANDLE_STMT, + self->m_hSTMT, + (char *) "[node-odbc] Error in ODBCResult::UV_AfterFetchAll" + )); + + doMoreWork = false; + } + //check to see if we are at the end of the recordset + else if (data->result == SQL_NO_DATA) { + doMoreWork = false; + } + else { + if (data->fetchMode == FETCH_ARRAY) { + data->rows->Set( + Integer::New(data->count), + ODBC::GetRecordArray( + self->m_hSTMT, + self->columns, + &self->colCount, + self->buffer, + self->bufferLength) + ); + } + else if (data->fetchMode == FETCH_OBJECT) { + data->rows->Set( + Integer::New(data->count), + ODBC::GetRecordTuple( + self->m_hSTMT, + self->columns, + &self->colCount, + self->buffer, + self->bufferLength) + ); + } + data->count++; + } + + if (doMoreWork) { + //Go back to the thread pool and fetch more data! + uv_queue_work( + uv_default_loop(), + work_req, + UV_FetchAll, + (uv_after_work_cb)UV_AfterFetchAll); + } + else { + ODBC::FreeColumns(self->columns, &self->colCount); + + Handle args[2]; + + if (data->errorCount > 0) { + args[0] = Local::New(data->objError); + } + else { + args[0] = Null(); + } + + args[1] = Local::New(data->rows); + + TryCatch try_catch; + + data->cb->Call(Context::GetCurrent()->Global(), 2, args); + data->cb.Dispose(); + data->rows.Dispose(); + data->objError.Dispose(); + + if (try_catch.HasCaught()) { + FatalException(try_catch); + } + + //TODO: Do we need to free self->rows somehow? + free(data); + free(work_req); + + self->Unref(); + } + + scope.Close(Undefined()); +} + +/* + * FetchAllSync + */ + +Handle ODBCResult::FetchAllSync(const Arguments& args) { + DEBUG_PRINTF("ODBCResult::FetchAllSync\n"); + + HandleScope scope; + + ODBCResult* self = ObjectWrap::Unwrap(args.Holder()); + + Local objError = Object::New(); + + SQLRETURN ret; + int count = 0; + int errorCount = 0; + int fetchMode = self->m_fetchMode; + + if (args.Length() == 1 && args[0]->IsObject()) { + Local obj = args[0]->ToObject(); + + if (obj->Has(OPTION_FETCH_MODE) && obj->Get(OPTION_FETCH_MODE)->IsInt32()) { + fetchMode = obj->Get(OPTION_FETCH_MODE)->ToInt32()->Value(); + } + } + + if (self->colCount == 0) { + self->columns = ODBC::GetColumns(self->m_hSTMT, &self->colCount); + } + + Local rows = Array::New(); + + //Only loop through the recordset if there are columns + if (self->colCount > 0) { + //loop through all records + while (true) { + ret = SQLFetch(self->m_hSTMT); + + //check to see if there was an error + if (ret == SQL_ERROR) { + errorCount++; + + objError = ODBC::GetSQLError( + SQL_HANDLE_STMT, + self->m_hSTMT, + (char *) "[node-odbc] Error in ODBCResult::UV_AfterFetchAll; probably" + " your query did not have a result set." + ); + + break; + } + + //check to see if we are at the end of the recordset + if (ret == SQL_NO_DATA) { + ODBC::FreeColumns(self->columns, &self->colCount); + + break; + } + + Handle value; + if (fetchMode == FETCH_ARRAY) + value = + ODBC::GetRecordArray( + self->m_hSTMT, + self->columns, + &self->colCount, + self->buffer, + self->bufferLength); + else if (fetchMode == FETCH_OBJECT) + value = ODBC::GetRecordTuple( + self->m_hSTMT, + self->columns, + &self->colCount, + self->buffer, + self->bufferLength); + + rows->Set(Integer::New(count), value); + count++; + } + } + else { + ODBC::FreeColumns(self->columns, &self->colCount); + } + + //throw the error object if there were errors + if (errorCount > 0) { + ThrowException(objError); + } + + return scope.Close(rows); +} + +/* + * CloseSync + * + */ + +Handle ODBCResult::CloseSync(const Arguments& args) { + DEBUG_PRINTF("ODBCResult::CloseSync\n"); + + HandleScope scope; + + OPT_INT_ARG(0, closeOption, SQL_DESTROY); + + ODBCResult* result = ObjectWrap::Unwrap(args.Holder()); + + DEBUG_PRINTF("ODBCResult::CloseSync closeOption=%i m_canFreeHandle=%i\n", + closeOption, result->m_canFreeHandle); + + if (closeOption == SQL_DESTROY && result->m_canFreeHandle) { + result->Free(); + } + else if (closeOption == SQL_DESTROY && !result->m_canFreeHandle) { + //We technically can't free the handle so, we'll SQL_CLOSE + uv_mutex_lock(&ODBC::g_odbcMutex); + + SQLFreeStmt(result->m_hSTMT, SQL_CLOSE); + + uv_mutex_unlock(&ODBC::g_odbcMutex); + } + else { + uv_mutex_lock(&ODBC::g_odbcMutex); + + SQLFreeStmt(result->m_hSTMT, closeOption); + + uv_mutex_unlock(&ODBC::g_odbcMutex); + } + + return scope.Close(True()); +} + +Handle ODBCResult::MoreResultsSync(const Arguments& args) { + DEBUG_PRINTF("ODBCResult::MoreResultsSync\n"); + + HandleScope scope; + + ODBCResult* result = ObjectWrap::Unwrap(args.Holder()); + + SQLRETURN ret = SQLMoreResults(result->m_hSTMT); + + if (ret == SQL_ERROR) { + ThrowException(ODBC::GetSQLError(SQL_HANDLE_STMT, result->m_hSTMT, (char *)"[node-odbc] Error in ODBCResult::MoreResultsSync")); + } + + return scope.Close(SQL_SUCCEEDED(ret) || ret == SQL_ERROR ? True() : False()); +} + +/* + * GetColumnNamesSync + */ + +Handle ODBCResult::GetColumnNamesSync(const Arguments& args) { + DEBUG_PRINTF("ODBCResult::GetColumnNamesSync\n"); + + HandleScope scope; + + ODBCResult* self = ObjectWrap::Unwrap(args.Holder()); + + Local cols = Array::New(); + + if (self->colCount == 0) { + self->columns = ODBC::GetColumns(self->m_hSTMT, &self->colCount); + } + + for (int i = 0; i < self->colCount; i++) { + cols->Set(Integer::New(i), + String::New((const char *) self->columns[i].name)); + } + + return scope.Close(cols); +} + +/* + * GetColumnValue + */ + +Handle ODBCResult::GetColumnValue(const Arguments& args) { + DEBUG_PRINTF("ODBCResult::GetColumnValue\n"); + + HandleScope scope; + + ODBCResult* objODBCResult = ObjectWrap::Unwrap(args.Holder()); + + if (!objODBCResult->columns) + return ThrowException(Exception::Error(String::New("ODBCResult::GetColumnValue(): There are currently no columns. You must call Fetch() before calling GetColumnValue()."))); + + REQ_INT32_ARG(0, col); + if (col < 0 || col > objODBCResult->colCount) + return ThrowException(Exception::RangeError(String::New("ODBCResult::GetColumnValue(): The column index is invalid."))); + + uv_work_t* work_req = (uv_work_t *) (calloc(1, sizeof(uv_work_t))); + get_column_value_work_data* data = (get_column_value_work_data *) calloc(1, sizeof(get_column_value_work_data)); + + int maxBytes; + Local cb; + + if (args.Length() == 3) { + REQ_INT32_ARG(1, tmpMaxBytes); maxBytes = tmpMaxBytes; + REQ_FUN_ARG(2, tmpCB); cb = tmpCB; + } else if (args.Length() == 2) { + REQ_FUN_ARG(1, tmpCB); cb = tmpCB; + maxBytes = 0; // Not a partial request + } + + data->col = col; + data->bytesRequested = maxBytes; + data->cb = Persistent::New(cb); + data->objResult = objODBCResult; + data->resultBufferContents = 0; + data->resultBufferOffset = 0; + data->resultBufferLength = 0; + work_req->data = data; + + uv_queue_work( + uv_default_loop(), + work_req, + UV_GetColumnValue, + (uv_after_work_cb)UV_AfterGetColumnValue); + + objODBCResult->Ref(); + + return scope.Close(Undefined()); +} + +void ODBCResult::UV_GetColumnValue(uv_work_t* work_req) { + DEBUG_PRINTF("ODBCResult::UV_GetColumnValue\n"); + + get_column_value_work_data* data = (get_column_value_work_data*)(work_req->data); + + SQLLEN bytesRequested = data->bytesRequested; + if (bytesRequested <= 0 || bytesRequested > data->objResult->bufferLength) + bytesRequested = data->objResult->bufferLength; + + data->cType = ODBC::GetCColumnType(data->objResult->columns[data->col]); + + data->result = ODBC::FetchMoreData( + data->objResult->m_hSTMT, + data->objResult->columns[data->col], + data->cType, + data->bytesAvailable, + data->bytesRead, + data->objResult->buffer, bytesRequested, + data->resultBufferContents, data->resultBufferOffset, data->resultBufferLength); +} + +void ODBCResult::UV_AfterGetColumnValue(uv_work_t* work_req, int status) { + DEBUG_PRINTF("ODBCResult::UV_AfterGetColumnValue\n"); + + HandleScope scope; + + get_column_value_work_data* data = (get_column_value_work_data*)(work_req->data); + + SQLRETURN ret = data->result; + + DEBUG_PRINTF("ODBCResult::UV_AfterGetColumnValue: ret=%i, cType=%i, bytesRequested=%i, bytesRead=%i\n", ret, data->cType, data->bytesRequested, data->bytesRead); + + Handle args[3]; + int argc = 3; + + if (!SQL_SUCCEEDED(ret)) { + args[0] = ODBC::GetSQLError(SQL_HANDLE_STMT, data->objResult->m_hSTMT, "[node-odbc] Error in ODBCResult::GetColumnValue"); + argc = 1; + } else if (data->bytesRead == SQL_NULL_DATA) { + args[0] = Null(); + args[1] = Null(); + args[2] = False(); + } else if (data->cType != SQL_C_BINARY && data->cType != SQL_C_TCHAR) { + args[0] = Null(); + args[1] = ODBC::ConvertColumnValue(data->cType, data->objResult->buffer, data->bytesRead, 0, 0); + args[2] = False(); + } else if (data->bytesRequested == 0 && data->result == SQL_SUCCESS_WITH_INFO) { + // Not partial request, and there is more data to fetch ... queue some more work + + if (!data->resultBufferContents) { + Buffer* b = Buffer::New(data->bytesAvailable + (data->cType == SQL_C_TCHAR ? sizeof(TCHAR) : 0)); // Allow space for ODBC to add an unnecessary null terminator + memcpy(Buffer::Data(b), data->objResult->buffer, data->bytesRead); + data->resultBuffer = b->handle_; + data->resultBufferOffset = data->bytesRead; + data->resultBufferContents = Buffer::Data(b); + data->resultBufferLength = Buffer::Length(b); + DEBUG_PRINTF(" *** Created new SlowBuffer (length: %i), copied %i bytes from internal buffer\n", Buffer::Length(b), data->bytesRead); + } + + uv_queue_work( + uv_default_loop(), + work_req, + UV_GetColumnValue, + (uv_after_work_cb)UV_AfterGetColumnValue); + + return; + } else { + if (!data->resultBufferContents && data->cType == SQL_C_BINARY) { + Buffer* b = Buffer::New((const char*)data->objResult->buffer, (size_t)data->bytesRead); + data->resultBuffer = b->handle_; + data->resultBufferOffset = data->bytesRead; + data->resultBufferContents = Buffer::Data(b); + data->resultBufferLength = Buffer::Length(b); + DEBUG_PRINTF(" *** Created new SlowBuffer (length: %i), copied %i bytes from internal buffer\n", Buffer::Length(b), data->bytesRead); + } + + args[0] = Null(); + args[1] = ODBC::InterpretBuffers(data->cType, data->objResult->buffer, data->bytesRead, data->resultBuffer, data->resultBufferContents, data->resultBufferOffset); + args[2] = ret == SQL_SUCCESS_WITH_INFO ? True() : False(); + } + + TryCatch try_catch; + + DEBUG_PRINTF("Calling callback\n"); + + data->cb->Call(Context::GetCurrent()->Global(), argc, args); + + DEBUG_PRINTF("Called callback, hasCaught=%i\n", try_catch.HasCaught() ? 1 : 0); + + data->cb.Dispose(); + if (data->resultBufferContents) + data->resultBuffer.Dispose(); + + if (try_catch.HasCaught()) { + FatalException(try_catch); + } + + free(data); + free(work_req); + + data->objResult->Unref(); +} + +Handle ODBCResult::GetColumnValueSync(const Arguments& args) { + DEBUG_PRINTF("ODBCResult::GetColumnValueSync\n"); + + HandleScope scope; + + ODBCResult* objODBCResult = ObjectWrap::Unwrap(args.Holder()); + + REQ_INT32_ARG(0, col); + if (col < 0 || col >= objODBCResult->colCount) + return ThrowException(Exception::RangeError( + String::New("ODBCResult::GetColumnValueSync(): The column index requested is invalid.") + )); + + bool partial = true; + OPT_INT_ARG(1, maxBytes, 0); + if (maxBytes == 0) { + partial = false; + maxBytes = objODBCResult->bufferLength; + } + + DEBUG_PRINTF("ODBCResult::GetColumnValueSync: columns=0x%x, colCount=%i, column=%i, maxBytes=%i\n", objODBCResult->columns, objODBCResult->colCount, col, maxBytes); + + return scope.Close( + ODBC::GetColumnValue(objODBCResult->m_hSTMT, objODBCResult->columns[col], objODBCResult->buffer, maxBytes, true) + ); } \ No newline at end of file From 71393a7073780d6fde1828c4259c597179d60127 Mon Sep 17 00:00:00 2001 From: Lee Houghton Date: Thu, 16 Jan 2014 22:01:38 +0000 Subject: [PATCH 15/26] Fix bug in FetchMoreData where the result buffer offset was being incremented twice. --- src/odbc.cpp | 1963 +++++++++++++++++++++++++------------------------- 1 file changed, 982 insertions(+), 981 deletions(-) diff --git a/src/odbc.cpp b/src/odbc.cpp index c4eb078..73fd691 100644 --- a/src/odbc.cpp +++ b/src/odbc.cpp @@ -1,981 +1,982 @@ -/* - Copyright (c) 2013, Dan VerWeire - Copyright (c) 2010, Lee Smith - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ - -#include -#include -#include -#include -#include -#include -#include - -#include "odbc.h" -#include "odbc_connection.h" -#include "odbc_result.h" -#include "odbc_statement.h" - -#ifdef dynodbc -#include "dynodbc.h" -#endif - -using namespace v8; -using namespace node; - -uv_mutex_t ODBC::g_odbcMutex; -uv_async_t ODBC::g_async; - -Persistent ODBC::constructor_template; - -void ODBC::Init(v8::Handle target) { - DEBUG_PRINTF("ODBC::Init\n"); - HandleScope scope; - - Local t = FunctionTemplate::New(New); - - // Constructor Template - constructor_template = Persistent::New(t); - constructor_template->SetClassName(String::NewSymbol("ODBC")); - - // Reserve space for one Handle - Local instance_template = constructor_template->InstanceTemplate(); - instance_template->SetInternalFieldCount(1); - - // Constants - NODE_DEFINE_CONSTANT(constructor_template, SQL_CLOSE); - NODE_DEFINE_CONSTANT(constructor_template, SQL_DROP); - NODE_DEFINE_CONSTANT(constructor_template, SQL_UNBIND); - NODE_DEFINE_CONSTANT(constructor_template, SQL_RESET_PARAMS); - NODE_DEFINE_CONSTANT(constructor_template, SQL_DESTROY); //SQL_DESTROY is non-standard - NODE_DEFINE_CONSTANT(constructor_template, FETCH_ARRAY); - NODE_DEFINE_CONSTANT(constructor_template, FETCH_OBJECT); - NODE_DEFINE_CONSTANT(constructor_template, FETCH_NONE); - - // Prototype Methods - NODE_SET_PROTOTYPE_METHOD(constructor_template, "createConnection", CreateConnection); - NODE_SET_PROTOTYPE_METHOD(constructor_template, "createConnectionSync", CreateConnectionSync); - - // Attach the Database Constructor to the target object - target->Set( v8::String::NewSymbol("ODBC"), - constructor_template->GetFunction()); - - scope.Close(Undefined()); - -#if NODE_VERSION_AT_LEAST(0, 7, 9) - // Initialize uv_async so that we can prevent node from exiting - uv_async_init( uv_default_loop(), - &ODBC::g_async, - ODBC::WatcherCallback); - - // Not sure if the init automatically calls uv_ref() because there is weird - // behavior going on. When ODBC::Init is called which initializes the - // uv_async_t g_async above, there seems to be a ref which will keep it alive - // but we only want this available so that we can uv_ref() later on when - // we have a connection. - // so to work around this, I am possibly mistakenly calling uv_unref() once - // so that there are no references on the loop. - uv_unref((uv_handle_t *)&ODBC::g_async); -#endif - - // Initialize the cross platform mutex provided by libuv - uv_mutex_init(&ODBC::g_odbcMutex); -} - -ODBC::~ODBC() { - DEBUG_PRINTF("ODBC::~ODBC\n"); - this->Free(); -} - -void ODBC::Free() { - DEBUG_PRINTF("ODBC::Free\n"); - if (m_hEnv) { - uv_mutex_lock(&ODBC::g_odbcMutex); - - if (m_hEnv) { - SQLFreeHandle(SQL_HANDLE_ENV, m_hEnv); - m_hEnv = NULL; - } - - uv_mutex_unlock(&ODBC::g_odbcMutex); - } -} - -Handle ODBC::New(const Arguments& args) { - DEBUG_PRINTF("ODBC::New\n"); - HandleScope scope; - ODBC* dbo = new ODBC(); - - dbo->Wrap(args.Holder()); - dbo->m_hEnv = NULL; - - uv_mutex_lock(&ODBC::g_odbcMutex); - - int ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &dbo->m_hEnv); - - uv_mutex_unlock(&ODBC::g_odbcMutex); - - if (!SQL_SUCCEEDED(ret)) { - DEBUG_PRINTF("ODBC::New - ERROR ALLOCATING ENV HANDLE!!\n"); - - Local objError = ODBC::GetSQLError(SQL_HANDLE_ENV, dbo->m_hEnv); - - ThrowException(objError); - } - - SQLSetEnvAttr(dbo->m_hEnv, SQL_ATTR_ODBC_VERSION, (SQLPOINTER) SQL_OV_ODBC3, SQL_IS_UINTEGER); - - return scope.Close(args.Holder()); -} - -void ODBC::WatcherCallback(uv_async_t *w, int revents) { - DEBUG_PRINTF("ODBC::WatcherCallback\n"); - //i don't know if we need to do anything here -} - -/* - * CreateConnection - */ - -Handle ODBC::CreateConnection(const Arguments& args) { - DEBUG_PRINTF("ODBC::CreateConnection\n"); - HandleScope scope; - - REQ_FUN_ARG(0, cb); - - ODBC* dbo = ObjectWrap::Unwrap(args.Holder()); - - //initialize work request - uv_work_t* work_req = (uv_work_t *) (calloc(1, sizeof(uv_work_t))); - - //initialize our data - create_connection_work_data* data = - (create_connection_work_data *) (calloc(1, sizeof(create_connection_work_data))); - - data->cb = Persistent::New(cb); - data->dbo = dbo; - - work_req->data = data; - - uv_queue_work(uv_default_loop(), work_req, UV_CreateConnection, (uv_after_work_cb)UV_AfterCreateConnection); - - dbo->Ref(); - - return scope.Close(Undefined()); -} - -void ODBC::UV_CreateConnection(uv_work_t* req) { - DEBUG_PRINTF("ODBC::UV_CreateConnection\n"); - - //get our work data - create_connection_work_data* data = (create_connection_work_data *)(req->data); - - uv_mutex_lock(&ODBC::g_odbcMutex); - - //allocate a new connection handle - data->result = SQLAllocHandle(SQL_HANDLE_DBC, data->dbo->m_hEnv, &data->hDBC); - - uv_mutex_unlock(&ODBC::g_odbcMutex); -} - -void ODBC::UV_AfterCreateConnection(uv_work_t* req, int status) { - DEBUG_PRINTF("ODBC::UV_AfterCreateConnection\n"); - HandleScope scope; - - create_connection_work_data* data = (create_connection_work_data *)(req->data); - - TryCatch try_catch; - - if (!SQL_SUCCEEDED(data->result)) { - Local args[1]; - - args[0] = ODBC::GetSQLError(SQL_HANDLE_ENV, data->dbo->m_hEnv); - - data->cb->Call(Context::GetCurrent()->Global(), 1, args); - } - else { - Local args[2]; - args[0] = External::New(data->dbo->m_hEnv); - args[1] = External::New(data->hDBC); - - Local js_result(ODBCConnection::constructor_template-> - GetFunction()->NewInstance(2, args)); - - args[0] = Local::New(Null()); - args[1] = Local::New(js_result); - - data->cb->Call(Context::GetCurrent()->Global(), 2, args); - } - - if (try_catch.HasCaught()) { - FatalException(try_catch); - } - - - data->dbo->Unref(); - data->cb.Dispose(); - - free(data); - free(req); - - scope.Close(Undefined()); -} - -/* - * CreateConnectionSync - */ - -Handle ODBC::CreateConnectionSync(const Arguments& args) { - DEBUG_PRINTF("ODBC::CreateConnectionSync\n"); - HandleScope scope; - - ODBC* dbo = ObjectWrap::Unwrap(args.Holder()); - - HDBC hDBC; - - uv_mutex_lock(&ODBC::g_odbcMutex); - - //allocate a new connection handle - SQLRETURN ret = SQLAllocHandle(SQL_HANDLE_DBC, dbo->m_hEnv, &hDBC); - - if (!SQL_SUCCEEDED(ret)) { - //TODO: do something! - } - - uv_mutex_unlock(&ODBC::g_odbcMutex); - - Local params[2]; - params[0] = External::New(dbo->m_hEnv); - params[1] = External::New(hDBC); - - Local js_result(ODBCConnection::constructor_template-> - GetFunction()->NewInstance(2, params)); - - return scope.Close(js_result); -} - -/* - * GetColumns - */ - -Column* ODBC::GetColumns(SQLHSTMT hStmt, short* colCount) { - SQLRETURN ret; - SQLSMALLINT buflen; - - //always reset colCount for the current result set to 0; - *colCount = 0; - - //get the number of columns in the result set - ret = SQLNumResultCols(hStmt, colCount); - - if (!SQL_SUCCEEDED(ret)) { - return new Column[0]; - } - - Column *columns = new Column[*colCount]; - - for (int i = 0; i < *colCount; i++) { - //save the index number of this column - columns[i].index = i + 1; - //TODO:that's a lot of memory for each field name.... - columns[i].name = new unsigned char[MAX_FIELD_SIZE]; - - //set the first byte of name to \0 instead of memsetting the entire buffer - columns[i].name[0] = '\0'; - - //get the column name - ret = SQLColAttribute( hStmt, - columns[i].index, -#ifdef STRICT_COLUMN_NAMES - SQL_DESC_NAME, -#else - SQL_DESC_LABEL, -#endif - columns[i].name, - (SQLSMALLINT) MAX_FIELD_SIZE, - (SQLSMALLINT *) &buflen, - NULL); - - //store the len attribute - columns[i].len = buflen; - - //get the column type and store it directly in column[i].type - ret = SQLColAttribute( hStmt, - columns[i].index, - SQL_DESC_TYPE, - NULL, - 0, - NULL, - &columns[i].type); - } - - return columns; -} - -/* - * FreeColumns - */ - -void ODBC::FreeColumns(Column* columns, short* colCount) { - DEBUG_PRINTF("ODBC::FreeColumns(0x%x, %i)\n", columns, (int)*colCount); - - for(int i = 0; i < *colCount; i++) { - delete [] columns[i].name; - } - - delete [] columns; - - *colCount = 0; -} - -/* - * GetColumnValue - */ - -SQLSMALLINT ODBC::GetCColumnType(const Column& column) { - switch((int) column.type) { - case SQL_INTEGER: case SQL_SMALLINT: case SQL_TINYINT: - return SQL_C_SLONG; - - case SQL_NUMERIC: case SQL_DECIMAL: case SQL_BIGINT: - case SQL_FLOAT: case SQL_REAL: case SQL_DOUBLE: - return SQL_C_DOUBLE; - - case SQL_DATETIME: case SQL_TIMESTAMP: - return SQL_C_TYPE_TIMESTAMP; - - case SQL_BINARY: case SQL_VARBINARY: case SQL_LONGVARBINARY: - return SQL_C_BINARY; - - case SQL_BIT: - return SQL_C_BIT; - - default: - return SQL_C_TCHAR; - } -} - -SQLRETURN ODBC::GetColumnData( SQLHSTMT hStmt, const Column& column, - void* buffer, int bufferLength, SQLSMALLINT& cType, SQLINTEGER& len) { - - cType = GetCColumnType(column); - - switch(cType) { - case SQL_C_SLONG: - bufferLength = sizeof(long); - break; - - case SQL_C_DOUBLE: - bufferLength = sizeof(double); - break; - } - - return SQLGetData( - hStmt, - column.index, - cType, - buffer, - bufferLength, - &len); -} - -Handle ODBC::ConvertColumnValue( SQLSMALLINT cType, - uint16_t* buffer, SQLINTEGER bytesInBuffer, - Buffer* resultBuffer, size_t resultBufferOffset) { - - HandleScope scope; - - switch(cType) { - case SQL_C_SLONG: - assert(bytesInBuffer >= sizeof (long)); - // XXX Integer::New will truncate here if sizeof(long) > 4, it expects 32-bit numbers - return scope.Close(Integer::New(*reinterpret_cast(buffer))); - break; - - case SQL_C_DOUBLE: - assert(bytesInBuffer >= sizeof (double)); - return scope.Close(Number::New(*reinterpret_cast(buffer))); - break; - - case SQL_C_TYPE_TIMESTAMP: { - assert(bytesInBuffer >= sizeof (SQL_TIMESTAMP_STRUCT)); - SQL_TIMESTAMP_STRUCT& odbcTime = *reinterpret_cast(buffer); - struct tm timeInfo = { 0 }; - timeInfo.tm_year = odbcTime.year - 1900; - timeInfo.tm_mon = odbcTime.month - 1; - timeInfo.tm_mday = odbcTime.day; - timeInfo.tm_hour = odbcTime.hour; - timeInfo.tm_min = odbcTime.minute; - timeInfo.tm_sec = odbcTime.second; - - //a negative value means that mktime() should use timezone information - //and system databases to attempt to determine whether DST is in effect - //at the specified time. - timeInfo.tm_isdst = -1; - - #if defined(_WIN32) && defined (TIMEGM) - return scope.Close(Date::New((double(_mkgmtime32(&timeInfo)) * 1000) - + (odbcTime.fraction / 1000000.0))); - #elif defined(WIN32) - return scope.Close(Date::New((double(mktime(&timeInfo)) * 1000) - + (odbcTime.fraction / 1000000.0))); - #elif defined(TIMEGM) - return scope.Close(Date::New((double(timegm(&timeInfo)) * 1000) - + (odbcTime.fraction / 1000000.0))); - #else - return scope.Close(Date::New((double(timelocal(&timeInfo)) * 1000) - + (odbcTime.fraction / 1000000.0))); - #endif - break; - } - - case SQL_C_BIT: - assert(bytesInBuffer >= sizeof(SQLCHAR)); - return scope.Close(Boolean::New(!!*reinterpret_cast(buffer))); - - default: - assert(!"ODBC::ConvertColumnValue: Internal error (unexpected C type)"); - } -} - -SQLRETURN ODBC::FetchMoreData( SQLHSTMT hStmt, const Column& column, SQLSMALLINT cType, - SQLINTEGER& bytesAvailable, SQLINTEGER& bytesRead, - void* internalBuffer, SQLINTEGER internalBufferLength, - void* resultBuffer, size_t& offset, int resultBufferLength ) { - - bytesRead = 0; - SQLRETURN ret; - - if (resultBuffer) { - // Just use the node::Buffer we have to avoid memcpy()ing - SQLINTEGER remainingBuffer = resultBufferLength - offset; - ret = GetColumnData(hStmt, column, (char*)resultBuffer + offset, remainingBuffer, cType, bytesAvailable); - if (!SQL_SUCCEEDED(ret) || bytesAvailable == SQL_NULL_DATA) - return ret; - - bytesRead = min(bytesAvailable, remainingBuffer); - - if (cType == SQL_C_TCHAR && bytesRead == remainingBuffer) - bytesRead -= sizeof(TCHAR); // The last byte or two bytes of the buffer is actually a null terminator (gee, thanks, ODBC). - - offset += bytesRead; - - DEBUG_PRINTF("ODBC::FetchMoreData: SQLGetData(slowBuffer, bufferLength=%i): returned %i with %i bytes available\n", - remainingBuffer, ret, bytesAvailable); - } else { - // We don't know how much data there is to get back yet... - // Use our buffer for now for the first SQLGetData call, which will tell us the full length. - ret = GetColumnData(hStmt, column, internalBuffer, internalBufferLength, cType, bytesAvailable); - if (!SQL_SUCCEEDED(ret) || bytesAvailable == SQL_NULL_DATA) - return ret; - - bytesRead = min(bytesAvailable, internalBufferLength); - - if (cType == SQL_C_TCHAR && bytesRead == internalBufferLength) - bytesRead -= sizeof(TCHAR); // The last byte or two bytes of the buffer is actually a null terminator (gee, thanks, ODBC). - - offset += bytesRead; - - DEBUG_PRINTF("ODBC::FetchMoreData: SQLGetData(internalBuffer, bufferLength=%i): returned %i with %i bytes available\n", - internalBufferLength, ret, bytesAvailable); - - // Now would be a good time to create the result buffer, but we can't call - // Buffer::New here if we are in a worker function - } - - offset += bytesRead; - return ret; -} - -Handle ODBC::GetColumnValue( SQLHSTMT hStmt, Column column, - uint16_t* buffer, int bufferLength, - bool partial, bool fetch) { - HandleScope scope; - - // Reset the buffer - buffer[0] = '\0'; - - if (!fetch) - partial = true; - - DEBUG_PRINTF("ODBC::GetColumnValue: column=%s, type=%i, buffer=%x, bufferLength=%i, partial=%i, fetch=%i\n", - column.name, column.type, buffer, bufferLength, partial?1:0, fetch?1:0); - - SQLSMALLINT cType = GetCColumnType(column); - - // Fixed length column - if (cType != SQL_C_BINARY && cType != SQL_C_TCHAR) { - SQLINTEGER bytesAvailable = 0, bytesRead = 0; - - if (fetch) { - // Use the ODBCResult's buffer - // TODO For fixed-length columns, the driver will just assume the buffer is big enough - // Ensure this won't break anything. - - SQLRETURN ret = GetColumnData(hStmt, column, buffer, bufferLength, cType, bytesAvailable); - if (!SQL_SUCCEEDED(ret)) - return ThrowException(ODBC::GetSQLError(SQL_HANDLE_STMT, hStmt)); - - if (bytesAvailable == SQL_NULL_DATA) - return scope.Close(Null()); - - bytesRead = bytesAvailable > bufferLength ? bufferLength : bytesAvailable; - DEBUG_PRINTF(" *** SQLGetData(internalBuffer, bufferLength=%i) for fixed type: returned %i with %i bytes available\n", bufferLength, ret, bytesAvailable); - - return scope.Close(ConvertColumnValue(cType, buffer, bytesRead, 0, 0)); - } else { - return scope.Close(ConvertColumnValue(cType, buffer, bufferLength, 0, 0)); - } - } - - // Varying length column - fetch piece by piece (in as few pieces as possible - fetch the first piece into the - // ODBCResult's buffer to find out how big the column is, then allocate a node::Buffer to hold the rest of it) - Buffer* slowBuffer = 0; - size_t offset = 0; - - SQLRETURN ret = 0; - SQLINTEGER bytesAvailable = 0, bytesRead = 0; - - do { - if (fetch) { - ret = FetchMoreData( - hStmt, column, cType, - bytesAvailable, bytesRead, - buffer, bufferLength, - slowBuffer ? Buffer::Data(slowBuffer) : 0, offset, slowBuffer ? Buffer::Length(slowBuffer) : 0); - } else { - ret = SQL_SUCCESS; - bytesAvailable = bufferLength; - bytesRead = bufferLength; - } - - // Maybe this should be an error? - if (ret == SQL_NO_DATA) - return scope.Close(Undefined()); - - if (!SQL_SUCCEEDED(ret)) - return ThrowException(ODBC::GetSQLError(SQL_HANDLE_STMT, hStmt, "ODBC::GetColumnValue: Error getting data for result column")); - - if (slowBuffer) - assert(offset <= Buffer::Length(slowBuffer)); - - // The SlowBuffer is needed when - // (a) the result type is Binary - // (b) the result is a string longer than our internal buffer (and we hopefully know the length). - // Move data from internal buffer to node::Buffer now. - if (!slowBuffer && (ret == SQL_SUCCESS_WITH_INFO || cType == SQL_C_BINARY)) { - slowBuffer = Buffer::New(bytesAvailable + (cType == SQL_C_TCHAR ? sizeof(TCHAR) : 0)); // Allow space for ODBC to add an unnecessary null terminator - memcpy(Buffer::Data(slowBuffer), buffer, bytesRead); - DEBUG_PRINTF(" *** Created new SlowBuffer (length: %i), copied %i bytes from internal buffer\n", Buffer::Length(slowBuffer), bytesRead); - } - - // SQLGetData returns SQL_SUCCESS on the last chunk, SQL_SUCCESS_WITH_INFO and SQLSTATE 01004 on the preceding chunks. - } while (!partial && ret == SQL_SUCCESS_WITH_INFO); - - return scope.Close(InterpretBuffers(cType, buffer, bytesRead, slowBuffer->handle_, slowBuffer, offset)); -} - -Handle ODBC::InterpretBuffers( SQLSMALLINT cType, - void* internalBuffer, SQLINTEGER bytesRead, - Persistent resultBufferHandle, - void* resultBuffer, size_t resultBufferOffset) { - HandleScope scope; - - switch (cType) { - case SQL_C_BINARY: { - // XXX Can we trust the global Buffer object? What if the caller has set it to something non-Function? Crash? - Local bufferConstructor = Local::Cast(Context::GetCurrent()->Global()->Get(String::New("Buffer"))); - Handle constructorArgs[1] = { resultBufferHandle }; - return scope.Close(bufferConstructor->NewInstance(1, constructorArgs)); - } - - // TODO when UNICODE is not defined, assumes that the character encoding of the data received is UTF-8 - case SQL_C_TCHAR: default: -#ifdef UNICODE - // slowBuffer may or may not exist, depending on whether or not the data had to be split - if (resultBuffer) - assert (resultBufferOffset % 2 == 0); - if (resultBuffer) - return scope.Close(String::New(reinterpret_cast(resultBuffer), resultBufferOffset / 2)); - else - return scope.Close(String::New(reinterpret_cast(internalBuffer), bytesRead / 2)); -#else - if (resultBuffer) - return scope.Close(String::New(reinterpret_cast(resultBuffer), resultBufferOffset)); - else - return scope.Close(String::New(reinterpret_cast(internalBuffer), bytesRead)); -#endif - } -} - -/* - * GetRecordTuple - */ - -Local ODBC::GetRecordTuple ( SQLHSTMT hStmt, Column* columns, - short* colCount, uint16_t* buffer, - int bufferLength) { - HandleScope scope; - - Local tuple = Object::New(); - - for(int i = 0; i < *colCount; i++) { - Handle value = GetColumnValue( hStmt, columns[i], buffer, bufferLength ); - if (value->IsUndefined()) - return scope.Close(Local()); -#ifdef UNICODE - tuple->Set( String::New((uint16_t *) columns[i].name), value ); -#else - tuple->Set( String::New((const char *) columns[i].name), value ); -#endif - } - - //return tuple; - return scope.Close(tuple); -} - -/* - * GetRecordArray - */ - -Handle ODBC::GetRecordArray ( SQLHSTMT hStmt, Column* columns, - short* colCount, uint16_t* buffer, - int bufferLength) { - HandleScope scope; - - Local array = Array::New(); - - for(int i = 0; i < *colCount; i++) { - Handle value = GetColumnValue( hStmt, columns[i], buffer, bufferLength ); - if (value->IsUndefined()) - return scope.Close(Handle()); - array->Set( Integer::New(i), value ); - } - - //return array; - return scope.Close(array); -} - -/* - * GetParametersFromArray - */ - -Parameter* ODBC::GetParametersFromArray (Local values, int *paramCount) { - DEBUG_PRINTF("ODBC::GetParametersFromArray\n"); - *paramCount = values->Length(); - - Parameter* params = (Parameter *) malloc(*paramCount * sizeof(Parameter)); - - for (int i = 0; i < *paramCount; i++) { - Local value = values->Get(i); - - params[i].size = 0; - params[i].length = SQL_NULL_DATA; - params[i].buffer_length = 0; - params[i].decimals = 0; - - DEBUG_PRINTF("ODBC::GetParametersFromArray - ¶m[%i].length = %X\n", - i, ¶ms[i].length); - - if (value->IsString()) { - Local string = value->ToString(); - int length = string->Length(); - - params[i].c_type = SQL_C_TCHAR; -#ifdef UNICODE - params[i].type = (length >= 8000) ? SQL_WLONGVARCHAR : SQL_WVARCHAR; - params[i].buffer_length = (length * sizeof(uint16_t)) + sizeof(uint16_t); -#else - params[i].type = (length >= 8000) ? SQL_LONGVARCHAR : SQL_VARCHAR; - params[i].buffer_length = string->Utf8Length() + 1; -#endif - params[i].buffer = malloc(params[i].buffer_length); - params[i].size = params[i].buffer_length; - params[i].length = SQL_NTS;//params[i].buffer_length; - -#ifdef UNICODE - string->Write((uint16_t *) params[i].buffer); -#else - string->WriteUtf8((char *) params[i].buffer); -#endif - - DEBUG_PRINTF("ODBC::GetParametersFromArray - IsString(): params[%i] " - "c_type=%i type=%i buffer_length=%i size=%i length=%i " - "value=%s\n", i, params[i].c_type, params[i].type, - params[i].buffer_length, params[i].size, params[i].length, - (char*) params[i].buffer); - } - else if (value->IsNull()) { - params[i].c_type = SQL_C_DEFAULT; - params[i].type = SQL_VARCHAR; - params[i].length = SQL_NULL_DATA; - - DEBUG_PRINTF("ODBC::GetParametersFromArray - IsNull(): params[%i] " - "c_type=%i type=%i buffer_length=%i size=%i length=%i\n", - i, params[i].c_type, params[i].type, - params[i].buffer_length, params[i].size, params[i].length); - } - else if (value->IsInt32()) { - int64_t *number = new int64_t(value->IntegerValue()); - params[i].c_type = SQL_C_SBIGINT; - params[i].type = SQL_BIGINT; - params[i].buffer = number; - params[i].length = 0; - - DEBUG_PRINTF("ODBC::GetParametersFromArray - IsInt32(): params[%i] " - "c_type=%i type=%i buffer_length=%i size=%i length=%i " - "value=%lld\n", i, params[i].c_type, params[i].type, - params[i].buffer_length, params[i].size, params[i].length, - *number); - } - else if (value->IsNumber()) { - double *number = new double(value->NumberValue()); - - params[i].c_type = SQL_C_DOUBLE; - params[i].type = SQL_DECIMAL; - params[i].buffer = number; - params[i].buffer_length = sizeof(double); - params[i].length = params[i].buffer_length; - params[i].decimals = 7; - params[i].size = sizeof(double); - - DEBUG_PRINTF("ODBC::GetParametersFromArray - IsNumber(): params[%i] " - "c_type=%i type=%i buffer_length=%i size=%i length=%i " - "value=%f\n", - i, params[i].c_type, params[i].type, - params[i].buffer_length, params[i].size, params[i].length, - *number); - } - else if (value->IsBoolean()) { - bool *boolean = new bool(value->BooleanValue()); - params[i].c_type = SQL_C_BIT; - params[i].type = SQL_BIT; - params[i].buffer = boolean; - params[i].length = 0; - - DEBUG_PRINTF("ODBC::GetParametersFromArray - IsBoolean(): params[%i] " - "c_type=%i type=%i buffer_length=%i size=%i length=%i\n", - i, params[i].c_type, params[i].type, - params[i].buffer_length, params[i].size, params[i].length); - } - } - - return params; -} - -/* - * CallbackSQLError - */ - -Handle ODBC::CallbackSQLError (SQLSMALLINT handleType, - SQLHANDLE handle, - Persistent cb) { - HandleScope scope; - - return scope.Close(CallbackSQLError( - handleType, - handle, - (char *) "[node-odbc] SQL_ERROR", - cb)); -} - -Handle ODBC::CallbackSQLError (SQLSMALLINT handleType, - SQLHANDLE handle, - char* message, - Persistent cb) { - HandleScope scope; - - Local objError = ODBC::GetSQLError( - handleType, - handle, - message - ); - - Local args[1]; - args[0] = objError; - cb->Call(Context::GetCurrent()->Global(), 1, args); - - return scope.Close(Undefined()); -} - -/* - * GetSQLError - */ - -Local ODBC::GetSQLError (SQLSMALLINT handleType, SQLHANDLE handle) { - HandleScope scope; - - return scope.Close(GetSQLError( - handleType, - handle, - (char *) "[node-odbc] SQL_ERROR")); -} - -Local ODBC::GetSQLError (SQLSMALLINT handleType, SQLHANDLE handle, char* message) { - HandleScope scope; - - DEBUG_PRINTF("ODBC::GetSQLError : handleType=%i, handle=%p\n", handleType, handle); - - Local objError = Object::New(); - - SQLSMALLINT i = 0; - SQLINTEGER native; - - SQLSMALLINT len; - SQLINTEGER numfields; - SQLRETURN ret; - char errorSQLState[14]; - char errorMessage[512]; - - ret = SQLGetDiagField( - handleType, - handle, - 0, - SQL_DIAG_NUMBER, - &numfields, - SQL_IS_INTEGER, - &len); - - // Windows seems to define SQLINTEGER as long int, unixodbc as just int... %i should cover both - DEBUG_PRINTF("ODBC::GetSQLError : called SQLGetDiagField; ret=%i, numfields=%i\n", ret, numfields); - - for (i = 0; i < numfields; i++){ - DEBUG_PRINTF("ODBC::GetSQLError : calling SQLGetDiagRec; i=%i, numfields=%i\n", i, numfields); - - ret = SQLGetDiagRec( - handleType, - handle, - i + 1, - (SQLTCHAR *) errorSQLState, - &native, - (SQLTCHAR *) errorMessage, - sizeof(errorMessage), - &len); - - DEBUG_PRINTF("ODBC::GetSQLError : after SQLGetDiagRec; i=%i\n", i); - - if (SQL_SUCCEEDED(ret)) { - DEBUG_TPRINTF(SQL_T("ODBC::GetSQLError : errorMessage=%s, errorSQLState=%s\n"), errorMessage, errorSQLState); - - objError->Set(String::New("error"), String::New(message)); -#ifdef UNICODE - objError->SetPrototype(Exception::Error(String::New((uint16_t *) errorMessage))); - objError->Set(String::New("message"), String::New((uint16_t *) errorMessage)); - objError->Set(String::New("state"), String::New((uint16_t *) errorSQLState)); -#else - objError->SetPrototype(Exception::Error(String::New(errorMessage))); - objError->Set(String::New("message"), String::New(errorMessage)); - objError->Set(String::New("state"), String::New(errorSQLState)); -#endif - } else if (ret == SQL_NO_DATA) { - break; - } - } - - return scope.Close(objError); -} - -/* - * GetAllRecordsSync - */ - -Local ODBC::GetAllRecordsSync (HENV hENV, - HDBC hDBC, - HSTMT hSTMT, - uint16_t* buffer, - int bufferLength) { - DEBUG_PRINTF("ODBC::GetAllRecordsSync\n"); - - HandleScope scope; - - Local objError = Object::New(); - - int count = 0; - int errorCount = 0; - short colCount = 0; - - Column* columns = GetColumns(hSTMT, &colCount); - - Local rows = Array::New(); - - //loop through all records - while (true) { - SQLRETURN ret = SQLFetch(hSTMT); - - //check to see if there was an error - if (ret == SQL_ERROR) { - //TODO: what do we do when we actually get an error here... - //should we throw?? - - errorCount++; - - objError = ODBC::GetSQLError( - SQL_HANDLE_STMT, - hSTMT, - (char *) "[node-odbc] Error in ODBC::GetAllRecordsSync" - ); - - break; - } - - //check to see if we are at the end of the recordset - if (ret == SQL_NO_DATA) { - ODBC::FreeColumns(columns, &colCount); - - break; - } - - rows->Set( - Integer::New(count), - ODBC::GetRecordTuple( - hSTMT, - columns, - &colCount, - buffer, - bufferLength) - ); - - count++; - } - //TODO: what do we do about errors!?! - //we throw them - return scope.Close(rows); -} - -#ifdef dynodbc -Handle ODBC::LoadODBCLibrary(const Arguments& args) { - HandleScope scope; - - REQ_STR_ARG(0, js_library); - - bool result = DynLoadODBC(*js_library); - - return scope.Close((result) ? True() : False()); -} -#endif - -extern "C" void init (v8::Handle target) { -#ifdef dynodbc - target->Set(String::NewSymbol("loadODBCLibrary"), - FunctionTemplate::New(ODBC::LoadODBCLibrary)->GetFunction()); -#endif - - ODBC::Init(target); - ODBCResult::Init(target); - ODBCConnection::Init(target); - ODBCStatement::Init(target); -} - -NODE_MODULE(odbc_bindings, init) +/* + Copyright (c) 2013, Dan VerWeire + Copyright (c) 2010, Lee Smith + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +#include +#include +#include +#include +#include +#include +#include + +#include "odbc.h" +#include "odbc_connection.h" +#include "odbc_result.h" +#include "odbc_statement.h" + +#ifdef dynodbc +#include "dynodbc.h" +#endif + +using namespace v8; +using namespace node; + +uv_mutex_t ODBC::g_odbcMutex; +uv_async_t ODBC::g_async; + +Persistent ODBC::constructor_template; + +void ODBC::Init(v8::Handle target) { + DEBUG_PRINTF("ODBC::Init\n"); + HandleScope scope; + + Local t = FunctionTemplate::New(New); + + // Constructor Template + constructor_template = Persistent::New(t); + constructor_template->SetClassName(String::NewSymbol("ODBC")); + + // Reserve space for one Handle + Local instance_template = constructor_template->InstanceTemplate(); + instance_template->SetInternalFieldCount(1); + + // Constants + NODE_DEFINE_CONSTANT(constructor_template, SQL_CLOSE); + NODE_DEFINE_CONSTANT(constructor_template, SQL_DROP); + NODE_DEFINE_CONSTANT(constructor_template, SQL_UNBIND); + NODE_DEFINE_CONSTANT(constructor_template, SQL_RESET_PARAMS); + NODE_DEFINE_CONSTANT(constructor_template, SQL_DESTROY); //SQL_DESTROY is non-standard + NODE_DEFINE_CONSTANT(constructor_template, FETCH_ARRAY); + NODE_DEFINE_CONSTANT(constructor_template, FETCH_OBJECT); + NODE_DEFINE_CONSTANT(constructor_template, FETCH_NONE); + + // Prototype Methods + NODE_SET_PROTOTYPE_METHOD(constructor_template, "createConnection", CreateConnection); + NODE_SET_PROTOTYPE_METHOD(constructor_template, "createConnectionSync", CreateConnectionSync); + + // Attach the Database Constructor to the target object + target->Set( v8::String::NewSymbol("ODBC"), + constructor_template->GetFunction()); + + scope.Close(Undefined()); + +#if NODE_VERSION_AT_LEAST(0, 7, 9) + // Initialize uv_async so that we can prevent node from exiting + uv_async_init( uv_default_loop(), + &ODBC::g_async, + ODBC::WatcherCallback); + + // Not sure if the init automatically calls uv_ref() because there is weird + // behavior going on. When ODBC::Init is called which initializes the + // uv_async_t g_async above, there seems to be a ref which will keep it alive + // but we only want this available so that we can uv_ref() later on when + // we have a connection. + // so to work around this, I am possibly mistakenly calling uv_unref() once + // so that there are no references on the loop. + uv_unref((uv_handle_t *)&ODBC::g_async); +#endif + + // Initialize the cross platform mutex provided by libuv + uv_mutex_init(&ODBC::g_odbcMutex); +} + +ODBC::~ODBC() { + DEBUG_PRINTF("ODBC::~ODBC\n"); + this->Free(); +} + +void ODBC::Free() { + DEBUG_PRINTF("ODBC::Free\n"); + if (m_hEnv) { + uv_mutex_lock(&ODBC::g_odbcMutex); + + if (m_hEnv) { + SQLFreeHandle(SQL_HANDLE_ENV, m_hEnv); + m_hEnv = NULL; + } + + uv_mutex_unlock(&ODBC::g_odbcMutex); + } +} + +Handle ODBC::New(const Arguments& args) { + DEBUG_PRINTF("ODBC::New\n"); + HandleScope scope; + ODBC* dbo = new ODBC(); + + dbo->Wrap(args.Holder()); + dbo->m_hEnv = NULL; + + uv_mutex_lock(&ODBC::g_odbcMutex); + + int ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &dbo->m_hEnv); + + uv_mutex_unlock(&ODBC::g_odbcMutex); + + if (!SQL_SUCCEEDED(ret)) { + DEBUG_PRINTF("ODBC::New - ERROR ALLOCATING ENV HANDLE!!\n"); + + Local objError = ODBC::GetSQLError(SQL_HANDLE_ENV, dbo->m_hEnv); + + ThrowException(objError); + } + + SQLSetEnvAttr(dbo->m_hEnv, SQL_ATTR_ODBC_VERSION, (SQLPOINTER) SQL_OV_ODBC3, SQL_IS_UINTEGER); + + return scope.Close(args.Holder()); +} + +void ODBC::WatcherCallback(uv_async_t *w, int revents) { + DEBUG_PRINTF("ODBC::WatcherCallback\n"); + //i don't know if we need to do anything here +} + +/* + * CreateConnection + */ + +Handle ODBC::CreateConnection(const Arguments& args) { + DEBUG_PRINTF("ODBC::CreateConnection\n"); + HandleScope scope; + + REQ_FUN_ARG(0, cb); + + ODBC* dbo = ObjectWrap::Unwrap(args.Holder()); + + //initialize work request + uv_work_t* work_req = (uv_work_t *) (calloc(1, sizeof(uv_work_t))); + + //initialize our data + create_connection_work_data* data = + (create_connection_work_data *) (calloc(1, sizeof(create_connection_work_data))); + + data->cb = Persistent::New(cb); + data->dbo = dbo; + + work_req->data = data; + + uv_queue_work(uv_default_loop(), work_req, UV_CreateConnection, (uv_after_work_cb)UV_AfterCreateConnection); + + dbo->Ref(); + + return scope.Close(Undefined()); +} + +void ODBC::UV_CreateConnection(uv_work_t* req) { + DEBUG_PRINTF("ODBC::UV_CreateConnection\n"); + + //get our work data + create_connection_work_data* data = (create_connection_work_data *)(req->data); + + uv_mutex_lock(&ODBC::g_odbcMutex); + + //allocate a new connection handle + data->result = SQLAllocHandle(SQL_HANDLE_DBC, data->dbo->m_hEnv, &data->hDBC); + + uv_mutex_unlock(&ODBC::g_odbcMutex); +} + +void ODBC::UV_AfterCreateConnection(uv_work_t* req, int status) { + DEBUG_PRINTF("ODBC::UV_AfterCreateConnection\n"); + HandleScope scope; + + create_connection_work_data* data = (create_connection_work_data *)(req->data); + + TryCatch try_catch; + + if (!SQL_SUCCEEDED(data->result)) { + Local args[1]; + + args[0] = ODBC::GetSQLError(SQL_HANDLE_ENV, data->dbo->m_hEnv); + + data->cb->Call(Context::GetCurrent()->Global(), 1, args); + } + else { + Local args[2]; + args[0] = External::New(data->dbo->m_hEnv); + args[1] = External::New(data->hDBC); + + Local js_result(ODBCConnection::constructor_template-> + GetFunction()->NewInstance(2, args)); + + args[0] = Local::New(Null()); + args[1] = Local::New(js_result); + + data->cb->Call(Context::GetCurrent()->Global(), 2, args); + } + + if (try_catch.HasCaught()) { + FatalException(try_catch); + } + + + data->dbo->Unref(); + data->cb.Dispose(); + + free(data); + free(req); + + scope.Close(Undefined()); +} + +/* + * CreateConnectionSync + */ + +Handle ODBC::CreateConnectionSync(const Arguments& args) { + DEBUG_PRINTF("ODBC::CreateConnectionSync\n"); + HandleScope scope; + + ODBC* dbo = ObjectWrap::Unwrap(args.Holder()); + + HDBC hDBC; + + uv_mutex_lock(&ODBC::g_odbcMutex); + + //allocate a new connection handle + SQLRETURN ret = SQLAllocHandle(SQL_HANDLE_DBC, dbo->m_hEnv, &hDBC); + + if (!SQL_SUCCEEDED(ret)) { + //TODO: do something! + } + + uv_mutex_unlock(&ODBC::g_odbcMutex); + + Local params[2]; + params[0] = External::New(dbo->m_hEnv); + params[1] = External::New(hDBC); + + Local js_result(ODBCConnection::constructor_template-> + GetFunction()->NewInstance(2, params)); + + return scope.Close(js_result); +} + +/* + * GetColumns + */ + +Column* ODBC::GetColumns(SQLHSTMT hStmt, short* colCount) { + SQLRETURN ret; + SQLSMALLINT buflen; + + //always reset colCount for the current result set to 0; + *colCount = 0; + + //get the number of columns in the result set + ret = SQLNumResultCols(hStmt, colCount); + + if (!SQL_SUCCEEDED(ret)) { + return new Column[0]; + } + + Column *columns = new Column[*colCount]; + + for (int i = 0; i < *colCount; i++) { + //save the index number of this column + columns[i].index = i + 1; + //TODO:that's a lot of memory for each field name.... + columns[i].name = new unsigned char[MAX_FIELD_SIZE]; + + //set the first byte of name to \0 instead of memsetting the entire buffer + columns[i].name[0] = '\0'; + + //get the column name + ret = SQLColAttribute( hStmt, + columns[i].index, +#ifdef STRICT_COLUMN_NAMES + SQL_DESC_NAME, +#else + SQL_DESC_LABEL, +#endif + columns[i].name, + (SQLSMALLINT) MAX_FIELD_SIZE, + (SQLSMALLINT *) &buflen, + NULL); + + //store the len attribute + columns[i].len = buflen; + + //get the column type and store it directly in column[i].type + ret = SQLColAttribute( hStmt, + columns[i].index, + SQL_DESC_TYPE, + NULL, + 0, + NULL, + &columns[i].type); + } + + return columns; +} + +/* + * FreeColumns + */ + +void ODBC::FreeColumns(Column* columns, short* colCount) { + DEBUG_PRINTF("ODBC::FreeColumns(0x%x, %i)\n", columns, (int)*colCount); + + for(int i = 0; i < *colCount; i++) { + delete [] columns[i].name; + } + + delete [] columns; + + *colCount = 0; +} + +/* + * GetColumnValue + */ + +SQLSMALLINT ODBC::GetCColumnType(const Column& column) { + switch((int) column.type) { + case SQL_INTEGER: case SQL_SMALLINT: case SQL_TINYINT: + return SQL_C_SLONG; + + case SQL_NUMERIC: case SQL_DECIMAL: case SQL_BIGINT: + case SQL_FLOAT: case SQL_REAL: case SQL_DOUBLE: + return SQL_C_DOUBLE; + + case SQL_DATETIME: case SQL_TIMESTAMP: + return SQL_C_TYPE_TIMESTAMP; + + case SQL_BINARY: case SQL_VARBINARY: case SQL_LONGVARBINARY: + return SQL_C_BINARY; + + case SQL_BIT: + return SQL_C_BIT; + + default: + return SQL_C_TCHAR; + } +} + +SQLRETURN ODBC::GetColumnData( SQLHSTMT hStmt, const Column& column, + void* buffer, int bufferLength, SQLSMALLINT& cType, SQLINTEGER& len) { + + cType = GetCColumnType(column); + + switch(cType) { + case SQL_C_SLONG: + bufferLength = sizeof(long); + break; + + case SQL_C_DOUBLE: + bufferLength = sizeof(double); + break; + } + + return SQLGetData( + hStmt, + column.index, + cType, + buffer, + bufferLength, + &len); +} + +Handle ODBC::ConvertColumnValue( SQLSMALLINT cType, + uint16_t* buffer, SQLINTEGER bytesInBuffer, + Buffer* resultBuffer, size_t resultBufferOffset) { + + HandleScope scope; + + switch(cType) { + case SQL_C_SLONG: + assert(bytesInBuffer >= sizeof (long)); + // XXX Integer::New will truncate here if sizeof(long) > 4, it expects 32-bit numbers + return scope.Close(Integer::New(*reinterpret_cast(buffer))); + break; + + case SQL_C_DOUBLE: + assert(bytesInBuffer >= sizeof (double)); + return scope.Close(Number::New(*reinterpret_cast(buffer))); + break; + + case SQL_C_TYPE_TIMESTAMP: { + assert(bytesInBuffer >= sizeof (SQL_TIMESTAMP_STRUCT)); + SQL_TIMESTAMP_STRUCT& odbcTime = *reinterpret_cast(buffer); + struct tm timeInfo = { 0 }; + timeInfo.tm_year = odbcTime.year - 1900; + timeInfo.tm_mon = odbcTime.month - 1; + timeInfo.tm_mday = odbcTime.day; + timeInfo.tm_hour = odbcTime.hour; + timeInfo.tm_min = odbcTime.minute; + timeInfo.tm_sec = odbcTime.second; + + //a negative value means that mktime() should use timezone information + //and system databases to attempt to determine whether DST is in effect + //at the specified time. + timeInfo.tm_isdst = -1; + + #if defined(_WIN32) && defined (TIMEGM) + return scope.Close(Date::New((double(_mkgmtime32(&timeInfo)) * 1000) + + (odbcTime.fraction / 1000000.0))); + #elif defined(WIN32) + return scope.Close(Date::New((double(mktime(&timeInfo)) * 1000) + + (odbcTime.fraction / 1000000.0))); + #elif defined(TIMEGM) + return scope.Close(Date::New((double(timegm(&timeInfo)) * 1000) + + (odbcTime.fraction / 1000000.0))); + #else + return scope.Close(Date::New((double(timelocal(&timeInfo)) * 1000) + + (odbcTime.fraction / 1000000.0))); + #endif + break; + } + + case SQL_C_BIT: + assert(bytesInBuffer >= sizeof(SQLCHAR)); + return scope.Close(Boolean::New(!!*reinterpret_cast(buffer))); + + default: + assert(!"ODBC::ConvertColumnValue: Internal error (unexpected C type)"); + } +} + +SQLRETURN ODBC::FetchMoreData( SQLHSTMT hStmt, const Column& column, SQLSMALLINT cType, + SQLINTEGER& bytesAvailable, SQLINTEGER& bytesRead, + void* internalBuffer, SQLINTEGER internalBufferLength, + void* resultBuffer, size_t& offset, int resultBufferLength ) { + + bytesRead = 0; + SQLRETURN ret; + + if (resultBuffer) { + // Just use the node::Buffer we have to avoid memcpy()ing + SQLINTEGER remainingBuffer = resultBufferLength - offset; + ret = GetColumnData(hStmt, column, (char*)resultBuffer + offset, remainingBuffer, cType, bytesAvailable); + if (!SQL_SUCCEEDED(ret) || bytesAvailable == SQL_NULL_DATA) + return ret; + + bytesRead = min(bytesAvailable, remainingBuffer); + + if (cType == SQL_C_TCHAR && bytesRead == remainingBuffer) + bytesRead -= sizeof(TCHAR); // The last byte or two bytes of the buffer is actually a null terminator (gee, thanks, ODBC). + + offset += bytesRead; + + DEBUG_PRINTF("ODBC::FetchMoreData: SQLGetData(slowBuffer, bufferLength=%i): returned %i with %i bytes available\n", + remainingBuffer, ret, bytesAvailable); + } else { + // We don't know how much data there is to get back yet... + // Use our buffer for now for the first SQLGetData call, which will tell us the full length. + ret = GetColumnData(hStmt, column, internalBuffer, internalBufferLength, cType, bytesAvailable); + if (!SQL_SUCCEEDED(ret) || bytesAvailable == SQL_NULL_DATA) + return ret; + + bytesRead = min(bytesAvailable, internalBufferLength); + + if (cType == SQL_C_TCHAR && bytesRead == internalBufferLength) + bytesRead -= sizeof(TCHAR); // The last byte or two bytes of the buffer is actually a null terminator (gee, thanks, ODBC). + + offset += bytesRead; + + DEBUG_PRINTF("ODBC::FetchMoreData: SQLGetData(internalBuffer, bufferLength=%i): returned %i with %i bytes available\n", + internalBufferLength, ret, bytesAvailable); + + // Now would be a good time to create the result buffer, but we can't call + // Buffer::New here if we are on a worker thread + } + + return ret; +} + +Handle ODBC::GetColumnValue( SQLHSTMT hStmt, Column column, + uint16_t* buffer, int bufferLength, + bool partial, bool fetch) { + HandleScope scope; + + // Reset the buffer + buffer[0] = '\0'; + + if (!fetch) + partial = true; + + DEBUG_PRINTF("ODBC::GetColumnValue: column=%s, type=%i, buffer=%x, bufferLength=%i, partial=%i, fetch=%i\n", + column.name, column.type, buffer, bufferLength, partial?1:0, fetch?1:0); + + SQLSMALLINT cType = GetCColumnType(column); + + // Fixed length column + if (cType != SQL_C_BINARY && cType != SQL_C_TCHAR) { + SQLINTEGER bytesAvailable = 0, bytesRead = 0; + + if (fetch) { + // Use the ODBCResult's buffer + // TODO For fixed-length columns, the driver will just assume the buffer is big enough + // Ensure this won't break anything. + + SQLRETURN ret = GetColumnData(hStmt, column, buffer, bufferLength, cType, bytesAvailable); + if (!SQL_SUCCEEDED(ret)) + return ThrowException(ODBC::GetSQLError(SQL_HANDLE_STMT, hStmt)); + + if (bytesAvailable == SQL_NULL_DATA) + return scope.Close(Null()); + + bytesRead = bytesAvailable > bufferLength ? bufferLength : bytesAvailable; + DEBUG_PRINTF(" *** SQLGetData(internalBuffer, bufferLength=%i) for fixed type: returned %i with %i bytes available\n", bufferLength, ret, bytesAvailable); + + return scope.Close(ConvertColumnValue(cType, buffer, bytesRead, 0, 0)); + } else { + return scope.Close(ConvertColumnValue(cType, buffer, bufferLength, 0, 0)); + } + } + + // Varying length column - fetch piece by piece (in as few pieces as possible - fetch the first piece into the + // ODBCResult's buffer to find out how big the column is, then allocate a node::Buffer to hold the rest of it) + Buffer* slowBuffer = 0; + size_t offset = 0; + + SQLRETURN ret = 0; + SQLINTEGER bytesAvailable = 0, bytesRead = 0; + + do { + if (fetch) { + ret = FetchMoreData( + hStmt, column, cType, + bytesAvailable, bytesRead, + buffer, bufferLength, + slowBuffer ? Buffer::Data(slowBuffer) : 0, offset, slowBuffer ? Buffer::Length(slowBuffer) : 0); + } else { + ret = SQL_SUCCESS; + bytesAvailable = bufferLength; + bytesRead = bufferLength; + } + + // Maybe this should be an error? + if (ret == SQL_NO_DATA) + return scope.Close(Undefined()); + + if (!SQL_SUCCEEDED(ret)) + return ThrowException(ODBC::GetSQLError(SQL_HANDLE_STMT, hStmt, "ODBC::GetColumnValue: Error getting data for result column")); + + if (slowBuffer) + assert(offset <= Buffer::Length(slowBuffer)); + + // The SlowBuffer is needed when + // (a) the result type is Binary + // (b) the result is a string longer than our internal buffer (and we hopefully know the length). + // Move data from internal buffer to node::Buffer now. + if (!slowBuffer && (ret == SQL_SUCCESS_WITH_INFO || cType == SQL_C_BINARY)) { + slowBuffer = Buffer::New(bytesAvailable + (cType == SQL_C_TCHAR ? sizeof(TCHAR) : 0)); // Allow space for ODBC to add an unnecessary null terminator + memcpy(Buffer::Data(slowBuffer), buffer, bytesRead); + DEBUG_PRINTF(" *** Created new SlowBuffer (length: %i), copied %i bytes from internal buffer\n", Buffer::Length(slowBuffer), bytesRead); + } + + // SQLGetData returns SQL_SUCCESS on the last chunk, SQL_SUCCESS_WITH_INFO and SQLSTATE 01004 on the preceding chunks. + } while (!partial && ret == SQL_SUCCESS_WITH_INFO); + + return scope.Close(InterpretBuffers(cType, buffer, bytesRead, slowBuffer->handle_, slowBuffer, offset)); +} + +Handle ODBC::InterpretBuffers( SQLSMALLINT cType, + void* internalBuffer, SQLINTEGER bytesRead, + Persistent resultBufferHandle, + void* resultBuffer, size_t resultBufferOffset) { + DEBUG_PRINTF("ODBC::InterpretBuffers(%i, %p, %i, _, %p, %u)\n", cType, internalBuffer, bytesRead, resultBuffer, resultBufferOffset); + + HandleScope scope; + + switch (cType) { + case SQL_C_BINARY: { + // XXX Can we trust the global Buffer object? What if the caller has set it to something non-Function? Crash? + Local bufferConstructor = Local::Cast(Context::GetCurrent()->Global()->Get(String::New("Buffer"))); + Handle constructorArgs[1] = { resultBufferHandle }; + return scope.Close(bufferConstructor->NewInstance(1, constructorArgs)); + } + + // TODO when UNICODE is not defined, assumes that the character encoding of the data received is UTF-8 + case SQL_C_TCHAR: default: +#ifdef UNICODE + // slowBuffer may or may not exist, depending on whether or not the data had to be split + if (resultBuffer) + assert (resultBufferOffset % 2 == 0); + if (resultBuffer) + return scope.Close(String::New(reinterpret_cast(resultBuffer), resultBufferOffset / 2)); + else + return scope.Close(String::New(reinterpret_cast(internalBuffer), bytesRead / 2)); +#else + if (resultBuffer) + return scope.Close(String::New(reinterpret_cast(resultBuffer), resultBufferOffset)); + else + return scope.Close(String::New(reinterpret_cast(internalBuffer), bytesRead)); +#endif + } +} + +/* + * GetRecordTuple + */ + +Local ODBC::GetRecordTuple ( SQLHSTMT hStmt, Column* columns, + short* colCount, uint16_t* buffer, + int bufferLength) { + HandleScope scope; + + Local tuple = Object::New(); + + for(int i = 0; i < *colCount; i++) { + Handle value = GetColumnValue( hStmt, columns[i], buffer, bufferLength ); + if (value->IsUndefined()) + return scope.Close(Local()); +#ifdef UNICODE + tuple->Set( String::New((uint16_t *) columns[i].name), value ); +#else + tuple->Set( String::New((const char *) columns[i].name), value ); +#endif + } + + //return tuple; + return scope.Close(tuple); +} + +/* + * GetRecordArray + */ + +Handle ODBC::GetRecordArray ( SQLHSTMT hStmt, Column* columns, + short* colCount, uint16_t* buffer, + int bufferLength) { + HandleScope scope; + + Local array = Array::New(); + + for(int i = 0; i < *colCount; i++) { + Handle value = GetColumnValue( hStmt, columns[i], buffer, bufferLength ); + if (value->IsUndefined()) + return scope.Close(Handle()); + array->Set( Integer::New(i), value ); + } + + //return array; + return scope.Close(array); +} + +/* + * GetParametersFromArray + */ + +Parameter* ODBC::GetParametersFromArray (Local values, int *paramCount) { + DEBUG_PRINTF("ODBC::GetParametersFromArray\n"); + *paramCount = values->Length(); + + Parameter* params = (Parameter *) malloc(*paramCount * sizeof(Parameter)); + + for (int i = 0; i < *paramCount; i++) { + Local value = values->Get(i); + + params[i].size = 0; + params[i].length = SQL_NULL_DATA; + params[i].buffer_length = 0; + params[i].decimals = 0; + + DEBUG_PRINTF("ODBC::GetParametersFromArray - ¶m[%i].length = %X\n", + i, ¶ms[i].length); + + if (value->IsString()) { + Local string = value->ToString(); + int length = string->Length(); + + params[i].c_type = SQL_C_TCHAR; +#ifdef UNICODE + params[i].type = (length >= 8000) ? SQL_WLONGVARCHAR : SQL_WVARCHAR; + params[i].buffer_length = (length * sizeof(uint16_t)) + sizeof(uint16_t); +#else + params[i].type = (length >= 8000) ? SQL_LONGVARCHAR : SQL_VARCHAR; + params[i].buffer_length = string->Utf8Length() + 1; +#endif + params[i].buffer = malloc(params[i].buffer_length); + params[i].size = params[i].buffer_length; + params[i].length = SQL_NTS;//params[i].buffer_length; + +#ifdef UNICODE + string->Write((uint16_t *) params[i].buffer); +#else + string->WriteUtf8((char *) params[i].buffer); +#endif + + DEBUG_PRINTF("ODBC::GetParametersFromArray - IsString(): params[%i] " + "c_type=%i type=%i buffer_length=%i size=%i length=%i " + "value=%s\n", i, params[i].c_type, params[i].type, + params[i].buffer_length, params[i].size, params[i].length, + (char*) params[i].buffer); + } + else if (value->IsNull()) { + params[i].c_type = SQL_C_DEFAULT; + params[i].type = SQL_VARCHAR; + params[i].length = SQL_NULL_DATA; + + DEBUG_PRINTF("ODBC::GetParametersFromArray - IsNull(): params[%i] " + "c_type=%i type=%i buffer_length=%i size=%i length=%i\n", + i, params[i].c_type, params[i].type, + params[i].buffer_length, params[i].size, params[i].length); + } + else if (value->IsInt32()) { + int64_t *number = new int64_t(value->IntegerValue()); + params[i].c_type = SQL_C_SBIGINT; + params[i].type = SQL_BIGINT; + params[i].buffer = number; + params[i].length = 0; + + DEBUG_PRINTF("ODBC::GetParametersFromArray - IsInt32(): params[%i] " + "c_type=%i type=%i buffer_length=%i size=%i length=%i " + "value=%lld\n", i, params[i].c_type, params[i].type, + params[i].buffer_length, params[i].size, params[i].length, + *number); + } + else if (value->IsNumber()) { + double *number = new double(value->NumberValue()); + + params[i].c_type = SQL_C_DOUBLE; + params[i].type = SQL_DECIMAL; + params[i].buffer = number; + params[i].buffer_length = sizeof(double); + params[i].length = params[i].buffer_length; + params[i].decimals = 7; + params[i].size = sizeof(double); + + DEBUG_PRINTF("ODBC::GetParametersFromArray - IsNumber(): params[%i] " + "c_type=%i type=%i buffer_length=%i size=%i length=%i " + "value=%f\n", + i, params[i].c_type, params[i].type, + params[i].buffer_length, params[i].size, params[i].length, + *number); + } + else if (value->IsBoolean()) { + bool *boolean = new bool(value->BooleanValue()); + params[i].c_type = SQL_C_BIT; + params[i].type = SQL_BIT; + params[i].buffer = boolean; + params[i].length = 0; + + DEBUG_PRINTF("ODBC::GetParametersFromArray - IsBoolean(): params[%i] " + "c_type=%i type=%i buffer_length=%i size=%i length=%i\n", + i, params[i].c_type, params[i].type, + params[i].buffer_length, params[i].size, params[i].length); + } + } + + return params; +} + +/* + * CallbackSQLError + */ + +Handle ODBC::CallbackSQLError (SQLSMALLINT handleType, + SQLHANDLE handle, + Persistent cb) { + HandleScope scope; + + return scope.Close(CallbackSQLError( + handleType, + handle, + (char *) "[node-odbc] SQL_ERROR", + cb)); +} + +Handle ODBC::CallbackSQLError (SQLSMALLINT handleType, + SQLHANDLE handle, + char* message, + Persistent cb) { + HandleScope scope; + + Local objError = ODBC::GetSQLError( + handleType, + handle, + message + ); + + Local args[1]; + args[0] = objError; + cb->Call(Context::GetCurrent()->Global(), 1, args); + + return scope.Close(Undefined()); +} + +/* + * GetSQLError + */ + +Local ODBC::GetSQLError (SQLSMALLINT handleType, SQLHANDLE handle) { + HandleScope scope; + + return scope.Close(GetSQLError( + handleType, + handle, + (char *) "[node-odbc] SQL_ERROR")); +} + +Local ODBC::GetSQLError (SQLSMALLINT handleType, SQLHANDLE handle, char* message) { + HandleScope scope; + + DEBUG_PRINTF("ODBC::GetSQLError : handleType=%i, handle=%p\n", handleType, handle); + + Local objError = Object::New(); + + SQLSMALLINT i = 0; + SQLINTEGER native; + + SQLSMALLINT len; + SQLINTEGER numfields; + SQLRETURN ret; + char errorSQLState[14]; + char errorMessage[512]; + + ret = SQLGetDiagField( + handleType, + handle, + 0, + SQL_DIAG_NUMBER, + &numfields, + SQL_IS_INTEGER, + &len); + + // Windows seems to define SQLINTEGER as long int, unixodbc as just int... %i should cover both + DEBUG_PRINTF("ODBC::GetSQLError : called SQLGetDiagField; ret=%i, numfields=%i\n", ret, numfields); + + for (i = 0; i < numfields; i++){ + DEBUG_PRINTF("ODBC::GetSQLError : calling SQLGetDiagRec; i=%i, numfields=%i\n", i, numfields); + + ret = SQLGetDiagRec( + handleType, + handle, + i + 1, + (SQLTCHAR *) errorSQLState, + &native, + (SQLTCHAR *) errorMessage, + sizeof(errorMessage), + &len); + + DEBUG_PRINTF("ODBC::GetSQLError : after SQLGetDiagRec; i=%i\n", i); + + if (SQL_SUCCEEDED(ret)) { + DEBUG_TPRINTF(SQL_T("ODBC::GetSQLError : errorMessage=%s, errorSQLState=%s\n"), errorMessage, errorSQLState); + + objError->Set(String::New("error"), String::New(message)); +#ifdef UNICODE + objError->SetPrototype(Exception::Error(String::New((uint16_t *) errorMessage))); + objError->Set(String::New("message"), String::New((uint16_t *) errorMessage)); + objError->Set(String::New("state"), String::New((uint16_t *) errorSQLState)); +#else + objError->SetPrototype(Exception::Error(String::New(errorMessage))); + objError->Set(String::New("message"), String::New(errorMessage)); + objError->Set(String::New("state"), String::New(errorSQLState)); +#endif + } else if (ret == SQL_NO_DATA) { + break; + } + } + + return scope.Close(objError); +} + +/* + * GetAllRecordsSync + */ + +Local ODBC::GetAllRecordsSync (HENV hENV, + HDBC hDBC, + HSTMT hSTMT, + uint16_t* buffer, + int bufferLength) { + DEBUG_PRINTF("ODBC::GetAllRecordsSync\n"); + + HandleScope scope; + + Local objError = Object::New(); + + int count = 0; + int errorCount = 0; + short colCount = 0; + + Column* columns = GetColumns(hSTMT, &colCount); + + Local rows = Array::New(); + + //loop through all records + while (true) { + SQLRETURN ret = SQLFetch(hSTMT); + + //check to see if there was an error + if (ret == SQL_ERROR) { + //TODO: what do we do when we actually get an error here... + //should we throw?? + + errorCount++; + + objError = ODBC::GetSQLError( + SQL_HANDLE_STMT, + hSTMT, + (char *) "[node-odbc] Error in ODBC::GetAllRecordsSync" + ); + + break; + } + + //check to see if we are at the end of the recordset + if (ret == SQL_NO_DATA) { + ODBC::FreeColumns(columns, &colCount); + + break; + } + + rows->Set( + Integer::New(count), + ODBC::GetRecordTuple( + hSTMT, + columns, + &colCount, + buffer, + bufferLength) + ); + + count++; + } + //TODO: what do we do about errors!?! + //we throw them + return scope.Close(rows); +} + +#ifdef dynodbc +Handle ODBC::LoadODBCLibrary(const Arguments& args) { + HandleScope scope; + + REQ_STR_ARG(0, js_library); + + bool result = DynLoadODBC(*js_library); + + return scope.Close((result) ? True() : False()); +} +#endif + +extern "C" void init (v8::Handle target) { +#ifdef dynodbc + target->Set(String::NewSymbol("loadODBCLibrary"), + FunctionTemplate::New(ODBC::LoadODBCLibrary)->GetFunction()); +#endif + + ODBC::Init(target); + ODBCResult::Init(target); + ODBCConnection::Init(target); + ODBCStatement::Init(target); +} + +NODE_MODULE(odbc_bindings, init) From d9612e77b8e6a44aae404bdfaaf1b3e4d59e6b58 Mon Sep 17 00:00:00 2001 From: Lee Houghton Date: Thu, 16 Jan 2014 22:19:44 +0000 Subject: [PATCH 16/26] Add test for GetColumnValue (async) TODO: Fix behaviour of maxBytes for strings (should really be maxChars), maxBytes is pretty useless for strings when you don't know if the underlying representation is unicode or utf-8. --- test/test-get-column-value.js | 108 ++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 test/test-get-column-value.js diff --git a/test/test-get-column-value.js b/test/test-get-column-value.js new file mode 100644 index 0000000..93e7712 --- /dev/null +++ b/test/test-get-column-value.js @@ -0,0 +1,108 @@ +var common = require("./common") + , odbc = require("../") + , db = new odbc.Database() + , assert = require("assert"); + +db.open(common.connectionString, function (err) { + db.fetchMode = odbc.FETCH_NONE; + + function rand(length) { + var buf = new Buffer(length); + for (var i = 0; i < buf.length; i++) + buf[i] = 1 + Math.floor(Math.random() * 255); + return buf.toString("binary"); + } + + var x = rand(694853), y = rand(524288), z = rand(524289), w = null + + console.log("x.length:", x.length, x.substr(0,80)); + console.log("y.length:", y.length); + console.log("z.length:", z.length); + + db.queryResult("select ?, ?, ?, null", [x, y, z, w], function (err, result, more) { + assert.ifError(err); + + result.fetch(function(err, data) { + assert.ifError(err); + assert.equal(data, null); + + result.getColumnValue(0, function (err, xval, more) { + assert.ifError(err); + assert(!more, "more should be falsy when no maxBytes is passed"); + + console.log("xval.length:", xval.length); + + result.getColumnValue(1, 2048*2+2, function (err, yval1, more) { + assert.ifError(err); + assert(more, "more should be truthy when maxBytes is less than the length of the string"); + + console.log(y.length, yval1.length) + assert.equal(yval1.length, 2048); + + result.getColumnValue(1, function (err, yval2, more) { + assert.ifError(err); + assert(!more, "more should be falsy on the second call for y"); + + result.getColumnValue(2, function (err, zval, more) { + assert.ifError(err); + assert(!more, "more should be falsy for z"); + + result.getColumnValue(3, function (err, wval, more) { + assert.ifError(err); + assert(!more, "more should be falsy for w"); + + assert.equal(xval, x); + assert.equal(yval1, y.substr(0, 2048)); + assert.equal(yval2, y.substr(2048, y.length - 2048)); + assert.equal(zval, z); + assert.equal(wval, null); + + console.log("Tests OK"); + db.close(assert.ifError); + }); + }); + }); + }); + }); + }); + }); +}); + +// var sql = "declare @data varbinary(max) = 0x00112233445566778899aabbccddeeff, @n int = 4;\ +// while @n > 0 select @data += @data, @n -= 1;\ +// select datalength(@data) [length], @data [data], substring(@data,3,254) [data2] from (select 1 [x] union all select 2) rows;" + +// db.fetchMode = odbc.FETCH_NONE +// db.queryResult(sql, function (err, result, more) { +// console.log('db.queryResult callback:', err, result, more) + +// var row = 1; + +// function loop() { +// result.fetch(function (err, _, more) { +// console.log('result.fetch callback (row', row++ + '):', err, _, more) + +// assert.ifError(err) + +// if (more) { +// var bytes = result.getColumnValueSync(0), received = 0; +// console.log('Bytes to fetch:', bytes) + +// console.log('Column 1 value: ', result.getColumnValueSync(1)) + +// result.getColumnValue(2, 0, function (err, res) { +// console.log(err, res) +// console.log(res.constructor.name); +// console.log(res.length); +// loop() +// }) +// } else { +// console.log('Test finished') +// db.close(function() {}) +// } +// }) +// } + +// loop() +// }) +//}) \ No newline at end of file From 2b0b9bb38e623a02adb2bd66204677724ea75e30 Mon Sep 17 00:00:00 2001 From: Lee Houghton Date: Thu, 16 Jan 2014 22:21:02 +0000 Subject: [PATCH 17/26] Fix assertion on NULL in GetColumnValue Caused by FetchMoreData not passing on SQL_NULL_DATA --- src/odbc.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/odbc.cpp b/src/odbc.cpp index 73fd691..ea96e44 100644 --- a/src/odbc.cpp +++ b/src/odbc.cpp @@ -478,6 +478,10 @@ SQLRETURN ODBC::FetchMoreData( SQLHSTMT hStmt, const Column& column, SQLSMALLINT // We don't know how much data there is to get back yet... // Use our buffer for now for the first SQLGetData call, which will tell us the full length. ret = GetColumnData(hStmt, column, internalBuffer, internalBufferLength, cType, bytesAvailable); + + if (bytesAvailable == SQL_NULL_DATA) + bytesRead = bytesAvailable; + if (!SQL_SUCCEEDED(ret) || bytesAvailable == SQL_NULL_DATA) return ret; From 5ae51d6b61eb814b71e7e5f8f67a8ff19be73813 Mon Sep 17 00:00:00 2001 From: Lee Houghton Date: Thu, 24 Jul 2014 12:20:28 +0100 Subject: [PATCH 18/26] Resolve C2308 (concatenating mismatched strings) --- src/odbc.cpp | 16 ++++++++-------- src/odbc_connection.cpp | 2 +- src/odbc_statement.cpp | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/odbc.cpp b/src/odbc.cpp index 5d42d89..abc8741 100644 --- a/src/odbc.cpp +++ b/src/odbc.cpp @@ -721,8 +721,8 @@ Parameter* ODBC::GetParametersFromArray (Local values, int *paramCount) { #endif DEBUG_PRINTF("ODBC::GetParametersFromArray - IsString(): params[%i] " - "c_type=%i type=%i buffer_length=%i size=%i length=%i " - "value=%s\n", i, params[i].c_type, params[i].type, + L"c_type=%i type=%i buffer_length=%i size=%i length=%i " + L"value=%s\n", i, params[i].c_type, params[i].type, params[i].buffer_length, params[i].size, params[i].length, (char*) params[i].buffer); } @@ -732,7 +732,7 @@ Parameter* ODBC::GetParametersFromArray (Local values, int *paramCount) { params[i].length = SQL_NULL_DATA; DEBUG_PRINTF("ODBC::GetParametersFromArray - IsNull(): params[%i] " - "c_type=%i type=%i buffer_length=%i size=%i length=%i\n", + L"c_type=%i type=%i buffer_length=%i size=%i length=%i\n", i, params[i].c_type, params[i].type, params[i].buffer_length, params[i].size, params[i].length); } @@ -744,8 +744,8 @@ Parameter* ODBC::GetParametersFromArray (Local values, int *paramCount) { params[i].length = 0; DEBUG_PRINTF("ODBC::GetParametersFromArray - IsInt32(): params[%i] " - "c_type=%i type=%i buffer_length=%i size=%i length=%i " - "value=%lld\n", i, params[i].c_type, params[i].type, + L"c_type=%i type=%i buffer_length=%i size=%i length=%i " + L"value=%lld\n", i, params[i].c_type, params[i].type, params[i].buffer_length, params[i].size, params[i].length, *number); } @@ -761,8 +761,8 @@ Parameter* ODBC::GetParametersFromArray (Local values, int *paramCount) { params[i].size = sizeof(double); DEBUG_PRINTF("ODBC::GetParametersFromArray - IsNumber(): params[%i] " - "c_type=%i type=%i buffer_length=%i size=%i length=%i " - "value=%f\n", + L"c_type=%i type=%i buffer_length=%i size=%i length=%i " + L"value=%f\n", i, params[i].c_type, params[i].type, params[i].buffer_length, params[i].size, params[i].length, *number); @@ -775,7 +775,7 @@ Parameter* ODBC::GetParametersFromArray (Local values, int *paramCount) { params[i].length = 0; DEBUG_PRINTF("ODBC::GetParametersFromArray - IsBoolean(): params[%i] " - "c_type=%i type=%i buffer_length=%i size=%i length=%i\n", + L"c_type=%i type=%i buffer_length=%i size=%i length=%i\n", i, params[i].c_type, params[i].type, params[i].buffer_length, params[i].size, params[i].length); } diff --git a/src/odbc_connection.cpp b/src/odbc_connection.cpp index 514db3b..ae8b0dc 100644 --- a/src/odbc_connection.cpp +++ b/src/odbc_connection.cpp @@ -1141,7 +1141,7 @@ Handle ODBCConnection::QuerySync(const Arguments& args) { DEBUG_PRINTF( "ODBCConnection::UV_Query - param[%i]: c_type=%i type=%i " - "buffer_length=%i size=%i length=%i &length=%X\n", i, prm.c_type, prm.type, + L"buffer_length=%i size=%i length=%i &length=%X\n", i, prm.c_type, prm.type, prm.buffer_length, prm.size, prm.length, ¶ms[i].length); ret = SQLBindParameter( diff --git a/src/odbc_statement.cpp b/src/odbc_statement.cpp index bcd4e7e..0e02605 100644 --- a/src/odbc_statement.cpp +++ b/src/odbc_statement.cpp @@ -800,7 +800,7 @@ Handle ODBCStatement::BindSync(const Arguments& args) { DEBUG_PRINTF( "ODBCStatement::BindSync - param[%i]: c_type=%i type=%i " - "buffer_length=%i size=%i length=%i &length=%X decimals=%i value=%x\n", + L"buffer_length=%i size=%i length=%i &length=%X decimals=%i value=%x\n", i, prm.c_type, prm.type, prm.buffer_length, prm.size, prm.length, &stmt->params[i].length, prm.decimals, prm.buffer ); @@ -934,7 +934,7 @@ void ODBCStatement::UV_Bind(uv_work_t* req) { DEBUG_PRINTF( "ODBCStatement::UV_Bind - param[%i]: c_type=%i type=%i " - "buffer_length=%i size=%i length=%i &length=%X decimals=%i value=%s\n", + L"buffer_length=%i size=%i length=%i &length=%X decimals=%i value=%s\n", i, prm.c_type, prm.type, prm.buffer_length, prm.size, prm.length, &data->stmt->params[i].length, prm.decimals, prm.buffer ); From 7bb4657751ae7dca553366da4cf4c08656552fe8 Mon Sep 17 00:00:00 2001 From: Lee Houghton Date: Thu, 24 Jul 2014 14:12:14 +0100 Subject: [PATCH 19/26] * Fix null buffer reference in GetColumnValue() when fetching short string * Handle SQL_DATE and SQL_TIMESTAMP separately --- src/odbc.cpp | 132 ++++++++++++++++++++++++++++++--------------------- src/odbc.h | 2 +- 2 files changed, 79 insertions(+), 55 deletions(-) diff --git a/src/odbc.cpp b/src/odbc.cpp index abc8741..aa6b306 100644 --- a/src/odbc.cpp +++ b/src/odbc.cpp @@ -345,17 +345,22 @@ void ODBC::FreeColumns(Column* columns, short* colCount) { */ SQLSMALLINT ODBC::GetCColumnType(const Column& column) { - switch((int) column.type) { + switch ((int)column.type) { case SQL_INTEGER: case SQL_SMALLINT: case SQL_TINYINT: return SQL_C_SLONG; - + case SQL_NUMERIC: case SQL_DECIMAL: case SQL_BIGINT: case SQL_FLOAT: case SQL_REAL: case SQL_DOUBLE: return SQL_C_DOUBLE; - case SQL_DATETIME: case SQL_TIMESTAMP: + // Date and time + case SQL_TIMESTAMP: case SQL_TYPE_TIMESTAMP: return SQL_C_TYPE_TIMESTAMP; + // Just date + case SQL_DATE: case SQL_TYPE_DATE: + return SQL_C_TYPE_DATE; + case SQL_BINARY: case SQL_VARBINARY: case SQL_LONGVARBINARY: return SQL_C_BINARY; @@ -391,63 +396,82 @@ SQLRETURN ODBC::GetColumnData( SQLHSTMT hStmt, const Column& column, &len); } -Handle ODBC::ConvertColumnValue( SQLSMALLINT cType, +Handle ODBC::ConvertColumnValue( SQLSMALLINT cType, uint16_t* buffer, SQLINTEGER bytesInBuffer, Buffer* resultBuffer, size_t resultBufferOffset) { HandleScope scope; - switch(cType) { - case SQL_C_SLONG: - assert(bytesInBuffer >= sizeof (long)); - // XXX Integer::New will truncate here if sizeof(long) > 4, it expects 32-bit numbers - return scope.Close(Integer::New(*reinterpret_cast(buffer))); - break; - - case SQL_C_DOUBLE: - assert(bytesInBuffer >= sizeof (double)); - return scope.Close(Number::New(*reinterpret_cast(buffer))); - break; - - case SQL_C_TYPE_TIMESTAMP: { - assert(bytesInBuffer >= sizeof (SQL_TIMESTAMP_STRUCT)); - SQL_TIMESTAMP_STRUCT& odbcTime = *reinterpret_cast(buffer); - struct tm timeInfo = { 0 }; - timeInfo.tm_year = odbcTime.year - 1900; - timeInfo.tm_mon = odbcTime.month - 1; - timeInfo.tm_mday = odbcTime.day; - timeInfo.tm_hour = odbcTime.hour; - timeInfo.tm_min = odbcTime.minute; - timeInfo.tm_sec = odbcTime.second; - - //a negative value means that mktime() should use timezone information - //and system databases to attempt to determine whether DST is in effect - //at the specified time. - timeInfo.tm_isdst = -1; - - #if defined(_WIN32) && defined (TIMEGM) - return scope.Close(Date::New((double(_mkgmtime32(&timeInfo)) * 1000) - + (odbcTime.fraction / 1000000.0))); - #elif defined(WIN32) - return scope.Close(Date::New((double(mktime(&timeInfo)) * 1000) - + (odbcTime.fraction / 1000000.0))); - #elif defined(TIMEGM) - return scope.Close(Date::New((double(timegm(&timeInfo)) * 1000) - + (odbcTime.fraction / 1000000.0))); - #else - return scope.Close(Date::New((double(timelocal(&timeInfo)) * 1000) - + (odbcTime.fraction / 1000000.0))); - #endif - break; - } + switch (cType) { + case SQL_C_SLONG: + assert(bytesInBuffer >= sizeof(long)); + // XXX Integer::New will truncate here if sizeof(long) > 4, it expects 32-bit numbers + return scope.Close(Integer::New(*reinterpret_cast(buffer))); + break; + + case SQL_C_DOUBLE: + assert(bytesInBuffer >= sizeof(double)); + return scope.Close(Number::New(*reinterpret_cast(buffer))); + break; - case SQL_C_BIT: - assert(bytesInBuffer >= sizeof(SQLCHAR)); - return scope.Close(Boolean::New(!!*reinterpret_cast(buffer))); + case SQL_C_TYPE_TIMESTAMP: case SQL_C_TYPE_DATE: { + struct tm timeInfo = { 0 }; + SQLUINTEGER fraction = 0; + + switch (cType) { + case SQL_C_TYPE_TIMESTAMP: { + assert(bytesInBuffer >= sizeof(SQL_TIMESTAMP_STRUCT)); + SQL_TIMESTAMP_STRUCT& odbcTime = *reinterpret_cast(buffer); + + timeInfo.tm_year = odbcTime.year - 1900; + timeInfo.tm_mon = odbcTime.month - 1; + timeInfo.tm_mday = odbcTime.day; + timeInfo.tm_hour = odbcTime.hour; + timeInfo.tm_min = odbcTime.minute; + timeInfo.tm_sec = odbcTime.second; + fraction = odbcTime.fraction; + break; + } + + case SQL_C_TYPE_DATE: { + assert(bytesInBuffer >= sizeof(SQL_DATE_STRUCT)); + SQL_DATE_STRUCT& odbcDate = *reinterpret_cast(buffer); + + timeInfo.tm_year = odbcDate.year - 1900; + timeInfo.tm_mon = odbcDate.month - 1; + timeInfo.tm_mday = odbcDate.day; + break; + } + } - default: - assert(!"ODBC::ConvertColumnValue: Internal error (unexpected C type)"); + //a negative value means that mktime() should use timezone information + //and system databases to attempt to determine whether DST is in effect + //at the specified time. + timeInfo.tm_isdst = -1; + +#if defined(_WIN32) && defined (TIMEGM) + return scope.Close(Date::New((double(_mkgmtime32(&timeInfo)) * 1000) + + (fraction / 1000000.0))); +#elif defined(WIN32) + return scope.Close(Date::New((double(mktime(&timeInfo)) * 1000) + + (fraction / 1000000.0))); +#elif defined(TIMEGM) + return scope.Close(Date::New((double(timegm(&timeInfo)) * 1000) + + (fraction / 1000000.0))); +#else + return scope.Close(Date::New((double(timelocal(&timeInfo)) * 1000) + + (fraction / 1000000.0))); +#endif + break; } + + case SQL_C_BIT: + assert(bytesInBuffer >= sizeof(SQLCHAR)); + return scope.Close(Boolean::New(!!*reinterpret_cast(buffer))); + + default: + assert(!"ODBC::ConvertColumnValue: Internal error (unexpected C type)"); + } } SQLRETURN ODBC::FetchMoreData( SQLHSTMT hStmt, const Column& column, SQLSMALLINT cType, @@ -587,12 +611,12 @@ Handle ODBC::GetColumnValue( SQLHSTMT hStmt, Column column, // SQLGetData returns SQL_SUCCESS on the last chunk, SQL_SUCCESS_WITH_INFO and SQLSTATE 01004 on the preceding chunks. } while (!partial && ret == SQL_SUCCESS_WITH_INFO); - return scope.Close(InterpretBuffers(cType, buffer, bytesRead, slowBuffer->handle_, slowBuffer, offset)); + return scope.Close(InterpretBuffers(cType, buffer, bytesRead, slowBuffer ? slowBuffer->handle_ : Handle(), slowBuffer, offset)); } Handle ODBC::InterpretBuffers( SQLSMALLINT cType, void* internalBuffer, SQLINTEGER bytesRead, - Persistent resultBufferHandle, + Handle resultBufferHandle, void* resultBuffer, size_t resultBufferOffset) { DEBUG_PRINTF("ODBC::InterpretBuffers(%i, %p, %i, _, %p, %u)\n", cType, internalBuffer, bytesRead, resultBuffer, resultBufferOffset); diff --git a/src/odbc.h b/src/odbc.h index 314dbec..b348f0a 100644 --- a/src/odbc.h +++ b/src/odbc.h @@ -84,7 +84,7 @@ class ODBC : public node::ObjectWrap { static SQLRETURN GetColumnData(SQLHSTMT hStmt, const Column& column, void* buffer, int bufferLength, SQLSMALLINT& cType, SQLINTEGER& len); static Handle ConvertColumnValue(SQLSMALLINT cType, uint16_t* buffer, SQLINTEGER bytesInBuffer, node::Buffer* resultBuffer, size_t resultBufferOffset); static SQLRETURN FetchMoreData(SQLHSTMT hStmt, const Column& column, SQLSMALLINT cType, SQLINTEGER& bytesAvailable, SQLINTEGER& bytesRead, void* internalBuffer, SQLINTEGER internalBufferLength, void* resultBuffer, size_t& offset, int resultBufferLength); - static Handle InterpretBuffers(SQLSMALLINT cType, void* internalBuffer, SQLINTEGER bytesRead, Persistent resultBufferHandle, void* resultBuffer, size_t resultBufferOffset); + static Handle InterpretBuffers(SQLSMALLINT cType, void* internalBuffer, SQLINTEGER bytesRead, Handle resultBufferHandle, void* resultBuffer, size_t resultBufferOffset); static Handle GetColumnValue(SQLHSTMT hStmt, Column column, uint16_t* buffer, int bufferLength, bool partial = false, bool fetch = true); static Local GetRecordTuple (SQLHSTMT hStmt, Column* columns, short* colCount, uint16_t* buffer, int bufferLength); static Handle GetRecordArray (SQLHSTMT hStmt, Column* columns, short* colCount, uint16_t* buffer, int bufferLength); From 0104fee81681c80a243a3f091e172dd877f6df42 Mon Sep 17 00:00:00 2001 From: Lee Houghton Date: Thu, 24 Jul 2014 14:23:29 +0100 Subject: [PATCH 20/26] Remove DEBUG flag. --- binding.gyp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binding.gyp b/binding.gyp index acce441..3c80a81 100644 --- a/binding.gyp +++ b/binding.gyp @@ -10,7 +10,7 @@ 'src/dynodbc.cpp' ], 'defines' : [ - 'UNICODE', 'DEBUG' + 'UNICODE' ], 'conditions' : [ [ 'OS == "linux"', { From 45c22a89df7f061e2b6709eefc657dc770c715e0 Mon Sep 17 00:00:00 2001 From: Lee Houghton Date: Wed, 30 Jul 2014 13:26:56 +0100 Subject: [PATCH 21/26] Fix SQL server smalldatetime values being treated as dates without a timestamp because they cmoe through as SQL_DATE (just go back to treating everything as a date and time, since JavaScript doesn't care anyway whether there was originally a time component or not) (cherry picked from commit fb76807eaea21f170c7ca5c85a60370aa80c44e3) --- src/odbc.cpp | 42 ++++++++++++------------------------------ 1 file changed, 12 insertions(+), 30 deletions(-) diff --git a/src/odbc.cpp b/src/odbc.cpp index aa6b306..04f9957 100644 --- a/src/odbc.cpp +++ b/src/odbc.cpp @@ -355,11 +355,8 @@ SQLSMALLINT ODBC::GetCColumnType(const Column& column) { // Date and time case SQL_TIMESTAMP: case SQL_TYPE_TIMESTAMP: - return SQL_C_TYPE_TIMESTAMP; - - // Just date case SQL_DATE: case SQL_TYPE_DATE: - return SQL_C_TYPE_DATE; + return SQL_C_TYPE_TIMESTAMP; case SQL_BINARY: case SQL_VARBINARY: case SQL_LONGVARBINARY: return SQL_C_BINARY; @@ -414,35 +411,20 @@ Handle ODBC::ConvertColumnValue( SQLSMALLINT cType, return scope.Close(Number::New(*reinterpret_cast(buffer))); break; - case SQL_C_TYPE_TIMESTAMP: case SQL_C_TYPE_DATE: { + case SQL_C_TYPE_TIMESTAMP: { struct tm timeInfo = { 0 }; SQLUINTEGER fraction = 0; - switch (cType) { - case SQL_C_TYPE_TIMESTAMP: { - assert(bytesInBuffer >= sizeof(SQL_TIMESTAMP_STRUCT)); - SQL_TIMESTAMP_STRUCT& odbcTime = *reinterpret_cast(buffer); - - timeInfo.tm_year = odbcTime.year - 1900; - timeInfo.tm_mon = odbcTime.month - 1; - timeInfo.tm_mday = odbcTime.day; - timeInfo.tm_hour = odbcTime.hour; - timeInfo.tm_min = odbcTime.minute; - timeInfo.tm_sec = odbcTime.second; - fraction = odbcTime.fraction; - break; - } - - case SQL_C_TYPE_DATE: { - assert(bytesInBuffer >= sizeof(SQL_DATE_STRUCT)); - SQL_DATE_STRUCT& odbcDate = *reinterpret_cast(buffer); - - timeInfo.tm_year = odbcDate.year - 1900; - timeInfo.tm_mon = odbcDate.month - 1; - timeInfo.tm_mday = odbcDate.day; - break; - } - } + assert(bytesInBuffer >= sizeof(SQL_TIMESTAMP_STRUCT)); + SQL_TIMESTAMP_STRUCT& odbcTime = *reinterpret_cast(buffer); + + timeInfo.tm_year = odbcTime.year - 1900; + timeInfo.tm_mon = odbcTime.month - 1; + timeInfo.tm_mday = odbcTime.day; + timeInfo.tm_hour = odbcTime.hour; + timeInfo.tm_min = odbcTime.minute; + timeInfo.tm_sec = odbcTime.second; + fraction = odbcTime.fraction; //a negative value means that mktime() should use timezone information //and system databases to attempt to determine whether DST is in effect From fa8c544d0edf6b633a2e87c358db2447e380bdc4 Mon Sep 17 00:00:00 2001 From: Lee Houghton Date: Thu, 4 Sep 2014 14:41:17 +0100 Subject: [PATCH 22/26] Changed to use SQLLEN where appropriate (possible omissions, needs testing) (cherry picked from commit bfa1e2271fa6409ff6dfe3753bc47656b4cff37d) --- src/odbc.cpp | 18 +++++++++--------- src/odbc.h | 8 ++++---- src/odbc_result.h | 6 +++--- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/odbc.cpp b/src/odbc.cpp index 04f9957..46e261b 100644 --- a/src/odbc.cpp +++ b/src/odbc.cpp @@ -370,7 +370,7 @@ SQLSMALLINT ODBC::GetCColumnType(const Column& column) { } SQLRETURN ODBC::GetColumnData( SQLHSTMT hStmt, const Column& column, - void* buffer, int bufferLength, SQLSMALLINT& cType, SQLINTEGER& len) { + void* buffer, SQLLEN bufferLength, SQLSMALLINT& cType, SQLLEN& len) { cType = GetCColumnType(column); @@ -457,16 +457,16 @@ Handle ODBC::ConvertColumnValue( SQLSMALLINT cType, } SQLRETURN ODBC::FetchMoreData( SQLHSTMT hStmt, const Column& column, SQLSMALLINT cType, - SQLINTEGER& bytesAvailable, SQLINTEGER& bytesRead, - void* internalBuffer, SQLINTEGER internalBufferLength, - void* resultBuffer, size_t& offset, int resultBufferLength ) { + SQLLEN& bytesAvailable, SQLLEN& bytesRead, + void* internalBuffer, SQLLEN internalBufferLength, + void* resultBuffer, size_t& offset, SQLLEN resultBufferLength ) { bytesRead = 0; SQLRETURN ret; if (resultBuffer) { // Just use the node::Buffer we have to avoid memcpy()ing - SQLINTEGER remainingBuffer = resultBufferLength - offset; + SQLLEN remainingBuffer = resultBufferLength - offset; ret = GetColumnData(hStmt, column, (char*)resultBuffer + offset, remainingBuffer, cType, bytesAvailable); if (!SQL_SUCCEEDED(ret) || bytesAvailable == SQL_NULL_DATA) return ret; @@ -509,7 +509,7 @@ SQLRETURN ODBC::FetchMoreData( SQLHSTMT hStmt, const Column& column, SQLSMALLINT } Handle ODBC::GetColumnValue( SQLHSTMT hStmt, Column column, - uint16_t* buffer, int bufferLength, + uint16_t* buffer, SQLLEN bufferLength, bool partial, bool fetch) { HandleScope scope; @@ -526,7 +526,7 @@ Handle ODBC::GetColumnValue( SQLHSTMT hStmt, Column column, // Fixed length column if (cType != SQL_C_BINARY && cType != SQL_C_TCHAR) { - SQLINTEGER bytesAvailable = 0, bytesRead = 0; + SQLLEN bytesAvailable = 0, bytesRead = 0; if (fetch) { // Use the ODBCResult's buffer @@ -555,7 +555,7 @@ Handle ODBC::GetColumnValue( SQLHSTMT hStmt, Column column, size_t offset = 0; SQLRETURN ret = 0; - SQLINTEGER bytesAvailable = 0, bytesRead = 0; + SQLLEN bytesAvailable = 0, bytesRead = 0; do { if (fetch) { @@ -597,7 +597,7 @@ Handle ODBC::GetColumnValue( SQLHSTMT hStmt, Column column, } Handle ODBC::InterpretBuffers( SQLSMALLINT cType, - void* internalBuffer, SQLINTEGER bytesRead, + void* internalBuffer, SQLLEN bytesRead, Handle resultBufferHandle, void* resultBuffer, size_t resultBufferOffset) { DEBUG_PRINTF("ODBC::InterpretBuffers(%i, %p, %i, _, %p, %u)\n", cType, internalBuffer, bytesRead, resultBuffer, resultBufferOffset); diff --git a/src/odbc.h b/src/odbc.h index b348f0a..d7fe168 100644 --- a/src/odbc.h +++ b/src/odbc.h @@ -81,11 +81,11 @@ class ODBC : public node::ObjectWrap { static Column* GetColumns(SQLHSTMT hStmt, short* colCount); static void FreeColumns(Column* columns, short* colCount); static SQLRETURN GetCColumnType(const Column& column); - static SQLRETURN GetColumnData(SQLHSTMT hStmt, const Column& column, void* buffer, int bufferLength, SQLSMALLINT& cType, SQLINTEGER& len); + static SQLRETURN GetColumnData(SQLHSTMT hStmt, const Column& column, void* buffer, SQLLEN bufferLength, SQLSMALLINT& cType, SQLLEN& len); static Handle ConvertColumnValue(SQLSMALLINT cType, uint16_t* buffer, SQLINTEGER bytesInBuffer, node::Buffer* resultBuffer, size_t resultBufferOffset); - static SQLRETURN FetchMoreData(SQLHSTMT hStmt, const Column& column, SQLSMALLINT cType, SQLINTEGER& bytesAvailable, SQLINTEGER& bytesRead, void* internalBuffer, SQLINTEGER internalBufferLength, void* resultBuffer, size_t& offset, int resultBufferLength); - static Handle InterpretBuffers(SQLSMALLINT cType, void* internalBuffer, SQLINTEGER bytesRead, Handle resultBufferHandle, void* resultBuffer, size_t resultBufferOffset); - static Handle GetColumnValue(SQLHSTMT hStmt, Column column, uint16_t* buffer, int bufferLength, bool partial = false, bool fetch = true); + static SQLRETURN FetchMoreData(SQLHSTMT hStmt, const Column& column, SQLSMALLINT cType, SQLLEN& bytesAvailable, SQLLEN& bytesRead, void* internalBuffer, SQLLEN internalBufferLength, void* resultBuffer, size_t& offset, SQLLEN resultBufferLength); + static Handle InterpretBuffers(SQLSMALLINT cType, void* internalBuffer, SQLLEN bytesRead, Handle resultBufferHandle, void* resultBuffer, size_t resultBufferOffset); + static Handle GetColumnValue(SQLHSTMT hStmt, Column column, uint16_t* buffer, SQLLEN bufferLength, bool partial = false, bool fetch = true); static Local GetRecordTuple (SQLHSTMT hStmt, Column* columns, short* colCount, uint16_t* buffer, int bufferLength); static Handle GetRecordArray (SQLHSTMT hStmt, Column* columns, short* colCount, uint16_t* buffer, int bufferLength); static Handle CallbackSQLError (SQLSMALLINT handleType, SQLHANDLE handle, Persistent cb); diff --git a/src/odbc_result.h b/src/odbc_result.h index 3c2395e..014efa8 100644 --- a/src/odbc_result.h +++ b/src/odbc_result.h @@ -90,10 +90,10 @@ class ODBCResult : public node::ObjectWrap { Persistent resultBuffer; // Used for keeping the buffer alive void* resultBufferContents; size_t resultBufferOffset; - size_t resultBufferLength; + SQLLEN resultBufferLength; - SQLINTEGER bytesAvailable; - SQLINTEGER bytesRead; + SQLLEN bytesAvailable; + SQLLEN bytesRead; }; From 851a2da167cc626f6d21f1b59a92128415f25224 Mon Sep 17 00:00:00 2001 From: Lee Houghton Date: Thu, 4 Sep 2014 15:21:46 +0100 Subject: [PATCH 23/26] Use SQLINTEGER and SQLDOUBLE instead of long and double in ConvertColumnValue/GetCColumnType (cherry picked from commit c88022f32c7070f0d644cfdc0785217d0f31c7c0) --- src/odbc.cpp | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/odbc.cpp b/src/odbc.cpp index 46e261b..3580cbb 100644 --- a/src/odbc.cpp +++ b/src/odbc.cpp @@ -376,11 +376,11 @@ SQLRETURN ODBC::GetColumnData( SQLHSTMT hStmt, const Column& column, switch(cType) { case SQL_C_SLONG: - bufferLength = sizeof(long); + bufferLength = sizeof(SQLINTEGER); break; case SQL_C_DOUBLE: - bufferLength = sizeof(double); + bufferLength = sizeof(SQLDOUBLE); break; } @@ -401,14 +401,13 @@ Handle ODBC::ConvertColumnValue( SQLSMALLINT cType, switch (cType) { case SQL_C_SLONG: - assert(bytesInBuffer >= sizeof(long)); - // XXX Integer::New will truncate here if sizeof(long) > 4, it expects 32-bit numbers - return scope.Close(Integer::New(*reinterpret_cast(buffer))); + assert(bytesInBuffer >= sizeof(SQLINTEGER)); + return scope.Close(Integer::New(*reinterpret_cast(buffer))); break; case SQL_C_DOUBLE: - assert(bytesInBuffer >= sizeof(double)); - return scope.Close(Number::New(*reinterpret_cast(buffer))); + assert(bytesInBuffer >= sizeof(SQLDOUBLE)); + return scope.Close(Number::New(*reinterpret_cast(buffer))); break; case SQL_C_TYPE_TIMESTAMP: { From 2702f8b6fda5bece78734ce79670d45d9576d8f7 Mon Sep 17 00:00:00 2001 From: Lee Houghton Date: Mon, 6 Oct 2014 12:44:09 +0100 Subject: [PATCH 24/26] Handle null binary values in ODBC::GetColumnValue --- src/odbc.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/odbc.cpp b/src/odbc.cpp index 3580cbb..a2fbce0 100644 --- a/src/odbc.cpp +++ b/src/odbc.cpp @@ -572,6 +572,9 @@ Handle ODBC::GetColumnValue( SQLHSTMT hStmt, Column column, // Maybe this should be an error? if (ret == SQL_NO_DATA) return scope.Close(Undefined()); + + if (bytesAvailable == SQL_NULL_DATA) + return scope.Close(Null()); if (!SQL_SUCCEEDED(ret)) return ThrowException(ODBC::GetSQLError(SQL_HANDLE_STMT, hStmt, "ODBC::GetColumnValue: Error getting data for result column")); From dd268af5840eb04a276c213a6c39c655c07981eb Mon Sep 17 00:00:00 2001 From: Lee Houghton Date: Tue, 14 Oct 2014 13:53:09 +0100 Subject: [PATCH 25/26] On Windows, use SystemTimeToFileTime instead of timegm (since it handles dates before 1970 correctly, unlike MSVC's _mkgmtime32). (cherry picked from commit d244a9899b1b6b9b3cd39c4b486710706bc12bab) --- src/odbc.cpp | 75 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 30 deletions(-) diff --git a/src/odbc.cpp b/src/odbc.cpp index a2fbce0..b101418 100644 --- a/src/odbc.cpp +++ b/src/odbc.cpp @@ -393,6 +393,50 @@ SQLRETURN ODBC::GetColumnData( SQLHSTMT hStmt, const Column& column, &len); } +#if defined(WIN32) +double SQLTimeToV8Time(SQL_TIMESTAMP_STRUCT odbcTime) { + SYSTEMTIME systemTime = { + odbcTime.year, odbcTime.month, 0, odbcTime.day, + odbcTime.hour, odbcTime.minute, odbcTime.second, + odbcTime.fraction / 1000000 // wMilliseconds + }; + FILETIME fileTime; + + if(!SystemTimeToFileTime(&systemTime, &fileTime)) + return 0; + + LARGE_INTEGER ll; + ll.LowPart = fileTime.dwLowDateTime; + ll.HighPart = fileTime.dwHighDateTime; + + return double(ll.QuadPart - 116444736000000000I64) / 10000; // 100ns intervals to 1ms intervals +} +#else +double SQLTimeToV8Time(SQL_TIMESTAMP_STRUCT odbcTime) { + struct tm timeInfo = { 0 }; + SQLUINTEGER fraction = 0; + + timeInfo.tm_year = odbcTime.year - 1900; + timeInfo.tm_mon = odbcTime.month - 1; + timeInfo.tm_mday = odbcTime.day; + timeInfo.tm_hour = odbcTime.hour; + timeInfo.tm_min = odbcTime.minute; + timeInfo.tm_sec = odbcTime.second; + fraction = odbcTime.fraction; + + //a negative value means that mktime() should use timezone information + //and system databases to attempt to determine whether DST is in effect + //at the specified time. + timeInfo.tm_isdst = -1; + +# if defined(TIMEGM) + return (double(timegm(&timeInfo)) * 1000) + (fraction / 1000000.0); +# else + return (double(timelocal(&timeInfo)) * 1000) + (fraction / 1000000.0); +# endif +} +#endif + Handle ODBC::ConvertColumnValue( SQLSMALLINT cType, uint16_t* buffer, SQLINTEGER bytesInBuffer, Buffer* resultBuffer, size_t resultBufferOffset) { @@ -411,39 +455,10 @@ Handle ODBC::ConvertColumnValue( SQLSMALLINT cType, break; case SQL_C_TYPE_TIMESTAMP: { - struct tm timeInfo = { 0 }; - SQLUINTEGER fraction = 0; - assert(bytesInBuffer >= sizeof(SQL_TIMESTAMP_STRUCT)); SQL_TIMESTAMP_STRUCT& odbcTime = *reinterpret_cast(buffer); - timeInfo.tm_year = odbcTime.year - 1900; - timeInfo.tm_mon = odbcTime.month - 1; - timeInfo.tm_mday = odbcTime.day; - timeInfo.tm_hour = odbcTime.hour; - timeInfo.tm_min = odbcTime.minute; - timeInfo.tm_sec = odbcTime.second; - fraction = odbcTime.fraction; - - //a negative value means that mktime() should use timezone information - //and system databases to attempt to determine whether DST is in effect - //at the specified time. - timeInfo.tm_isdst = -1; - -#if defined(_WIN32) && defined (TIMEGM) - return scope.Close(Date::New((double(_mkgmtime32(&timeInfo)) * 1000) - + (fraction / 1000000.0))); -#elif defined(WIN32) - return scope.Close(Date::New((double(mktime(&timeInfo)) * 1000) - + (fraction / 1000000.0))); -#elif defined(TIMEGM) - return scope.Close(Date::New((double(timegm(&timeInfo)) * 1000) - + (fraction / 1000000.0))); -#else - return scope.Close(Date::New((double(timelocal(&timeInfo)) * 1000) - + (fraction / 1000000.0))); -#endif - break; + return scope.Close(Date::New(SQLTimeToV8Time(odbcTime))); } case SQL_C_BIT: From 2f818f95a3c3c5487dee364a3d569c36111d54cf Mon Sep 17 00:00:00 2001 From: Lee Houghton Date: Wed, 15 Oct 2014 11:55:10 +0100 Subject: [PATCH 26/26] Default to local time on Windows (as before), and handle the TIMEGM define by not using TzSpecificLocalTimeToSystemTime in that case. --- src/odbc.cpp | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/odbc.cpp b/src/odbc.cpp index b101418..36ebf4b 100644 --- a/src/odbc.cpp +++ b/src/odbc.cpp @@ -395,20 +395,38 @@ SQLRETURN ODBC::GetColumnData( SQLHSTMT hStmt, const Column& column, #if defined(WIN32) double SQLTimeToV8Time(SQL_TIMESTAMP_STRUCT odbcTime) { - SYSTEMTIME systemTime = { + SYSTEMTIME time = { odbcTime.year, odbcTime.month, 0, odbcTime.day, odbcTime.hour, odbcTime.minute, odbcTime.second, odbcTime.fraction / 1000000 // wMilliseconds }; FILETIME fileTime; - if(!SystemTimeToFileTime(&systemTime, &fileTime)) - return 0; - + // Date::New expects UTC. If TIMEGM is not set, we need to convert + // from local time to UTC before passing to Date::New(). + // + // First, do any time zone conversion, then convert system time to + // file time (the number of 100-nanosecond intervals since 1/1/1601 UTC). + +# if defined(TIMEGM) + if (!SystemTimeToFileTime(&time, &fileTime)) + return 0; +# else + // Time is local time, convert to UTC first. + SYSTEMTIME utcTime; + if (!TzSpecificLocalTimeToSystemTime(NULL, &time, &utcTime)) + return 0; + if (!SystemTimeToFileTime(&utcTime, &fileTime)) + return 0; +# endif + + // This is way MSDN recommends converting a FILETIME to a 64-bit integer. Using + // signed integer types here to allow dates before 1/1/1970. LARGE_INTEGER ll; ll.LowPart = fileTime.dwLowDateTime; ll.HighPart = fileTime.dwHighDateTime; + // 116444736000000000I64 is the epoch as a FILETIME (1/1/1970) return double(ll.QuadPart - 116444736000000000I64) / 10000; // 100ns intervals to 1ms intervals } #else