From d909a3afeb48eaf8f4ef5e124a99c66f6e696baa Mon Sep 17 00:00:00 2001 From: Sharad Chandran R Date: Wed, 22 May 2024 16:07:30 +0530 Subject: [PATCH] Fixed issue which throws an error, when the same SELECT SQL statement is run for the second time with a different bind type. (Issue #1669) --- doc/src/release_notes.rst | 3 ++ lib/errors.js | 12 +++++ lib/thin/protocol/constants.js | 1 + lib/thin/protocol/messages/withData.js | 13 ++++- lib/thin/protocol/protocol.js | 4 ++ lib/thin/statement.js | 4 ++ lib/thin/statementCache.js | 5 ++ test/binding.js | 68 ++++++++++++++++++++++++++ test/dmlReturning.js | 24 +++++++++ test/list.txt | 2 + test/plsqlBindCursorsIN.js | 32 ++++++++++++ 11 files changed, 166 insertions(+), 2 deletions(-) diff --git a/doc/src/release_notes.rst b/doc/src/release_notes.rst index bc410830..11db148e 100644 --- a/doc/src/release_notes.rst +++ b/doc/src/release_notes.rst @@ -12,6 +12,9 @@ node-oracledb `v6.5.1 `__. #) Fixed exponent check condition for out-of-bounds number. See `Issue #1659 `__. diff --git a/lib/errors.js b/lib/errors.js index 4fc655f9..4f983a77 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -31,6 +31,17 @@ const util = require('util'); // define error prefix for all messages const ERR_PREFIX = "NJS"; +const ERR_INTEGRITY_ERROR_CODES = [ + 1, // unique constraint violated + 1400, // cannot insert NULL + 1438, // value larger than specified precision + 2290, // check constraint violated + 2291, // integrity constraint violated - parent key not found + 2292, // integrity constraint violated - child record found + 21525, // attribute or collection element violated its constraints + 40479, // internal JSON serializer error +]; + // define error number constants (used in JavaScript library) const ERR_INVALID_POOL = 2; const ERR_INVALID_CONNECTION = 3; @@ -695,6 +706,7 @@ function transformErr(err, fnOpt) { // define exports module.exports = { + ERR_INTEGRITY_ERROR_CODES, ERR_INVALID_POOL, ERR_INVALID_CONNECTION, ERR_INVALID_PROPERTY_VALUE, diff --git a/lib/thin/protocol/constants.js b/lib/thin/protocol/constants.js index 7ee66a51..70855853 100644 --- a/lib/thin/protocol/constants.js +++ b/lib/thin/protocol/constants.js @@ -724,6 +724,7 @@ module.exports = { TNS_XML_TYPE_FLAG_SKIP_NEXT_4: 0x100000, // errors + TNS_ERR_INCONSISTENT_DATA_TYPES: 932, TNS_ERR_VAR_NOT_IN_SELECT_LIST: 1007, TNS_ERR_INBAND_MESSAGE: 12573, TNS_ERR_INVALID_SERVICE_NAME: 12514, diff --git a/lib/thin/protocol/messages/withData.js b/lib/thin/protocol/messages/withData.js index 46277090..10a63b75 100644 --- a/lib/thin/protocol/messages/withData.js +++ b/lib/thin/protocol/messages/withData.js @@ -113,9 +113,18 @@ class MessageWithData extends Message { this.errorInfo.num = 0; this.errorOccurred = false; this.statement.moreRowsToFetch = false; + } else if (this.retry) { + this.retry = false; + } else if (this.statement.isQuery && + (this.errorInfo.num === constants.TNS_ERR_VAR_NOT_IN_SELECT_LIST + || this.errorInfo.num === constants.TNS_ERR_INCONSISTENT_DATA_TYPES)) { + this.retry = true; + this.connection.statementCache.clearCursor(this.statement); } else if (this.errorInfo.num !== 0 && this.errorInfo.cursorId !== 0) { - this.connection.statementCache._cachedStatements.delete(this.statement.sql); - this.statement.returnToCache = false; + if (!errors.ERR_INTEGRITY_ERROR_CODES.includes(this.errorInfo.num)) { + this.connection.statementCache.clearCursor(this.statement); + this.statement.returnToCache = false; + } } if (this.errorInfo.batchErrors) { this.errorOccurred = false; diff --git a/lib/thin/protocol/protocol.js b/lib/thin/protocol/protocol.js index 94f3d6b2..87c46fb0 100644 --- a/lib/thin/protocol/protocol.js +++ b/lib/thin/protocol/protocol.js @@ -174,6 +174,10 @@ class Protocol { if (callTimeoutExpired) { errors.throwErr(errors.ERR_CALL_TIMEOUT_EXCEEDED, this.callTimeout); } + if (message.retry) { + message.errorOccurred = false; + return await this._processMessage(message); + } let err = new Error(message.errorInfo.message); err.offset = message.errorInfo.pos; err.errorNum = message.errorInfo.num; diff --git a/lib/thin/statement.js b/lib/thin/statement.js index 4ee068bb..8be16050 100644 --- a/lib/thin/statement.js +++ b/lib/thin/statement.js @@ -29,6 +29,7 @@ const { Buffer } = require('buffer'); const constants = require('../constants'); const errors = require('../errors'); +const protoConstants = require('./protocol/constants'); /** * It is used to cache the metadata about bind information @@ -519,6 +520,9 @@ class Statement { // cursor also requires a full execute. //--------------------------------------------------------------------------- _setVariable(bindInfo, variable) { + if (variable.type._oraTypeNum === protoConstants.TNS_DATA_TYPE_CURSOR) { + this.requiresFullExecute = true; + } if (variable.maxSize !== bindInfo.maxSize || variable.dir !== bindInfo.dir || variable.isArray !== bindInfo.isArray diff --git a/lib/thin/statementCache.js b/lib/thin/statementCache.js index 9c40b2c1..c26e96f6 100644 --- a/lib/thin/statementCache.js +++ b/lib/thin/statementCache.js @@ -146,6 +146,11 @@ class StatementCache { return stmt; } + clearCursor(statement) { + this._addCursorToClose(statement); + statement.cursorId = 0; + } + //--------------------------------------------------------------------------- // returnStatement() // Return the statement to the statement cache, if applicable. If the diff --git a/test/binding.js b/test/binding.js index ce06ceeb..74465703 100755 --- a/test/binding.js +++ b/test/binding.js @@ -1119,4 +1119,72 @@ describe('4. binding.js', function() { } }); }); + + describe('4.15 binding different data types for same sql', function() { + let connection; + let sysDBAConn; + let sid; + const numIters = 40; + + before(async function() { + if (!dbConfig.test.DBA_PRIVILEGE) this.skip(); + const dbaConfig = { + user: dbConfig.test.DBA_user, + password: dbConfig.test.DBA_password, + connectionString: dbConfig.connectString, + privilege: oracledb.SYSDBA + }; + sysDBAConn = await oracledb.getConnection(dbaConfig); + connection = await oracledb.getConnection(dbConfig); + sid = await testsUtil.getSid(connection); + }); + + after(async function() { + if (connection) { + await connection.close(); + } + if (sysDBAConn) { + await sysDBAConn.close(); + } + }); + + it('4.15.1 change bindtypes using bindByPosition for queries', + async function() { + const sql = 'SELECT :1 FROM DUAL'; + const dt = new Date(); + const openCount = await testsUtil.getOpenCursorCount(sysDBAConn, sid); + for (let i = 0; i < numIters; i++) { + let result = await connection.execute(sql, [1]); + assert.strictEqual(result.rows[0][0], 1); + result = await connection.execute(sql, [dt]); + assert.deepStrictEqual(result.rows[0][0], dt); + result = await connection.execute(sql, [2]); + assert.strictEqual(result.rows[0][0], 2); + } + const newOpenCount = await testsUtil. + getOpenCursorCount(sysDBAConn, sid); + + // ensure cursors are not linearly opened as numIters causing leak. + assert(newOpenCount - openCount < 4); + }); + + it('4.15.2 change bindtypes using bindByName for queries', + async function() { + const sql = 'SELECT :x FROM DUAL'; + const openCount = await testsUtil.getOpenCursorCount(sysDBAConn, sid); + const dt = new Date(); + for (let i = 0; i < numIters; i++) { + let result = await connection.execute(sql, {x: 1}); + assert.strictEqual(result.rows[0][0], 1); + result = await connection.execute(sql, {x: dt}); + assert.deepStrictEqual(result.rows[0][0], dt); + result = await connection.execute(sql, {x: 2}); + assert.strictEqual(result.rows[0][0], 2); + } + const newOpenCount = await testsUtil.getOpenCursorCount(sysDBAConn, sid); + + // ensure cursors are not linearly opened as numIters causing leak. + assert(newOpenCount - openCount < 4); + }); + }); }); diff --git a/test/dmlReturning.js b/test/dmlReturning.js index 88b330c8..287b7994 100644 --- a/test/dmlReturning.js +++ b/test/dmlReturning.js @@ -515,5 +515,29 @@ describe('6. dmlReturning.js', function() { // NJS-011: encountered bind value and type mismatch }); + it('6.2.10 INSERT statement, check re-execute by changing the out bindtype', async function() { + let id = 50; + let sql = `INSERT INTO ${tableName} VALUES (${id}, TO_DATE('2015-01-11','YYYY-DD-MM')) + RETURNING num, content INTO :rnum, :rcontent`; + const dt = new Date('2015-11-01 00:00:00'); + const bindVar = + { + rnum: { type: oracledb.NUMBER, dir: oracledb.BIND_OUT }, + rcontent: { type: oracledb.DATE, dir: oracledb.BIND_OUT } + }; + + // outbind as date. + let result = await connection.execute(sql, bindVar); + assert.strictEqual(result.outBinds.rcontent[0].getTime(), dt.getTime()); + + // Change outbind type to string + bindVar.rcontent.type = oracledb.STRING; + id = 51; + sql = `INSERT INTO ${tableName} VALUES (${id}, TO_DATE('2015-01-11','YYYY-DD-MM')) + RETURNING num, content INTO :rnum, :rcontent`; + result = await connection.execute(sql, bindVar); + assert.strictEqual(result.outBinds.rcontent[0], '01-NOV-15'); + }); + }); // 6.2 }); diff --git a/test/list.txt b/test/list.txt index f90d1ca5..cd0037b7 100755 --- a/test/list.txt +++ b/test/list.txt @@ -222,6 +222,7 @@ Overview of node-oracledb functional tests 6.2.7 DELETE statement, single row matched, Object binding format 6.2.8 DELETE statement, multiple rows matched, Array binding format 6.2.9 Negative test - bind value and type mismatch + 6.2.10 INSERT statement, check re-execute by changing the out bindtype 7. autoCommit.js 7.1 autoCommit takes effect when setting oracledb.autoCommit before connecting @@ -4804,6 +4805,7 @@ oracledb.OUT_FORMAT_OBJECT and resultSet = true 239.4 implicit binding type 239.5 check REF CURSOR round-trips with no prefetching 239.6 check REF CURSOR round-trips with prefetching + 239.7 check REF CURSOR bind with re-execute 240. errorOffset.js 240.1 checks the offset value of the error diff --git a/test/plsqlBindCursorsIN.js b/test/plsqlBindCursorsIN.js index a106a66b..b8167bd0 100644 --- a/test/plsqlBindCursorsIN.js +++ b/test/plsqlBindCursorsIN.js @@ -263,4 +263,36 @@ describe('239. plsqlBindCursorsIN.js', () => { ((oracledb.thin) ? assert.strictEqual(rt, 3) : assert.strictEqual(rt, 2)); }); // 239.6 + + it('239.7 check REF CURSOR bind with re-execute ', async () => { + const refCursorOptions = { + resultSet: true, + }; + let result = await conn.execute(sqlRefCursor, [], refCursorOptions); + + const plsql = `begin ${procName1}(:bv); end;`; + await conn.execute( + plsql, + { + bv: {val: result.resultSet, type: oracledb.CURSOR, dir: oracledb.BIND_IN, maxSize: 10 } + } + ); + result = await conn.execute(sqlRefCursor, [], refCursorOptions); + + // Check Re-execute. + await conn.execute( + plsql, + { + bv: {val: result.resultSet, type: oracledb.CURSOR, dir: oracledb.BIND_IN, maxSize: 10 } + } + ); + + await result.resultSet.close(); + + const sqlQuery = `select * from ${tableName}`; + const queryResult = await conn.execute(sqlQuery); + + assert.strictEqual(queryResult.rows.length, 3); + }); // 239.7 + });