Added JsonId class to to represent JSON ID values returned by SODA in Oracle Database 23ai and higher in the _id attribute of documents stored in native SODA collections
This commit is contained in:
parent
63d8bb5a0b
commit
10c45fcb01
|
@ -10,6 +10,13 @@ For deprecated and desupported features, see :ref:`Deprecations and desupported
|
|||
node-oracledb `v6.5.0 <https://github.com/oracle/node-oracledb/compare/v6.4.0...v6.5.0>`__ (TBD)
|
||||
--------------------------------------------------------------------------------------------------------
|
||||
|
||||
Common Changes
|
||||
++++++++++++++
|
||||
|
||||
#) Added class :ref:`oracledb.JsonId <jsonid>` to represent JSON ID values
|
||||
returned by SODA in Oracle Database 23.4 and higher in the ``_id``
|
||||
attribute of documents stored in native collections.
|
||||
|
||||
Thin Mode Changes
|
||||
++++++++++++++++++
|
||||
|
||||
|
|
|
@ -79,6 +79,7 @@ module.exports = {
|
|||
TNS_JSON_TYPE_ARRAY: 0xc0,
|
||||
TNS_JSON_TYPE_EXTENDED: 0x7b,
|
||||
TNS_JSON_TYPE_VECTOR: 0x01,
|
||||
TNS_JSON_TYPE_ID: 0x7e,
|
||||
|
||||
// timezone offsets
|
||||
TZ_HOUR_OFFSET: 20,
|
||||
|
|
|
@ -143,6 +143,11 @@ class OsonDecoder extends BaseBuffer {
|
|||
return this.readBytes(this.readUInt32BE()).toString();
|
||||
} else if (nodeType === constants.TNS_JSON_TYPE_NUMBER_LENGTH_UINT8) {
|
||||
return parseFloat(this.readOracleNumber());
|
||||
} else if (nodeType === constants.TNS_JSON_TYPE_ID) {
|
||||
const buf = this.readBytes(this.readUInt8());
|
||||
const jsonId = new types.JsonId(buf.length);
|
||||
buf.copy(jsonId);
|
||||
return jsonId;
|
||||
} else if (nodeType === constants.TNS_JSON_TYPE_BINARY_LENGTH_UINT16) {
|
||||
return Buffer.from(this.readBytes(this.readUInt16BE()));
|
||||
} else if (nodeType === constants.TNS_JSON_TYPE_BINARY_LENGTH_UINT32) {
|
||||
|
@ -562,6 +567,11 @@ class OsonTreeSegment extends GrowableBuffer {
|
|||
this.writeUInt32BE(buf.length);
|
||||
this.writeBytes(buf);
|
||||
|
||||
} else if (value instanceof types.JsonId) {
|
||||
this.writeUInt8(constants.TNS_JSON_TYPE_ID);
|
||||
this.writeUInt8(value.length);
|
||||
this.writeBytes(Buffer.from(value.buffer));
|
||||
|
||||
// handle objects
|
||||
} else {
|
||||
this._encodeObject(value, fnamesSeg);
|
||||
|
|
|
@ -832,6 +832,7 @@ module.exports = {
|
|||
AqQueue,
|
||||
BaseDbObject,
|
||||
Connection,
|
||||
JsonId: types.JsonId,
|
||||
Lob,
|
||||
Pool,
|
||||
PoolStatistics,
|
||||
|
|
|
@ -63,6 +63,7 @@ class Settings {
|
|||
this.thinDriverInitialized = false;
|
||||
this.createFetchTypeMap(this.fetchAsString, this.fetchAsBuffer);
|
||||
this.fetchTypeHandler = undefined;
|
||||
this._JsonId = types.JsonId;
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
|
|
|
@ -105,6 +105,12 @@ function transformJsonValue(value) {
|
|||
if (value instanceof BaseDbObject)
|
||||
return {fields: [], values: []};
|
||||
|
||||
// JsonId is a special type to represent autogenerated id
|
||||
// for SODA documents.
|
||||
if (value instanceof types.JsonId) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// all other objects are transformed to an object with two arrays (fields
|
||||
// and values)
|
||||
const outValue = {};
|
||||
|
|
10
lib/types.js
10
lib/types.js
|
@ -332,6 +332,13 @@ dbTypeByColumnTypeName.set("SMALLINT", DB_TYPE_NUMBER);
|
|||
dbTypeByColumnTypeName.set("TIMESTAMP WITH LOCAL TZ", DB_TYPE_TIMESTAMP_LTZ);
|
||||
dbTypeByColumnTypeName.set("TIMESTAMP WITH TZ", DB_TYPE_TIMESTAMP_TZ);
|
||||
|
||||
// It abstracts the autogenerated SODA Document key.
|
||||
class JsonId extends Uint8Array {
|
||||
toJSON() {
|
||||
return (Buffer.from(this.buffer).toString('hex'));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DbType,
|
||||
DB_TYPE_BFILE,
|
||||
|
@ -368,5 +375,6 @@ module.exports = {
|
|||
DB_TYPE_XMLTYPE,
|
||||
getTypeByColumnTypeName,
|
||||
getTypeByNum,
|
||||
getTypeByOraTypeNum
|
||||
getTypeByOraTypeNum,
|
||||
JsonId
|
||||
};
|
||||
|
|
|
@ -426,9 +426,11 @@ bool njsBaton_getJsonNodeValue(njsBaton *baton, dpiJsonNode *node,
|
|||
napi_value key, temp;
|
||||
dpiJsonArray *array;
|
||||
dpiJsonObject *obj;
|
||||
void *destData = NULL;
|
||||
size_t byteLength = 0;
|
||||
double temp_double;
|
||||
uint32_t i;
|
||||
napi_value global, vectorBytes;
|
||||
napi_value global, vectorBytes, arrBuf;
|
||||
|
||||
// null is a special case
|
||||
if (node->nativeTypeNum == DPI_NATIVE_TYPE_NULL) {
|
||||
|
@ -493,6 +495,15 @@ bool njsBaton_getJsonNodeValue(njsBaton *baton, dpiJsonNode *node,
|
|||
NJS_CHECK_NAPI(env, napi_call_function(env, global,
|
||||
baton->jsDecodeVectorFn, 1, &vectorBytes, value))
|
||||
return true;
|
||||
case DPI_ORACLE_TYPE_JSON_ID:
|
||||
byteLength = node->value->asBytes.length;
|
||||
NJS_CHECK_NAPI(env, napi_create_arraybuffer(env,
|
||||
byteLength, &destData, &arrBuf))
|
||||
memcpy(destData, node->value->asBytes.ptr, byteLength);
|
||||
NJS_CHECK_NAPI(env, napi_new_instance(env,
|
||||
baton->jsJsonIdConstructor, 1, &arrBuf, value))
|
||||
return true;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -807,6 +818,10 @@ bool njsBaton_setJsValues(njsBaton *baton, napi_env env)
|
|||
NJS_CHECK_NAPI(env, napi_get_reference_value(env,
|
||||
baton->globals->jsEncodeVectorFn, &baton->jsEncodeVectorFn))
|
||||
|
||||
// acquire the JsonId constructor
|
||||
NJS_CHECK_NAPI(env, napi_get_reference_value(env,
|
||||
baton->globals->jsJsonIdConstructor, &baton->jsJsonIdConstructor))
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -243,9 +243,16 @@ static bool njsJsonBuffer_populateNode(njsJsonBuffer *buf, dpiJsonNode *node,
|
|||
// handle buffers
|
||||
NJS_CHECK_NAPI(env, napi_is_buffer(env, value, &check))
|
||||
if (check) {
|
||||
NJS_CHECK_NAPI(env, napi_instanceof(env, value,
|
||||
baton->jsJsonIdConstructor, &check))
|
||||
NJS_CHECK_NAPI(env, napi_get_buffer_info(env, value,
|
||||
(void**) &tempBuffer, &tempBufferLength))
|
||||
node->oracleTypeNum = DPI_ORACLE_TYPE_RAW;
|
||||
if (check) {
|
||||
// Handle JsonId
|
||||
node->oracleTypeNum = DPI_ORACLE_TYPE_JSON_ID;
|
||||
} else {
|
||||
node->oracleTypeNum = DPI_ORACLE_TYPE_RAW;
|
||||
}
|
||||
node->nativeTypeNum = DPI_NATIVE_TYPE_BYTES;
|
||||
node->value->asBytes.ptr = tempBuffer;
|
||||
node->value->asBytes.length = (uint32_t) tempBufferLength;
|
||||
|
|
|
@ -126,6 +126,7 @@ static void njsModule_finalizeGlobals(napi_env env, void *finalize_data,
|
|||
NJS_DELETE_REF_AND_CLEAR(globals->jsMakeDateFn);
|
||||
NJS_DELETE_REF_AND_CLEAR(globals->jsDecodeVectorFn);
|
||||
NJS_DELETE_REF_AND_CLEAR(globals->jsEncodeVectorFn);
|
||||
NJS_DELETE_REF_AND_CLEAR(globals->jsJsonIdConstructor);
|
||||
free(globals);
|
||||
}
|
||||
|
||||
|
@ -189,6 +190,12 @@ static bool njsModule_populateGlobals(napi_env env, napi_value module,
|
|||
&globals->jsSodaOperationConstructor))
|
||||
return false;
|
||||
|
||||
// get the JsonId class
|
||||
NJS_CHECK_NAPI(env, napi_get_named_property(env, settings, "_JsonId",
|
||||
&temp))
|
||||
NJS_CHECK_NAPI(env, napi_create_reference(env, temp, 1,
|
||||
&globals->jsJsonIdConstructor))
|
||||
|
||||
// store a reference to the _makeDate() function
|
||||
NJS_CHECK_NAPI(env, napi_get_named_property(env, settings,
|
||||
"_getDateComponents", &temp))
|
||||
|
@ -267,6 +274,7 @@ static bool njsModule_initDPI(napi_env env, napi_value *args,
|
|||
|
||||
// initialize structure
|
||||
memset(¶ms, 0, sizeof(params));
|
||||
params.useJsonId = 1; // Enable JSONID
|
||||
if (*libDirLength > 0)
|
||||
params.oracleClientLibDir = *libDir;
|
||||
if (*configDirLength > 0)
|
||||
|
|
|
@ -469,6 +469,7 @@ struct njsBaton {
|
|||
napi_value jsMakeDateFn;
|
||||
napi_value jsDecodeVectorFn;
|
||||
napi_value jsEncodeVectorFn;
|
||||
napi_value jsJsonIdConstructor;
|
||||
|
||||
// calling object value (used for setting a reference on created objects)
|
||||
napi_value jsCallingObj;
|
||||
|
@ -556,6 +557,7 @@ struct njsModuleGlobals {
|
|||
napi_ref jsMakeDateFn;
|
||||
napi_ref jsDecodeVectorFn;
|
||||
napi_ref jsEncodeVectorFn;
|
||||
napi_ref jsJsonIdConstructor;
|
||||
};
|
||||
|
||||
// data for class Pool exposed to JS.
|
||||
|
|
|
@ -724,4 +724,109 @@ describe('244.dataTypeJson.js', function() {
|
|||
|
||||
}); // 244.9
|
||||
|
||||
describe('244.10 Verify auto-generated SODA document key', function() {
|
||||
const TABLE = 'nodb_244_63soda';
|
||||
let supportsJsonId;
|
||||
|
||||
before('create table, insert data', async function() {
|
||||
supportsJsonId = (testsUtil.getClientVersion() >= 2304000000) &&
|
||||
(connection.oracleServerVersion >= 2304000000);
|
||||
if (!supportsJsonId) {
|
||||
this.skip();
|
||||
}
|
||||
const sql = `CREATE JSON COLLECTION TABLE if not exists ${TABLE}`;
|
||||
await connection.execute(sql);
|
||||
}); // before()
|
||||
|
||||
after(async function() {
|
||||
if (!supportsJsonId) {
|
||||
return;
|
||||
}
|
||||
const sql = `DROP TABLE if exists ${TABLE}`;
|
||||
await connection.execute(sql);
|
||||
}); // after()
|
||||
|
||||
it('244.10.1 Verify Json Id on select', async function() {
|
||||
const inpDoc = {"name": "Jenny"};
|
||||
let sql = ` insert into ${TABLE} values (:1)`;
|
||||
let result = await connection.execute(sql, [{
|
||||
type: oracledb.DB_TYPE_JSON,
|
||||
val: inpDoc
|
||||
}]);
|
||||
|
||||
// Verify _id is generated.
|
||||
sql = `select * from ${TABLE}`;
|
||||
result = await connection.execute(sql);
|
||||
let genDoc = result.rows[0][0];
|
||||
assert(("_id" in genDoc));
|
||||
const autogenID = genDoc._id;
|
||||
|
||||
// Verify update with new values without passing _id.
|
||||
inpDoc.name = "Scott";
|
||||
sql = ` update ${TABLE} set DATA = :1`;
|
||||
result = await connection.execute(sql, [{
|
||||
type: oracledb.DB_TYPE_JSON,
|
||||
val: inpDoc
|
||||
}]);
|
||||
sql = `select * from ${TABLE}`;
|
||||
result = await connection.execute(sql);
|
||||
genDoc = result.rows[0][0];
|
||||
const updatedID = genDoc._id;
|
||||
assert.deepStrictEqual(updatedID, autogenID);
|
||||
assert.strictEqual(inpDoc.name, genDoc.name);
|
||||
|
||||
// Verify update with new values with passing _id from the generated Doc.
|
||||
genDoc.name = "John";
|
||||
sql = ` update ${TABLE} set DATA = :1`;
|
||||
result = await connection.execute(sql, [{
|
||||
type: oracledb.DB_TYPE_JSON,
|
||||
val: genDoc
|
||||
}]);
|
||||
sql = `select * from ${TABLE}`;
|
||||
result = await connection.execute(sql);
|
||||
const updatedDoc = result.rows[0][0];
|
||||
assert.deepStrictEqual(updatedDoc, genDoc);
|
||||
const expectedJsonData = genDoc;
|
||||
expectedJsonData._id = Buffer.from(autogenID).toString('hex');
|
||||
assert.deepStrictEqual(JSON.stringify(expectedJsonData),
|
||||
JSON.stringify(updatedDoc));
|
||||
|
||||
// Insert Document with Previously generated JsonId type.
|
||||
const jsonId = new oracledb.JsonId(genDoc._id);
|
||||
const inpDocWithJsonIdKey = {"_id": jsonId, "name": "Bob"};
|
||||
sql = ` insert into ${TABLE} values (:1)`;
|
||||
result = await connection.execute(sql, [{
|
||||
type: oracledb.DB_TYPE_JSON,
|
||||
val: inpDocWithJsonIdKey
|
||||
}]);
|
||||
sql = `select * from ${TABLE}`;
|
||||
result = await connection.execute(sql);
|
||||
genDoc = result.rows[1][0];
|
||||
assert.deepStrictEqual(genDoc, inpDocWithJsonIdKey);
|
||||
|
||||
// overwrite the auto-generated _id with user key should fail.
|
||||
genDoc._id = "RandomId";
|
||||
sql = ` update ${TABLE} set DATA = :1`;
|
||||
await assert.rejects(
|
||||
async () => await connection.execute(sql, [{
|
||||
type: oracledb.DB_TYPE_JSON,
|
||||
val: genDoc
|
||||
}]),
|
||||
/ORA-54059:/ // cannot update an immutable column to a different value
|
||||
);
|
||||
|
||||
// User provided keys should still work.
|
||||
const inpDocWithUserKey = {"_id": 1, "name": "Jenny"};
|
||||
sql = ` insert into ${TABLE} values (:1)`;
|
||||
result = await connection.execute(sql, [{
|
||||
type: oracledb.DB_TYPE_JSON,
|
||||
val: inpDocWithUserKey
|
||||
}]);
|
||||
sql = `select * from ${TABLE}`;
|
||||
result = await connection.execute(sql);
|
||||
genDoc = result.rows[2][0];
|
||||
assert.deepStrictEqual(genDoc, inpDocWithUserKey);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -4907,6 +4907,8 @@ oracledb.OUT_FORMAT_OBJECT and resultSet = true
|
|||
244.9.1 works with oracledb.fetchAsString
|
||||
244.9.2 doesn't work with outFormat: oracledb.DB_TYPE_JSON
|
||||
244.9.3 could work with fetchInfo oracledb.STRING
|
||||
244.10 Verify auto-generated SODA document key
|
||||
244.10.1 Verify Json Id on select
|
||||
|
||||
245. fetchLobAsStrBuf.js
|
||||
245.1 CLOB,BLOB Insert
|
||||
|
|
|
@ -500,7 +500,7 @@ testsUtil.isDate = function(date) {
|
|||
// return a fake version for thin to facilitate client version checks
|
||||
testsUtil.getClientVersion = function() {
|
||||
if (oracledb.thin)
|
||||
return 2302000000;
|
||||
return 2304000000;
|
||||
return oracledb.oracleClientVersion;
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue