Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for associative arrays #153

Merged
merged 13 commits into from Sep 17, 2014
6 changes: 6 additions & 0 deletions src/connection.cpp
Expand Up @@ -205,6 +205,7 @@ int Connection::SetValuesOnStatement(oracle::occi::Statement* stmt, ExecuteBaton
uint32_t index = 1; uint32_t index = 1;
int outputParam = -1; int outputParam = -1;
OutParam * outParam = NULL; OutParam * outParam = NULL;
arrayParam_t* arrParam;
for (vector<value_t*>::iterator iterator = values.begin(), end = values.end(); iterator != end; ++iterator, index++) { for (vector<value_t*>::iterator iterator = values.begin(), end = values.end(); iterator != end; ++iterator, index++) {
value_t* val = *iterator; value_t* val = *iterator;
int outParamType; int outParamType;
Expand All @@ -222,6 +223,11 @@ int Connection::SetValuesOnStatement(oracle::occi::Statement* stmt, ExecuteBaton
case VALUE_TYPE_TIMESTAMP: case VALUE_TYPE_TIMESTAMP:
stmt->setTimestamp(index, *((oracle::occi::Timestamp*)val->value)); stmt->setTimestamp(index, *((oracle::occi::Timestamp*)val->value));
break; break;
case VALUE_TYPE_ARRAY:
stmt->setDatabaseNCHARParam(index, true);
arrParam = (arrayParam_t*)val->value;
stmt->setDataBufferArray(index, arrParam->value, arrParam->elementsType, arrParam->collectionLength, &arrParam->collectionLength, arrParam->elementsSize, arrParam->elementLength, NULL, NULL);
break;
case VALUE_TYPE_OUTPUT: case VALUE_TYPE_OUTPUT:
outParam = static_cast<OutParam*>(val->value); outParam = static_cast<OutParam*>(val->value);
// std::cout << "OutParam B: " << outParam << " "<< outParam->type() << " " << outParam->_inOut.hasInParam << std::endl; // std::cout << "OutParam B: " << outParam << " "<< outParam->type() << " " << outParam->_inOut.hasInParam << std::endl;
Expand Down
128 changes: 128 additions & 0 deletions src/executeBaton.cpp
Expand Up @@ -3,6 +3,8 @@
#include "connection.h" #include "connection.h"
#include "outParam.h" #include "outParam.h"
#include <iostream> #include <iostream>
#include <string.h>
#include <cmath>
using namespace std; using namespace std;


ExecuteBaton::ExecuteBaton(Connection* connection, const char* sql, v8::Local<v8::Array>* values, v8::Handle<v8::Function>* callback) { ExecuteBaton::ExecuteBaton(Connection* connection, const char* sql, v8::Local<v8::Array>* values, v8::Handle<v8::Function>* callback) {
Expand Down Expand Up @@ -45,6 +47,18 @@ void ExecuteBaton::ResetValues() {
case VALUE_TYPE_TIMESTAMP: case VALUE_TYPE_TIMESTAMP:
delete (oracle::occi::Timestamp*)val->value; delete (oracle::occi::Timestamp*)val->value;
break; break;
case VALUE_TYPE_ARRAY:
arrayParam_t* arrParam = (arrayParam_t*)val->value;
if (arrParam->value != NULL && arrParam->elementsType == oracle::occi::OCCI_SQLT_STR)
delete (char*)arrParam->value;
else if (arrParam->value != NULL && arrParam->elementsType == oracle::occi::OCCI_SQLT_NUM)
delete (char*)arrParam->value;

if (arrParam->elementLength != NULL)
delete arrParam->elementLength;

delete (arrayParam_t*)val->value;
break;
} }
delete val; delete val;
} }
Expand Down Expand Up @@ -136,6 +150,15 @@ void ExecuteBaton::CopyValuesToBaton(ExecuteBaton* baton, v8::Local<v8::Array>*
baton->values.push_back(value); baton->values.push_back(value);
} }


// array
else if (val->IsArray()) {
value->type = VALUE_TYPE_ARRAY;
Local<Array> arr = Local<Array>::Cast(val);
value->value = new arrayParam_t();
GetVectorParam(baton, (arrayParam_t*)value->value, arr);
baton->values.push_back(value);
}

// output // output
else if(val->IsObject() && val->ToObject()->FindInstanceInPrototypeChain(uni::Deref(OutParam::constructorTemplate)) != v8::Null()) { else if(val->IsObject() && val->ToObject()->FindInstanceInPrototypeChain(uni::Deref(OutParam::constructorTemplate)) != v8::Null()) {
OutParam* op = node::ObjectWrap::Unwrap<OutParam>(val->ToObject()); OutParam* op = node::ObjectWrap::Unwrap<OutParam>(val->ToObject());
Expand All @@ -161,6 +184,111 @@ void ExecuteBaton::CopyValuesToBaton(ExecuteBaton* baton, v8::Local<v8::Array>*
baton->error = new std::string(message.str()); baton->error = new std::string(message.str());
return; return;
} }
}
}

void ExecuteBaton::GetVectorParam(ExecuteBaton* baton, arrayParam_t* arrParam, Local<Array> arr) {
// In case the array is empty just initialize the fields as we would need something in Connection::SetValuesOnStatement
if (arr->Length() < 1) {
arrParam->value = new int[0];
arrParam->collectionLength = 0;
arrParam->elementsSize = 0;
arrParam->elementLength = new ub2[0];
arrParam->elementsType = oracle::occi::OCCIINT;
return;
}

// Next we create the array buffer that will be used later as the value for the param (in Connection::SetValuesOnStatement)
// The array type will be derived from the type of the first element.
Local<Value> val = arr->Get(0);

// String array
if (val->IsString()) {
arrParam->elementsType = oracle::occi::OCCI_SQLT_STR;

// Find the longest string, this is necessary in order to create a buffer later.
int longestString = 0;
for(unsigned int i = 0; i < arr->Length(); i++) {
Local<Value> currVal = arr->Get(i);
if (currVal->ToString()->Utf8Length() > longestString)
longestString = currVal->ToString()->Utf8Length();
}

// Add 1 for '\0'
++longestString;

// Create a long char* that will hold the entire array, it is important to create a FIXED SIZE array,
// meaning all strings have the same allocated length.
char* strArr = new char[arr->Length() * longestString];
arrParam->elementLength = new ub2[arr->Length()];

// loop thru the arr and copy the strings into the strArr
int bytesWritten = 0;
for(unsigned int i = 0; i < arr->Length(); i++) {
Local<Value> currVal = arr->Get(i);
if(!currVal->IsString()) {
std::ostringstream message;
message << "Input array has object with invalid type at index " << i << ", all object must be of type 'string' which is the type of the first element";
baton->error = new std::string(message.str());
return;
}

String::Utf8Value utfStr(currVal);

// Copy this string onto the strArr (we put \0 in the beginning as this is what strcat expects).
strArr[bytesWritten] = '\0';
strncat(strArr + bytesWritten, *utfStr, longestString);
bytesWritten += longestString;

// Set the length of this element, add +1 for the '\0'
arrParam->elementLength[i] = utfStr.length() + 1;
}

arrParam->value = strArr;
arrParam->collectionLength = arr->Length();
arrParam->elementsSize = longestString;
}

// Integer array.
else if (val->IsNumber()) {
arrParam->elementsType = oracle::occi::OCCI_SQLT_NUM;

// Allocate memory for the numbers array, Number in Oracle is 21 bytes
unsigned char* numArr = new unsigned char[arr->Length() * 21];
arrParam->elementLength = new ub2[arr->Length()];

for(unsigned int i = 0; i < arr->Length(); i++) {
Local<Value> currVal = arr->Get(i);
if(!currVal->IsNumber()) {
std::ostringstream message;
message << "Input array has object with invalid type at index " << i << ", all object must be of type 'number' which is the type of the first element";
baton->error = new std::string(message.str());
return;
}

// JS numbers can exceed oracle numbers, make sure this is not the case.
double d = currVal->ToNumber()->Value();
if (d > 9.99999999999999999999999999999999999999*std::pow(10, 125) || d < -9.99999999999999999999999999999999999999*std::pow(10, 125)) {
std::ostringstream message;
message << "Input array has number that is out of the range of Oracle numbers, check the number at index " << i;
baton->error = new std::string(message.str());
return;
}

// Convert the JS number into Oracle Number and get its bytes representation
oracle::occi::Number n = d;
oracle::occi::Bytes b = n.toBytes();
arrParam->elementLength[i] = b.length ();
b.getBytes(&numArr[i*21], b.length());
}

arrParam->value = numArr;
arrParam->collectionLength = arr->Length();
arrParam->elementsSize = 21;
}


// Unsupported type
else {
baton->error = new std::string("The type of the first element in the input array is not supported");
} }
} }
13 changes: 12 additions & 1 deletion src/executeBaton.h
Expand Up @@ -23,7 +23,8 @@ enum {
VALUE_TYPE_DATE = 5, VALUE_TYPE_DATE = 5,
VALUE_TYPE_TIMESTAMP = 6, VALUE_TYPE_TIMESTAMP = 6,
VALUE_TYPE_CLOB = 7, VALUE_TYPE_CLOB = 7,
VALUE_TYPE_BLOB = 8 VALUE_TYPE_BLOB = 8,
VALUE_TYPE_ARRAY = 9
}; };


struct column_t { struct column_t {
Expand All @@ -41,6 +42,15 @@ struct value_t {
void* value; void* value;
}; };


struct arrayParam_t {
// This will hold the info needed for binding array values
void* value;
ub4 collectionLength;
sb4 elementsSize; // The size of each element in the array
ub2* elementLength; // An array that holds the actual length of each element in the array (in case of strings)
oracle::occi::Type elementsType;
};

struct output_t { struct output_t {
int type; int type;
int index; int index;
Expand Down Expand Up @@ -77,6 +87,7 @@ class ExecuteBaton {
int updateCount; int updateCount;


static void CopyValuesToBaton(ExecuteBaton* baton, v8::Local<v8::Array>* values); static void CopyValuesToBaton(ExecuteBaton* baton, v8::Local<v8::Array>* values);
static void GetVectorParam(ExecuteBaton* baton, arrayParam_t *value, v8::Local<v8::Array> arr);
}; };


#endif #endif
146 changes: 146 additions & 0 deletions test/assocArrays.js
@@ -0,0 +1,146 @@
/*
tests-settings.json:
{
"hostname": "localhost",
"user": "test",
"password": "test"
}
*/

var nodeunit = require("nodeunit");
var oracle = require("../");

var settings;

try {
settings = JSON.parse(require('fs').readFileSync('./tests-settings-custom.json', 'utf8'));
} catch (ex) {}
settings = settings || JSON.parse(require('fs').readFileSync('./tests-settings.json', 'utf8'));

function initDb(connection, cb) {
// Create the TEST_PKG for the test (first spec then body).

var spec = '\
CREATE OR REPLACE PACKAGE "TEST_PKG" IS \
TYPE Y_STRINGS_TABLE IS TABLE OF NVARCHAR2(4000) INDEX BY PLS_INTEGER; \
TYPE Y_NUMBERS_TABLE IS TABLE OF NUMBER INDEX BY PLS_INTEGER; \
PROCEDURE sp_get_numbers(i_arr Y_NUMBERS_TABLE, o_out OUT sys_refcursor); \
PROCEDURE sp_get_strings(i_arr Y_STRINGS_TABLE, o_out OUT sys_refcursor); \
END test_pkg;'

var body = '\
CREATE OR REPLACE PACKAGE BODY "TEST_PKG" IS \
PROCEDURE sp_get_numbers(i_arr Y_NUMBERS_TABLE, o_out OUT sys_refcursor) IS \
vals sys.ODCINumberList := sys.ODCINumberList(); \
BEGIN \
FOR i IN i_arr.first..i_arr.last LOOP \
vals.extend(1); \
vals(i) := i_arr(i); \
END LOOP; \
OPEN o_out FOR SELECT * FROM TABLE(vals); \
END; \
PROCEDURE sp_get_strings(i_arr Y_STRINGS_TABLE, o_out OUT sys_refcursor) IS \
vals sys.ODCIVarchar2List := sys.ODCIVarchar2List(); \
BEGIN \
FOR i IN i_arr.first..i_arr.last LOOP \
vals.extend(1); \
vals(i) := i_arr(i); \
END LOOP; \
OPEN o_out FOR SELECT * FROM TABLE(vals); \
END; \
END test_pkg;';

connection.execute(spec, [], function(err) {
if (err) throw err;

connection.execute(body, [], function(err) {
if (err) throw err;
cb();
});
});
}


exports['AssocArrays'] = nodeunit.testCase({
setUp: function(callback) {
var self = this;
oracle.connect(settings, function(err, connection) {
if (err) return callback(err);
self.connection = connection;
initDb(self.connection, callback);
});
},

tearDown: function(callback) {
if (this.connection) {
this.connection.close();
}
callback();
},

"AssocArrays - Select using a numbers array": function(test) {
var out = new oracle.OutParam(oracle.OCCICURSOR);
var arr = [12.453, -98.31, -5, 5, 3.876e123, -3.876e123];
this.connection.execute('Begin TEST_PKG.sp_get_numbers(:1, :2); End;', [arr, out], function(err, results) {
if(err) { console.error(err); return; }
test.equal(results.returnParam.length, arr.length);

// Loop thru all the values we passed and check that we got them back.
// Due to floating point precision (and exponential notation) we are testing
// that they are "close enough".
arr.forEach(function(val, i) {
var res = results.returnParam[i]['COLUMN_VALUE'];
test.ok(Math.abs(res/val - 1) < 0.001, 'Expected: ' + val + ' Got: ' + res);
});
test.done();
});
},

"AssocArrays - Select using too big positive number": function(test) {
var out = new oracle.OutParam(oracle.OCCICURSOR);
var arr = [12.453, Number.MAX_VALUE, -5, 5, 0];
var self = this;
test.throws(function(){
self.connection.execute('Begin TEST_PKG.sp_get_numbers(:1, :2); End;', [arr, out], function(err, results) {});
});
test.done();
},

"AssocArrays - Select using too big negative number": function(test) {
var out = new oracle.OutParam(oracle.OCCICURSOR);
var arr = [12.453, -1*Number.MAX_VALUE, -5, 5, 0];
var self = this;
test.throws(function(){
self.connection.execute('Begin TEST_PKG.sp_get_numbers(:1, :2); End;', [arr, out], function(err, results) {});
});
test.done();
},

"AssocArrays - Select using strings": function(test) {
var out = new oracle.OutParam(oracle.OCCICURSOR);
var arr = ['1234567890', 'ThE ', 'quick ', ' BrOwN ','fox' ,'Ju m p s', '!!!', 'noo ??', '~!@#$%^&*()_+', ''];
this.connection.execute('Begin TEST_PKG.sp_get_strings(:1, :2); End;', [arr, out], function(err, results) {
if(err) { console.error(err); return; }
test.equal(results.returnParam.length, arr.length);
arr.forEach(function(val, i) {
var res = results.returnParam[i]['COLUMN_VALUE'] || '';
test.equal(res, val);
});
test.done();
});
},

"AssocArrays - Select using UTF8 strings": function(test) {
var out = new oracle.OutParam(oracle.OCCICURSOR);
var arr = ['тест', ' тест ', '12тест34', 'AB тест'];
this.connection.execute('Begin TEST_PKG.sp_get_strings(:1, :2); End;', [arr, out], function(err, results) {
if(err) { console.error(err); return; }
test.equal(results.returnParam.length, arr.length);
arr.forEach(function(val, i) {
var res = results.returnParam[i]['COLUMN_VALUE'] || '';
test.equal(res, val);
});
test.done();
});
}
});