diff --git a/binding.gyp b/binding.gyp index cb1e8dd..3c80a81 100644 --- a/binding.gyp +++ b/binding.gyp @@ -1,42 +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/strptime.c', - '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' + ], + '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.cpp b/src/odbc.cpp index 27da0f3..36ebf4b 100644 --- a/src/odbc.cpp +++ b/src/odbc.cpp @@ -1,985 +1,1044 @@ -/* - 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 "odbc.h" -#include "odbc_connection.h" -#include "odbc_result.h" -#include "odbc_statement.h" - -#ifdef dynodbc -#include "dynodbc.h" -#endif - -#ifdef _WIN32 -#include "strptime.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); - - // 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) { - for(int i = 0; i < *colCount; i++) { - delete [] columns[i].name; - } - - delete [] columns; - - *colCount = 0; -} - -/* - * GetColumnValue - */ - -Handle ODBC::GetColumnValue( SQLHSTMT hStmt, Column column, - uint16_t* buffer, int bufferLength) { - HandleScope scope; - SQLLEN len = 0; - - //reset the buffer - buffer[0] = '\0'; - - //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); - } - } - 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); - } - } - 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 - }; - - 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 { - 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; -#ifdef TIMEGM - return scope.Close(Date::New((double(timegm(&timeInfo)) * 1000) - + (odbcTime.fraction / 1000000))); -#else - return scope.Close(Date::New((double(timelocal(&timeInfo)) * 1000) - + (odbcTime.fraction / 1000000))); -#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 - //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 ); - } - 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 && str.IsEmpty()) { - return scope.Close(Null()); - //return Null(); - } - - if (SQL_NO_DATA == ret) { - //we have captured all of the data - //double check that we have some data else return null - if (str.IsEmpty()){ - return scope.Close(Null()); - } - - 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 - } - - //if len is zero let's break out of the loop now and not attempt to - //call SQLGetData again. The specific reason for this is because - //some ODBC drivers may not correctly report SQL_NO_DATA the next - //time around causing an infinite loop here - if (len == 0) { - break; - } - - count += 1; - } - 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; - } - } while (true); - - return scope.Close(str); - //return str; - } -} - -/* - * 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++) { -#ifdef UNICODE - tuple->Set( String::New((uint16_t *) columns[i].name), - GetColumnValue( hStmt, columns[i], buffer, bufferLength)); -#else - tuple->Set( String::New((const char *) columns[i].name), - GetColumnValue( hStmt, columns[i], buffer, bufferLength)); -#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++) { - array->Set( Integer::New(i), - GetColumnValue( hStmt, columns[i], buffer, bufferLength)); - } - - //return array; - return scope.Close(array); -} - -/* - * GetParametersFromArray - */ - -Parameter* ODBC::GetParametersFromArray (Local values, int *paramCount) { - DEBUG_PRINTF("ODBC::GetParametersFromArray\n"); - *paramCount = values->Length(); - - Parameter* params = NULL; - - if (*paramCount > 0) { - 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 = SQL_WLONGVARCHAR; - params[i].buffer_length = (length * sizeof(uint16_t)) + sizeof(uint16_t); -#else - params[i].type = SQL_LONGVARCHAR; - 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(); - Local str = String::New(""); - - SQLINTEGER i = 0; - SQLINTEGER native; - - SQLSMALLINT len; - SQLINTEGER statusRecCount; - SQLRETURN ret; - char errorSQLState[14]; - char errorMessage[ERROR_MESSAGE_BUFFER_BYTES]; - - ret = SQLGetDiagField( - handleType, - handle, - 0, - SQL_DIAG_NUMBER, - &statusRecCount, - 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, statusRecCount=%i\n", ret, statusRecCount); - - for (i = 0; i < statusRecCount; i++){ - DEBUG_PRINTF("ODBC::GetSQLError : calling SQLGetDiagRec; i=%i, statusRecCount=%i\n", i, statusRecCount); - - ret = SQLGetDiagRec( - handleType, - handle, - i + 1, - (SQLTCHAR *) errorSQLState, - &native, - (SQLTCHAR *) errorMessage, - ERROR_MESSAGE_BUFFER_CHARS, - &len); - - DEBUG_PRINTF("ODBC::GetSQLError : after SQLGetDiagRec; i=%i\n", i); - - if (SQL_SUCCEEDED(ret)) { - DEBUG_PRINTF("ODBC::GetSQLError : errorMessage=%s, errorSQLState=%s\n", errorMessage, errorSQLState); - - objError->Set(String::New("error"), String::New(message)); -#ifdef UNICODE - str = String::Concat(str, String::New((uint16_t *) errorMessage)); - - objError->SetPrototype(Exception::Error(String::New((uint16_t *) errorMessage))); - objError->Set(String::New("message"), str); - objError->Set(String::New("state"), String::New((uint16_t *) errorSQLState)); -#else - str = String::Concat(str, String::New(errorMessage)); - - objError->SetPrototype(Exception::Error(String::New(errorMessage))); - objError->Set(String::New("message"), str); - objError->Set(String::New("state"), String::New(errorSQLState)); -#endif - } else if (ret == SQL_NO_DATA) { - break; - } - } - - if (statusRecCount == 0) { - //Create a default error object if there were no diag records - objError->Set(String::New("error"), String::New(message)); - objError->SetPrototype(Exception::Error(String::New(message))); - objError->Set(String::New("message"), String::New( - (const char *) "[node-odbc] An error occurred but no diagnostic information was available.")); - } - - 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; + + // Date and time + case SQL_TIMESTAMP: case SQL_TYPE_TIMESTAMP: + case SQL_DATE: case SQL_TYPE_DATE: + 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, SQLLEN bufferLength, SQLSMALLINT& cType, SQLLEN& len) { + + cType = GetCColumnType(column); + + switch(cType) { + case SQL_C_SLONG: + bufferLength = sizeof(SQLINTEGER); + break; + + case SQL_C_DOUBLE: + bufferLength = sizeof(SQLDOUBLE); + break; + } + + return SQLGetData( + hStmt, + column.index, + cType, + buffer, + bufferLength, + &len); +} + +#if defined(WIN32) +double SQLTimeToV8Time(SQL_TIMESTAMP_STRUCT odbcTime) { + SYSTEMTIME time = { + odbcTime.year, odbcTime.month, 0, odbcTime.day, + odbcTime.hour, odbcTime.minute, odbcTime.second, + odbcTime.fraction / 1000000 // wMilliseconds + }; + FILETIME fileTime; + + // 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 +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) { + + HandleScope scope; + + switch (cType) { + case SQL_C_SLONG: + assert(bytesInBuffer >= sizeof(SQLINTEGER)); + return scope.Close(Integer::New(*reinterpret_cast(buffer))); + break; + + case SQL_C_DOUBLE: + assert(bytesInBuffer >= sizeof(SQLDOUBLE)); + 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); + + return scope.Close(Date::New(SQLTimeToV8Time(odbcTime))); + } + + 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, + 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 + SQLLEN 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 (bytesAvailable == SQL_NULL_DATA) + bytesRead = 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, SQLLEN 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) { + SQLLEN 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; + SQLLEN 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 (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")); + + 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 ? slowBuffer->handle_ : Handle(), slowBuffer, offset)); +} + +Handle ODBC::InterpretBuffers( SQLSMALLINT cType, + 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); + + 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 = NULL; + + if (*paramCount > 0) { + 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 = SQL_WLONGVARCHAR; + params[i].buffer_length = (length * sizeof(uint16_t)) + sizeof(uint16_t); +#else + params[i].type = SQL_LONGVARCHAR; + 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] " + 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); + } + 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] " + 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); + } + 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] " + 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); + } + 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] " + 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); + } + 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] " + 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); + } + } + + 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(); + Local str = String::New(""); + + SQLSMALLINT i = 0; + SQLINTEGER native; + + SQLSMALLINT len; + SQLINTEGER statusRecCount; + SQLRETURN ret; + char errorSQLState[14]; + char errorMessage[ERROR_MESSAGE_BUFFER_BYTES]; + + ret = SQLGetDiagField( + handleType, + handle, + 0, + SQL_DIAG_NUMBER, + &statusRecCount, + 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, statusRecCount=%i\n", ret, statusRecCount); + + for (i = 0; i < statusRecCount; i++){ + DEBUG_PRINTF("ODBC::GetSQLError : calling SQLGetDiagRec; i=%i, statusRecCount=%i\n", i, statusRecCount); + + ret = SQLGetDiagRec( + handleType, + handle, + i + 1, + (SQLTCHAR *) errorSQLState, + &native, + (SQLTCHAR *) errorMessage, + ERROR_MESSAGE_BUFFER_CHARS, + &len); + + DEBUG_PRINTF("ODBC::GetSQLError : after SQLGetDiagRec; i=%i\n", i); + + if (SQL_SUCCEEDED(ret)) { + DEBUG_PRINTF("ODBC::GetSQLError : errorMessage=%s, errorSQLState=%s\n", errorMessage, errorSQLState); + + objError->Set(String::New("error"), String::New(message)); +#ifdef UNICODE + str = String::Concat(str, String::New((uint16_t *) errorMessage)); + + objError->SetPrototype(Exception::Error(String::New((uint16_t *) errorMessage))); + objError->Set(String::New("message"), str); + objError->Set(String::New("state"), String::New((uint16_t *) errorSQLState)); +#else + str = String::Concat(str, String::New(errorMessage)); + + objError->SetPrototype(Exception::Error(String::New(errorMessage))); + objError->Set(String::New("message"), str); + objError->Set(String::New("state"), String::New(errorSQLState)); +#endif + } else if (ret == SQL_NO_DATA) { + break; + } + } + + if (statusRecCount == 0) { + //Create a default error object if there were no diag records + objError->Set(String::New("error"), String::New(message)); + objError->SetPrototype(Exception::Error(String::New(message))); + objError->Set(String::New("message"), String::New( + (const char *) "[node-odbc] An error occurred but no diagnostic information was available.")); + } + + 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) diff --git a/src/odbc.h b/src/odbc.h index 6a9c587..d7fe168 100644 --- a/src/odbc.h +++ b/src/odbc.h @@ -20,6 +20,7 @@ #include #include +#include #include #include @@ -50,9 +51,9 @@ 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 - typedef struct { unsigned char *name; unsigned int len; @@ -79,7 +80,12 @@ 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, 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, 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); @@ -154,9 +160,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 @@ -177,6 +183,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()) \ @@ -248,5 +261,23 @@ 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 + +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_connection.cpp b/src/odbc_connection.cpp index 2ae9b86..ae8b0dc 100644 --- a/src/odbc_connection.cpp +++ b/src/odbc_connection.cpp @@ -809,14 +809,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; @@ -868,9 +871,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( @@ -946,8 +950,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(); @@ -1136,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( @@ -1737,4 +1742,4 @@ void ODBCConnection::UV_AfterEndTransaction(uv_work_t* req, int status) { free(req); scope.Close(Undefined()); -} \ No newline at end of file +} diff --git a/src/odbc_result.cpp b/src/odbc_result.cpp index d49a28f..47dc019 100644 --- a/src/odbc_result.cpp +++ b/src/odbc_result.cpp @@ -1,755 +1,944 @@ -/* - 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 "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, "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); - - // 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() { - DEBUG_PRINTF("ODBCResult::~ODBCResult m_hSTMT=%x\n", m_hSTMT); - 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 - 1; - - //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[2]; - - 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 { - args[1] = ODBC::GetRecordTuple( - data->objResult->m_hSTMT, - data->objResult->columns, - &data->objResult->colCount, - data->objResult->buffer, - data->objResult->bufferLength); - } - - TryCatch try_catch; - - data->cb->Call(Context::GetCurrent()->Global(), 2, args); - data->cb.Dispose(); - - if (try_catch.HasCaught()) { - FatalException(try_catch); - } - } - else { - ODBC::FreeColumns(data->objResult->columns, &data->objResult->colCount); - - Handle args[2]; - - //if there was an error, pass that as arg[0] otherwise Null - if (error) { - args[0] = objError; - } - else { - args[0] = Null(); - } - - args[1] = Null(); - - TryCatch try_catch; - - data->cb->Call(Context::GetCurrent()->Global(), 2, args); - data->cb.Dispose(); - - if (try_catch.HasCaught()) { - FatalException(try_catch); - } - } - - data->objResult->Unref(); - - free(data); - free(work_req); - - 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 { - data = ODBC::GetRecordTuple( - objResult->m_hSTMT, - objResult->columns, - &objResult->colCount, - objResult->buffer, - objResult->bufferLength); - } - - 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 { - 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; - } - - if (fetchMode == FETCH_ARRAY) { - rows->Set( - Integer::New(count), - ODBC::GetRecordArray( - self->m_hSTMT, - self->columns, - &self->colCount, - self->buffer, - self->bufferLength) - ); - } - else { - rows->Set( - Integer::New(count), - ODBC::GetRecordTuple( - self->m_hSTMT, - self->columns, - &self->colCount, - self->buffer, - self->bufferLength) - ); - } - 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); -} +/* + 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() { + DEBUG_PRINTF("ODBCResult::~ODBCResult m_hSTMT=%x\n", m_hSTMT); + 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); + } + } + + data->objResult->Unref(); + + free(data); + free(work_req); + + 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 diff --git a/src/odbc_result.h b/src/odbc_result.h index da4a3f8..014efa8 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); @@ -55,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); @@ -71,6 +76,26 @@ 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; + SQLSMALLINT cType; + + // State + Persistent resultBuffer; // Used for keeping the buffer alive + void* resultBufferContents; + size_t resultBufferOffset; + SQLLEN resultBufferLength; + + SQLLEN bytesAvailable; + SQLLEN bytesRead; + + }; ODBCResult *self(void) { return this; } diff --git a/src/odbc_statement.cpp b/src/odbc_statement.cpp index 42b702d..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=%s\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 ); 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 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); }); 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"); }); }); }); 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 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 + //}]); }); }); }); 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); 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);