diff --git a/lib/db2a.js b/lib/db2a.js index f3e43f9..232fa5c 100644 --- a/lib/db2a.js +++ b/lib/db2a.js @@ -347,7 +347,7 @@ const SQL_VARCHAR = 12; const SQL_BLOB = 13; const SQL_CLOB = 14; const SQL_DBCLOB = 15; -const SQL_DATALINK = 16; +const SQL_BOOLEAN = 16; const SQL_WCHAR = 17; const SQL_WVARCHAR = 18; const SQL_BIGINT = 19; @@ -362,6 +362,7 @@ const SQL_VARGRAPHIC = 96; const SQL_LONGVARGRAPHIC = SQL_VARGRAPHIC; const SQL_BINARY = -2; const SQL_VARBINARY = -3; +const SQL_DATALINK = -400; const SQL_LONGVARBINARY = SQL_VARBINARY; const SQL_DATE = 91; const SQL_TYPE_DATE = 91; @@ -1150,6 +1151,7 @@ module.exports.SQL_VARCHAR = SQL_VARCHAR; module.exports.SQL_BLOB = SQL_BLOB; module.exports.SQL_CLOB = SQL_CLOB; module.exports.SQL_DBCLOB = SQL_DBCLOB; +module.exports.SQL_BOOLEAN = SQL_BOOLEAN; module.exports.SQL_DATALINK = SQL_DATALINK; module.exports.SQL_WCHAR = SQL_WCHAR; module.exports.SQL_WVARCHAR = SQL_WVARCHAR; diff --git a/src/db2ia/dberror.h b/src/db2ia/dberror.h index d0737c2..a1a206f 100644 --- a/src/db2ia/dberror.h +++ b/src/db2ia/dberror.h @@ -9,6 +9,24 @@ #include #include "sqlcli.h" + +// SQL type code 16 is release-dependent at runtime: on IBM i 7.5+ the driver +// describes a BOOLEAN column as 16 and a DATALINK as -400, whereas on 7.4 and +// earlier 16 is DATALINK (there is no BOOLEAN type). The bundled sqlcli.h is +// stale on every release (it defines SQL_DATALINK as 16 and omits SQL_BOOLEAN), +// so the defines below normalise both constants to their 7.5+ runtime values. +// +// Consequence: these constants make code 16 mean BOOLEAN. On 7.4 a DATALINK +// column (described as 16) is therefore routed through the BOOLEAN path. This +// is intentional -- DATALINK is effectively unused and is not supported on 7.4. +#ifndef SQL_BOOLEAN +#define SQL_BOOLEAN 16 +#endif + +#ifdef SQL_DATALINK +#undef SQL_DATALINK +#endif +#define SQL_DATALINK -400 #include "napi.h" #define DEBUG(object, f_, ...) \ @@ -133,7 +151,9 @@ static const char* getSQLType(int sqlType) return "CLOB"; case SQL_DBCLOB: // SQL_DBCLOB = 15 return "DBCLOB"; - case SQL_DATALINK: // SQL_DATALINK = 16 + case SQL_BOOLEAN: // SQL_BOOLEAN = 16 (7.5+) + return "BOOLEAN"; + case SQL_DATALINK: // SQL_DATALINK = -400 (7.5+), was 16 (pre-7.5) return "DATALINK"; case SQL_WCHAR: // SQL_WCHAR = 17 return "WCHAR"; diff --git a/src/db2ia/dbstmt.cc b/src/db2ia/dbstmt.cc index 784c0d8..d521c1f 100644 --- a/src/db2ia/dbstmt.cc +++ b/src/db2ia/dbstmt.cc @@ -107,7 +107,7 @@ Napi::Object DbStmt::Init(Napi::Env env, Napi::Object exports) * function takes 0 or 1 argument. * info[0] (Boolean): true for ON false for OFF. * Returns: boolean true/false indicating the state of the debug switch. - * + * */ Napi::Value DbStmt::AsNumber(const Napi::CallbackInfo &info) { @@ -186,7 +186,7 @@ Napi::Value DbStmt::SetStmtAttr(const Napi::CallbackInfo &info) /* * DbStmt::GetStmtAttr * Description: Returns the current settings for the specified statement option - * Parameters: + * Parameters: * const Napi::CallbackInfo& info: * The information passed by Napi from the JavaScript call, including * arguments from the JavaScript function. In JavaScript, the exported @@ -194,7 +194,7 @@ Napi::Value DbStmt::SetStmtAttr(const Napi::CallbackInfo &info) * info[0] (Number): Attribute is the statement attribute to set. * Refer to the attribute table for more details. * Return: The attribute option in the format of a Number or a String. - * + * */ Napi::Value DbStmt::GetStmtAttr(const Napi::CallbackInfo &info) { @@ -275,7 +275,7 @@ class ExecAsyncWorker : public Napi::AsyncWorker // - SQL_ERROR // - SQL_INVALID_HANDLE // - SQL_NO_DATA_FOUND - // SQL_NO_DATA_FOUND is returned if the SQL statement is a Searched UPDATE + // SQL_NO_DATA_FOUND is returned if the SQL statement is a Searched UPDATE // or Searched DELETE and no rows satisfy the search condition. if (sqlReturnCode == SQL_SUCCESS_WITH_INFO) { @@ -744,7 +744,7 @@ class BindParamAsyncWorker : public Napi::AsyncWorker * Parameters: * const Napi::CallbackInfo& info: * The information passed by Napi from the JavaScript call. Contains 2 parameters, - * + * * info[0]: [Array]: An array of arrays, the inner arrays containing * the data to bind to the prepared statement. * info[1]: [Function]: The callback function, with @@ -896,7 +896,7 @@ class BindParametersAsyncWorker : public Napi::AsyncWorker * Parameters: * const Napi::CallbackInfo& info: * The information passed by Napi from the JavaScript call. Contains 2 parameters, - * + * * info[0]: [Array]: An array of the data to bind to the prepared statement. * info[1]: [Function]: The callback function, with * arguments passed to it in the format function(error): @@ -1134,7 +1134,7 @@ void DbStmt::Execute(const Napi::CallbackInfo &info) /* * DbStmt::ExecuteSync - * Syntex: executeSync(), executeSync(function(OutputParameters, error)) + * Syntex: executeSync(), executeSync(function(OutputParameters, error)) * Description: * Runs the "Execute" workflow synchronously, blocking the Node.js event * loop. Takes a statement prepared with "Prepare" and possibly bound @@ -1366,7 +1366,7 @@ class FetchAsyncWorker : public Napi::AsyncWorker * Syntex 1: fetch(function Callback(Row, ReturnCode/error)) * Syntex 2: fetch(int Orient, int Offset, function Callback(Row, ReturnCode/error)) * Description: - * Advances the cursor to the next row of the result set, and retrieves any bound columns. + * Advances the cursor to the next row of the result set, and retrieves any bound columns. * Or positions the cursor based on the requested orientation. * Parameters: * const Napi::CallbackInfo& info: @@ -2347,9 +2347,14 @@ int DbStmt::bindColData(Napi::Env env) sqlReturnCode = SQLBindCol(stmth, col + 1, SQL_C_CHAR, (SQLPOINTER)bindingRowInC[col], maxColLen, &dbColumn[col].rlength); } break; - default: // SQL_CHAR / SQL_VARCHAR + default: // SQL_CHAR / SQL_VARCHAR / SQL_BOOLEAN and other string-representable types { + // colPrecise * 4 + 1 accounts for multi-byte character expansion + null terminator. + // Minimum of 6 ensures types like BOOLEAN (colPrecise=1, but string representation + // "FALSE" needs 6 bytes) have a large enough buffer. maxColLen = dbColumn[col].colPrecise * 4 + 1; + if (maxColLen < 6) + maxColLen = 6; bindingRowInC[col] = (SQLCHAR *)calloc(maxColLen, sizeof(SQLCHAR)); sqlReturnCode = SQLBindCol(stmth, col + 1, SQL_C_CHAR, (SQLPOINTER)bindingRowInC[col], maxColLen, &dbColumn[col].rlength); } @@ -2430,8 +2435,12 @@ int DbStmt::fetchData() } else { + // rlength is the actual data length (e.g. 5 for CHAR(5) or "FALSE"). + // Allocate colLen + 1 to ensure null termination, since memcpy copies + // only the data bytes without a null terminator. The extra byte is + // zero-filled by calloc. colLen = dbColumn[col].rlength; - rowOfResultSetInC[col].data = (SQLCHAR *)calloc(colLen, sizeof(SQLCHAR)); + rowOfResultSetInC[col].data = (SQLCHAR *)calloc(colLen + 1, sizeof(SQLCHAR)); memcpy(rowOfResultSetInC[col].data, bindingRowInC[col], colLen * sizeof(SQLCHAR)); rowOfResultSetInC[col].rlength = colLen; } @@ -2467,6 +2476,16 @@ int DbStmt::buildJsObject(Napi::Env env, Napi::Array *array) }); break; } + case SQL_BOOLEAN: + { + // BOOLEAN comes back as the null-terminated string "TRUE" or "FALSE". + // The CLI reports the length indicator as SQL_NTS rather than an + // explicit byte count, so compare the string value instead of relying + // on rlength. NULL is already handled before this switch (JS null). + bool boolValue = (strcmp((const char *)resultSetInC[row][col].data, "TRUE") == 0); + value = Napi::Boolean::New(env, boolValue); + break; + } case SQL_SMALLINT: // -32768 to +32767 case SQL_INTEGER: // -2147483648 to +2147483647 // case SQL_BIGINT: // -9223372036854775808 to +9223372036854775807 @@ -2489,7 +2508,12 @@ int DbStmt::buildJsObject(Napi::Env env, Napi::Array *array) break; } default: - value = Napi::String::New(env, resultSetInC[row][col].data); + // Use the known data length to bound string creation rather than relying + // on null termination, as a safeguard against buffer overreads. + if (resultSetInC[row][col].rlength == SQL_NTS) + value = Napi::String::New(env, (const char *)resultSetInC[row][col].data); + else + value = Napi::String::New(env, (const char *)resultSetInC[row][col].data, resultSetInC[row][col].rlength); break; } } @@ -2587,7 +2611,7 @@ int DbStmt::bindParams(Napi::Env env, Napi::Array *params, std::string &error) error = "BIND INDICATOR TYPE OF PARAMETER " + std::to_string(i + 1) + " IS INVALID\n"; return -1; } - + param[i].io = io; bindIndicator = bindValue.ToNumber().Int32Value(); //convert from Napi::Value to an int @@ -2644,11 +2668,12 @@ int DbStmt::bindParams(Napi::Env env, Napi::Array *params, std::string &error) } else if (bindIndicator == 5 || value.IsBoolean()) { //Parameter is Boolean - bool *boolean = (bool *)malloc(sizeof(bool)); - *boolean = value.ToBoolean(); - param[i].valueType = SQL_C_BIT; - param[i].buf = boolean; - param[i].ind = 0; + // The IBM i CLI rejects SQL_C_BIT for a BOOLEAN parameter (HY003), so + // bind the value as the character string "TRUE"/"FALSE", which Db2 + // casts to BOOLEAN. strdup allocates exactly strlen+1 (freed in freeSp). + param[i].valueType = SQL_C_CHAR; + param[i].buf = strdup(value.ToBoolean() ? "TRUE" : "FALSE"); + param[i].ind = SQL_NTS; } else if (bindIndicator == SQL_BINARY || bindIndicator == SQL_BLOB || value.IsBuffer()) { //Parameter is blob/binary @@ -2682,11 +2707,12 @@ int DbStmt::bindParams(Napi::Env env, Napi::Array *params, std::string &error) } else if (value.IsBoolean()) { //Parameter is Boolean - bool *boolean = (bool *)malloc(sizeof(bool)); - *boolean = value.ToBoolean(); - param[i].valueType = SQL_C_BIT; - param[i].buf = boolean; - param[i].ind = 0; + // The IBM i CLI rejects SQL_C_BIT for a BOOLEAN parameter (HY003), so + // bind the value as the character string "TRUE"/"FALSE", which Db2 + // casts to BOOLEAN. strdup allocates exactly strlen+1 (freed in freeSp). + param[i].valueType = SQL_C_CHAR; + param[i].buf = strdup(value.ToBoolean() ? "TRUE" : "FALSE"); + param[i].ind = SQL_NTS; } else switch(param[i].paramType) { @@ -2752,7 +2778,7 @@ int DbStmt::bindParams(Napi::Env env, Napi::Array *params, std::string &error) { std::string string = value.ToString().Utf8Value(); const char *cString = string.c_str(); - if(strlen(cString) > 0) + if(strlen(cString) > 0) { strcpy((char *)param[i].buf, cString); param[i].ind = strlen(cString); @@ -2792,7 +2818,7 @@ int DbStmt::bindParams(Napi::Env env, Napi::Array *params, std::string &error) break; } } - + //link to doc https://www.ibm.com/support/knowledgecenter/en/ssw_ibm_i_73/cli/rzadpfnbndpm.htm sqlReturnCode = SQLBindParameter( stmth, //SQLHSTMT statement handle diff --git a/test/dataTypes.js b/test/dataTypes.js index da4458c..56ada6c 100755 --- a/test/dataTypes.js +++ b/test/dataTypes.js @@ -367,6 +367,187 @@ describe('Data Type Test', () => { // }); + describe('select char types', () => { + it('char', (done) => { + const sql = "select * from (values cast('ABCDE' as char(5))) as x (char_val)"; + dbStmt.exec(sql, (result, error) => { + expect(error).to.be.null; + expect(result).to.be.an('array'); + expect(result.length).to.be.greaterThan(0); + expect(Object.values(result[0])[0]).to.equal('ABCDE'); + done(); + }); + }); + + it('char with padding', (done) => { + const sql = "select * from (values cast('AB' as char(5))) as x (char_val)"; + dbStmt.exec(sql, (result, error) => { + expect(error).to.be.null; + expect(result).to.be.an('array'); + expect(result.length).to.be.greaterThan(0); + expect(Object.values(result[0])[0]).to.equal('AB '); + done(); + }); + }); + + it('varchar', (done) => { + const sql = "select * from (values cast('ABCDE' as varchar(10))) as x (varchar_val)"; + dbStmt.exec(sql, (result, error) => { + expect(error).to.be.null; + expect(result).to.be.an('array'); + expect(result.length).to.be.greaterThan(0); + expect(Object.values(result[0])[0]).to.equal('ABCDE'); + done(); + }); + }); + + it('char with 4-byte UTF-8 characters', (done) => { + // U+1F600 (😀) is 4 bytes in UTF-8. For CHAR(2), colPrecise=2, + // bind buffer = 2 * 4 + 1 = 9 bytes. Two 4-byte chars = 8 bytes + null = 9, + // which exactly fills the buffer, testing the null terminator boundary. + const emoji = '\u{1F600}\u{1F601}'; + const sql = 'select cast(? as char(8) ccsid 1208) as char_val from sysibm.sysdummy1'; + dbStmt.prepare(sql, (error) => { + if (error) { throw error; } + dbStmt.bindParameters([emoji], (error) => { + if (error) { throw error; } + dbStmt.execute((out, error) => { + if (error) { throw error; } + dbStmt.fetchAll((result, error) => { + if (error) { throw error; } + expect(result).to.be.an('array'); + expect(result.length).to.be.greaterThan(0); + expect(result[0].CHAR_VAL).to.equal(emoji); + done(); + }); + }); + }); + }); + }); + }); + + describe('select boolean type', () => { + it('boolean true', (done) => { + const sql = "select * from (values boolean('true')) as x (bool_val)"; + dbStmt.exec(sql, (result, error) => { + expect(error).to.be.null; + expect(result).to.be.an('array'); + expect(result.length).to.be.greaterThan(0); + expect(Object.values(result[0])[0]).to.equal(true); + done(); + }); + }); + + it('boolean false', (done) => { + const sql = "select * from (values boolean('false')) as x (bool_val)"; + dbStmt.exec(sql, (result, error) => { + expect(error).to.be.null; + expect(result).to.be.an('array'); + expect(result.length).to.be.greaterThan(0); + expect(Object.values(result[0])[0]).to.equal(false); + done(); + }); + }); + + it('boolean null', (done) => { + const sql = 'select * from (values cast(null as boolean)) as x (bool_val)'; + dbStmt.exec(sql, (result, error) => { + expect(error).to.be.null; + expect(result).to.be.an('array'); + expect(result.length).to.be.greaterThan(0); + expect(Object.values(result[0])[0]).to.be.null; + done(); + }); + }); + }); + + describe('bind boolean type', () => { + // Round-trips a JS boolean through a real BOOLEAN column: binds true/false/null + // as a parameter (write path) then reads it back (read path from a driver- + // described BOOLEAN column). The IBM i CLI rejects SQL_C_BIT for a BOOLEAN + // parameter, so bindParams binds the value as the string "TRUE"/"FALSE". + const user = (process.env.USER).toUpperCase(); + const table = `${user}.BOOLBIND`; + + before(() => { + const setup = new dbstmt(dbConn); + try { setup.execSync(`DROP TABLE ${table}`); } catch (e) { /* may not exist */ } + setup.execSync(`CREATE TABLE ${table} (ID INT, FLAG BOOLEAN)`); + setup.close(); + }); + + after(() => { + const cleanup = new dbstmt(dbConn); + try { cleanup.execSync(`DROP TABLE ${table}`); } catch (e) { /* ignore */ } + cleanup.close(); + }); + + function roundTrip(id, value, expected, done) { + dbStmt.prepare(`INSERT INTO ${table} (ID, FLAG) VALUES (?, ?)`, (error) => { + expect(error).to.be.null; + dbStmt.bindParameters([id, value], (error) => { + expect(error).to.be.null; + dbStmt.execute((out, error) => { + expect(error).to.be.null; + const reader = new dbstmt(dbConn); + reader.exec(`SELECT FLAG FROM ${table} WHERE ID = ${id}`, (result, error) => { + expect(error).to.be.null; + expect(result).to.be.an('array'); + expect(result.length).to.be.greaterThan(0); + expect(result[0].FLAG).to.equal(expected); + reader.close(); + done(); + }); + }); + }); + }); + } + + it('binds boolean true', (done) => { + roundTrip(1, true, true, done); + }); + + it('binds boolean false', (done) => { + roundTrip(2, false, false, done); + }); + + it('binds boolean null', (done) => { + roundTrip(3, null, null, done); + }); + }); + + describe('select datalink type', () => { + // SQL_DATALINK moved from 16 to -400 to make room for the true BOOLEAN type, + // so these tests guard against a DATALINK column being mis-typed. DB2 for i + // does not allow casting a character string to DATALINK (SQLCODE -461); a + // DATALINK value must be built with DLVALUE(). The driver returns it as the + // string URL via the default string-column path. + it('datalink with URL', (done) => { + const sql = "select dlvalue('http://example.com/file.txt') as datalink_val from sysibm.sysdummy1"; + dbStmt.exec(sql, (result, error) => { + expect(error).to.be.null; + expect(result).to.be.an('array'); + expect(result.length).to.be.greaterThan(0); + const value = Object.values(result[0])[0]; + expect(value).to.be.a('string'); + // DB2 normalizes the URL scheme and host to uppercase. + expect(value.toLowerCase()).to.equal('http://example.com/file.txt'); + done(); + }); + }); + + it('datalink null', (done) => { + const sql = 'select * from (values cast(null as datalink)) as x (datalink_val)'; + dbStmt.exec(sql, (result, error) => { + expect(error).to.be.null; + expect(result).to.be.an('array'); + expect(result.length).to.be.greaterThan(0); + expect(Object.values(result[0])[0]).to.be.null; + done(); + }); + }); + }); + describe('exec read blob test', () => { it('performs action of given SQL String', (done) => { const sql = 'SELECT CAST(\'test\' AS BLOB(10k)) FROM SYSIBM.SYSDUMMY1'; diff --git a/test/misc.js b/test/misc.js index 046ea0c..60d9b14 100644 --- a/test/misc.js +++ b/test/misc.js @@ -69,18 +69,18 @@ describe('Misc Test', () => { describe('The stored procedure with result set issue', () => { let user = (process.env.USER).toUpperCase(); let sql = `CALL ${user}.SPWITHRS`; - let crtSP = `CREATE OR REPLACE PROCEDURE ${user}.SPWITHRS() - LANGUAGE SQL + let crtSP = `CREATE OR REPLACE PROCEDURE ${user}.SPWITHRS() + LANGUAGE SQL DYNAMIC RESULT SETS 1 - BEGIN ATOMIC + BEGIN ATOMIC DECLARE C1 CURSOR FOR SELECT * FROM QIWS.QCUSTCDT LIMIT 5; OPEN C1 ; - + SET RESULT SETS WITH RETURN TO CLIENT CURSOR C1 ; - + END`; before((done) => { @@ -159,5 +159,15 @@ describe('Misc Test', () => { }); }); }); + + describe('SQL type constants', () => { + it('SQL_BOOLEAN should be defined with value 16', () => { + expect(db2a.SQL_BOOLEAN).to.equal(16); + }); + + it('SQL_DATALINK should be defined with value -400', () => { + expect(db2a.SQL_DATALINK).to.equal(-400); + }); + }); }); });