Skip to content

Commit 408e651

Browse files
committed
Add support to fetch XML as a String in Thin mode
1 parent 8a2973a commit 408e651

File tree

9 files changed

+140
-30
lines changed

9 files changed

+140
-30
lines changed

doc/src/release_notes.rst

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ Common Changes
2525

2626
#) Added constant ``oracledb.DB_TYPE_XMLTYPE`` to represent data of type
2727
``SYS.XMLTYPE`` in metadata ``fetchType`` and ``dbType`` attributes.
28-
Previously the constant used was ``oracledb.DB_TYPE_LONG`` in Thick mode.
28+
Previously the constant used was ``oracledb.DB_TYPE_LONG`` in Thick mode
29+
and ``oracledb.DB_TYPE_OBJECT`` in Thin mode.
2930

3031
#) Added support for using the Azure and Oracle Cloud Infrastructure (OCI)
3132
Software Development Kits (SDKs) to generate
@@ -59,6 +60,10 @@ Common Changes
5960
Thin Mode Changes
6061
++++++++++++++++++
6162

63+
#) Added support for fetching SYS.XMLTYPE data as strings. Note that unlike in
64+
Thick mode, fetching longer values does not require using
65+
``XMLTYPE.GETCLOBVAL()``.
66+
6267
#) Fixed bug in parsing SQL statements containing multi-line comments
6368
with multiple asterisks before the closing slash.
6469
`Issue #1625 <https://github.com/oracle/node-oracledb/issues/1625>`__.

doc/src/user_guide/appendix_a.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -764,7 +764,7 @@ when binding numeric values.
764764
- Thick mode only
765765
* - XMLType
766766
- DB_TYPE_XMLTYPE
767-
- Thick mode only
767+
- Yes
768768

769769
.. _testingmode:
770770

lib/errors.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ const ERR_WRONG_VALUE_FOR_DBOBJECT_ELEM = 135;
136136
const ERR_WRONG_CRED_FOR_EXTAUTH = 136;
137137
const ERR_MISSING_BIND_VALUE = 137;
138138
const ERR_SERVER_VERSION_NOT_SUPPORTED = 138;
139+
const ERR_UNEXPECTED_XML_TYPE = 139;
139140

140141
// Oracle Net layer errors start from 500
141142
const ERR_CONNECTION_CLOSED = 500;
@@ -393,6 +394,8 @@ messages.set(ERR_MISSING_BIND_VALUE, // NJS-137
393394
'a bind variable replacement value for placeholder ":%s" was not provided');
394395
messages.set(ERR_SERVER_VERSION_NOT_SUPPORTED, // NJS-138
395396
'connections to this database server version are not supported by node-oracledb in Thin mode');
397+
messages.set(ERR_UNEXPECTED_XML_TYPE, // NJS-139
398+
'unexpected XML type with flag %d');
396399

397400
// Oracle Net layer errors
398401

@@ -772,6 +775,7 @@ module.exports = {
772775
ERR_WRONG_CRED_FOR_EXTAUTH,
773776
ERR_MISSING_BIND_VALUE,
774777
ERR_SERVER_VERSION_NOT_SUPPORTED,
778+
ERR_UNEXPECTED_XML_TYPE,
775779
ERR_CONNECTION_CLOSED_CODE: `${ERR_PREFIX}-${ERR_CONNECTION_CLOSED}`,
776780
assert,
777781
assertArgCount,

lib/impl/resultset.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,17 @@ class ResultSetImpl {
147147
} else if (metadata.dbType === types.DB_TYPE_RAW) {
148148
converter = (v) => (v === null) ? null : v.toString('hex').toUpperCase();
149149
}
150+
} else if (metadata.dbType === types.DB_TYPE_XMLTYPE) {
151+
const xmlConverter = async function(val) {
152+
if (!val) {
153+
return val;
154+
}
155+
if (typeof val === 'string') {
156+
return val;
157+
}
158+
return await val.getData();
159+
};
160+
converter = xmlConverter;
150161
}
151162
if (userConverter && converter) {
152163
const internalConverter = converter;

lib/thin/dbObject.js

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const errors = require('../errors.js');
3232
const types = require('../types.js');
3333
const DbObjectImpl = require('../impl/dbObject.js');
3434
const { GrowableBuffer } = require('./protocol/buffer.js');
35+
const ThinLobImpl = require('./lob.js');
3536

3637
class DbObjectPickleBuffer extends GrowableBuffer {
3738

@@ -652,4 +653,37 @@ class ThinDbObjectImpl extends DbObjectImpl {
652653

653654
}
654655

655-
module.exports = ThinDbObjectImpl;
656+
//---------------------------------------------------------------------------
657+
// readXML()
658+
//
659+
// Decodes the raw Bytes to XML string or LOB object.
660+
//
661+
//---------------------------------------------------------------------------
662+
function readXML(conn, buf) {
663+
let colValue;
664+
665+
const xmlObj = new DbObjectPickleBuffer(buf);
666+
const tempobj = {};
667+
xmlObj.readHeader(tempobj);
668+
xmlObj.skipBytes(1);
669+
const xmlflag = xmlObj.readUInt32BE();
670+
if (xmlflag & constants.TNS_XML_TYPE_FLAG_SKIP_NEXT_4) {
671+
xmlObj.skipBytes(4);
672+
}
673+
const numBytesLeft = xmlObj.numBytesLeft();
674+
const ptr = xmlObj.readBytes(numBytesLeft);
675+
if (xmlflag & constants.TNS_XML_TYPE_STRING) {
676+
colValue = ptr.toString();
677+
} else if (xmlflag & constants.TNS_XML_TYPE_LOB) {
678+
const lobImpl = new ThinLobImpl();
679+
const locator = Buffer.from(ptr);
680+
lobImpl.init(conn, locator, types.DB_TYPE_CLOB, 0, 0);
681+
colValue = lobImpl;
682+
} else {
683+
// We only support String and Clob type.
684+
errors.throwErr(errors.ERR_UNEXPECTED_XML_TYPE, xmlflag);
685+
}
686+
return colValue;
687+
}
688+
689+
module.exports = { ThinDbObjectImpl, readXML };

lib/thin/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const ThinConnectionImpl = require('./connection.js');
3030
const ThinResultSetImpl = require('./resultSet.js');
3131
const ThinPoolImpl = require('./pool.js');
3232
const ThinLobImpl = require('./lob.js');
33-
const ThinDbObjectImpl = require('./dbObject.js');
33+
const { ThinDbObjectImpl } = require('./dbObject.js');
3434

3535
const impl = require('../impl');
3636
impl.ConnectionImpl = ThinConnectionImpl;

lib/thin/protocol/messages/withData.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const { Buffer } = require('buffer');
3030
const utils = require("../utils");
3131
const constants = require("../constants.js");
3232
const Message = require("./base.js");
33-
const ThinDbObjectImpl = require("../../dbObject.js");
33+
const { ThinDbObjectImpl, readXML } = require("../../dbObject.js");
3434
const ThinLobImpl = require("../../lob.js");
3535
const errors = require('../../../errors');
3636
const types = require('../../../types.js');
@@ -277,6 +277,9 @@ class MessageWithData extends Message {
277277
if (fetchInfo.dbTypeClass.partial) {
278278
this.connection._partialDbObjectTypes.push(fetchInfo.dbTypeClass);
279279
}
280+
if (fetchInfo.dbTypeClass.isXmlType) {
281+
fetchInfo.dbType = types.DB_TYPE_XMLTYPE;
282+
}
280283
break;
281284
default:
282285
break;
@@ -483,9 +486,14 @@ class MessageWithData extends Message {
483486
if (obj.packedData) {
484487
const objType = (variable.fetchInfo) ? variable.fetchInfo.dbTypeClass :
485488
variable.typeClass;
486-
colValue = new ThinDbObjectImpl(objType, obj.packedData);
487-
colValue.toid = obj.toid;
488-
colValue.oid = obj.oid;
489+
490+
if (variable.type === types.DB_TYPE_XMLTYPE) {
491+
colValue = readXML(this.connection, obj.packedData);
492+
} else {
493+
colValue = new ThinDbObjectImpl(objType, obj.packedData);
494+
colValue.toid = obj.toid;
495+
colValue.oid = obj.oid;
496+
}
489497
}
490498
} else {
491499
errors.throwErr(errors.ERR_UNSUPPORTED_DATA_TYPE, dbType.num,

test/dataTypeXML.js

Lines changed: 63 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,6 @@ describe('181. dataTypeXML.js', function() {
5454
'</Warehouse>\n';
5555

5656
before('create table and insert a row', async function() {
57-
if (oracledb.thin) {
58-
this.skip();
59-
}
6057

6158
const connection = await oracledb.getConnection(dbConfig);
6259

@@ -74,8 +71,9 @@ describe('181. dataTypeXML.js', function() {
7471
" EXECUTE IMMEDIATE (' \n" +
7572
" CREATE TABLE " + tableName + " ( \n" +
7673
" num number(9) not null, \n" +
77-
" content xmltype not null \n" +
78-
" ) \n" +
74+
" content xmltype not null, \n" +
75+
" embedLOBXML xmltype \n" +
76+
" ) XMLTYPE embedLOBXML STORE AS CLOB\n" +
7977
" '); \n" +
8078
"END; ";
8179
await connection.execute(sql);
@@ -106,19 +104,17 @@ describe('181. dataTypeXML.js', function() {
106104
it('181.1 basic case, insert XML data and query back', async () => {
107105
const conn = await oracledb.getConnection(dbConfig);
108106
const sql = "select content from " + tableName + " where num = :id";
109-
const expectedMetadata = {
110-
name: 'CONTENT',
111-
dbType: oracledb.DB_TYPE_XMLTYPE,
112-
nullable: false,
113-
isJson: false,
114-
dbTypeName: 'XMLTYPE',
115-
fetchType: oracledb.DB_TYPE_XMLTYPE
116-
};
117107
const bindVar = { id: testRowID };
118108
const options = { outFormat: oracledb.OUT_FORMAT_OBJECT };
119-
const result = await conn.execute(sql, bindVar, options);
120-
assert.strictEqual(result.rows[0].CONTENT, testXMLData);
121-
assert.deepStrictEqual(result.metaData[0], expectedMetadata);
109+
110+
// test execute and re-execute
111+
for (let i = 0; i < 2; i++) {
112+
const result = await conn.execute(sql, bindVar, options);
113+
assert.strictEqual(result.rows[0].CONTENT, testXMLData);
114+
assert.strictEqual(result.metaData[0].dbType, oracledb.DB_TYPE_XMLTYPE);
115+
assert.strictEqual(result.metaData[0].fetchType, oracledb.DB_TYPE_XMLTYPE);
116+
}
117+
122118
await conn.close();
123119
}); // 181.1
124120

@@ -182,9 +178,7 @@ describe('181. dataTypeXML.js', function() {
182178
await conn.close();
183179
}); // 181.4
184180

185-
// ORA-19011: Character string buffer too small
186-
it.skip('181.5 inserts data that is larger than 4K', async () => {
187-
181+
it('181.5 inserts data that is larger than 4K', async () => {
188182
const ID = 50;
189183
const str = 'a'.repeat(31 * 1024);
190184
const head = '<data>', tail = '</data>\n';
@@ -197,13 +191,60 @@ describe('181. dataTypeXML.js', function() {
197191
let result = await conn.execute(sql, bindValues);
198192
assert.strictEqual(result.rowsAffected, 1);
199193

200-
sql = "select content from " + tableName + " where num = :id";
201194
const bindVar = { id: ID };
202195
const options = { outFormat: oracledb.OUT_FORMAT_OBJECT };
203-
result = await conn.execute(sql, bindVar, options);
204-
assert.strictEqual(result.rows[0].CONTENT, xml);
196+
if (oracledb.thin) {
197+
sql = "select content from " + tableName + " where num = :id";
198+
result = await conn.execute(sql, bindVar, options);
199+
assert.strictEqual(result.rows[0].CONTENT, xml);
200+
} else {
201+
// For thick, we get an error ORA-19011: Character string buffer too small, so use getclobval
202+
options.fetchInfo = { "OBJ": { type: oracledb.STRING } };
203+
sql = "select xmltype.getclobval(content) as obj from " + tableName + " where num = :id";
204+
result = await conn.execute(sql, bindVar, options);
205+
assert.strictEqual(result.rows[0].OBJ, xml);
206+
}
205207
await conn.commit();
206208
await conn.close();
207209
}); // 181.5
208210

211+
it('181.6 Insert and Verify Embedded LOB locator inside XML', async () => {
212+
const ID = 51;
213+
const largestr = 'a'.repeat(64 * 1024);
214+
const smallstr = 'a'.repeat(1 * 1024);
215+
const head = '<data>', tail = '</data>\n';
216+
const xml = head.concat(smallstr).concat(tail);
217+
const largexml = head.concat(largestr).concat(tail);
218+
219+
const conn = await oracledb.getConnection(dbConfig);
220+
const lob = await conn.createLob(oracledb.CLOB);
221+
await lob.write(largexml);
222+
223+
let sql = `insert into ${tableName} ( num, content, embedLOBXML )
224+
values(:id, XMLType(:bv1), XMLType(:bv2))`;
225+
const bindValues = { id: ID, bv1: xml, bv2: {type: oracledb.CLOB, val: lob }};
226+
let result = await conn.execute(sql, bindValues);
227+
assert.strictEqual(result.rowsAffected, 1);
228+
229+
const bindVar = { id: ID };
230+
const options = { outFormat: oracledb.OUT_FORMAT_OBJECT };
231+
232+
if (oracledb.thin) {
233+
sql = "select embedlobxml as OBJ from " + tableName + " where num = :id";
234+
} else {
235+
options.fetchInfo = {"OBJ": {type: oracledb.STRING}};
236+
sql = "select xmltype.getclobval(embedlobxml) as OBJ from " + tableName + " where num = :id";
237+
}
238+
result = await conn.execute(sql, bindVar, options);
239+
assert.strictEqual(result.rows[0].OBJ, largexml);
240+
241+
// free temp lob
242+
await new Promise((resolve) => {
243+
lob.once('close', resolve);
244+
lob.destroy();
245+
});
246+
await conn.commit();
247+
await conn.close();
248+
}); // 181.6
249+
209250
});

test/dbObject1.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,14 @@ describe('200. dbObject1.js', () => {
120120
assert.strictEqual(result.rowsAffected, 1);
121121
await conn.commit();
122122

123+
const options = {};
123124
sql = `SELECT * FROM ${TABLE} WHERE num = ${seq}`;
125+
options.fetchInfo = {"PERSON": { type: oracledb.STRING }};
126+
await assert.rejects(
127+
async () => await conn.execute(sql, [], options),
128+
/NJS-119:/
129+
);
130+
124131
result = await conn.execute(sql);
125132

126133
assert.strictEqual(result.rows[0][0], seq);

0 commit comments

Comments
 (0)