oracle-nosqldb
Version:
Node.js driver for Oracle NoSQL Database
517 lines (454 loc) • 16.2 kB
JavaScript
/*-
* Copyright (c) 2018, 2024 Oracle and/or its affiliates. All rights reserved.
*
* Licensed under the Universal Permissive License v 1.0 as shown at
* https://oss.oracle.com/licenses/upl/
*/
;
const assert = require('assert');
const isInt32 = require('../utils').isInt32;
const sortMapEntries = require('../utils').sortMapEntries;
const TableState = require('../constants').TableState;
const AdminState = require('../constants').AdminState;
const ScanDirection = require('../constants').ScanDirection;
const ServiceType = require('../constants').ServiceType;
const EMPTY_VALUE = require('../constants').EMPTY_VALUE;
const CapacityMode = require('../constants').CapacityMode;
const Type = require('./constants').Type;
const TTLTimeUnit = require('./constants').TTLTimeUnit;
const MathContext = require('./constants').MathContext;
const ErrorCode = require('../error_code');
const error = require('../error');
const NoSQLError = error.NoSQLError;
const NoSQLArgumentError = error.NoSQLArgumentError;
const NoSQLProtocolError = error.NoSQLProtocolError;
class Protocol {
//Consider special cases where error code/message needs to be changed.
static _createError(errCode, msg, req) {
assert(req && req._op);
switch(errCode) {
case ErrorCode.BAD_PROTOCOL_MESSAGE:
//Older servers will send this message when they don't support
//current query version.
if (msg && msg.toLowerCase().includes('invalid query version')) {
errCode = ErrorCode.UNSUPPORTED_QUERY_VERSION;
}
break;
case ErrorCode.TABLE_NOT_FOUND:
//Special case for TABLE_NOT_FOUND errors on writeMany with
//multiple tables. Earlier server versions do not support this and
//will return a TABLE_NOT_FOUND error with the table names in a
//single string, separated by commas, with no brackets, like:
//table1,table2,table3
//Later versions may legitimately return TABLE_NOT_FOUND error,
//but table names will be inside a bracketed list, like:
//[table1, table2, table3]
//Using string comparison for op's name to avoid introducing
//another dependency on ops module.
if (req._op.name === 'WriteMultipleOp' && req.tableName == null &&
msg && msg.includes(',') && !msg.includes('[')) {
errCode = ErrorCode.OPERATION_NOT_SUPPORTED;
msg = 'WriteMany operation with multiple tables is not \
supported by the version of the connected server';
}
break;
default:
break;
}
return NoSQLError.create(errCode, msg, null, req);
}
//Serialization.
static writeTimeout(dw, timeout) {
dw.writeInt(timeout);
}
static writeConsistency(dw, cons) {
dw.writeByte(cons.ordinal);
}
static writeScanDirection(dw, dir) {
if (!dir) {
dir = ScanDirection.UNORDERED;
}
dw.writeByte(dir.ordinal);
}
//Assumes ttl already in canonical form, see TTLUtil#validate()
static writeTTL(dw, ttl) {
if (ttl == null) { //null or undefined
return dw.writeLong(-1);
}
if (ttl.days != null) {
dw.writeLong(ttl.days !== Infinity ? ttl.days : 0);
return dw.writeByte(TTLTimeUnit.DAYS);
}
assert(ttl.hours != null);
dw.writeLong(ttl.hours);
dw.writeByte(TTLTimeUnit.HOURS);
}
static writeArray(dw, array, opt) {
const lengthOffset = dw.buffer.length;
dw.writeInt32BE(0);
const start = dw.buffer.length;
dw.writeInt32BE(array.length);
for(let val of array) {
this.writeFieldValue(dw, val, opt);
}
dw.buffer.writeInt32BE(dw.buffer.length - start, lengthOffset);
}
static _writeMapEntries(dw, ent, cnt, opt) {
if (opt._writeSortedMaps) {
//used by the query code to serialize grouping columns of type MAP
ent = sortMapEntries(ent);
}
const lengthOffset = dw.buffer.length;
dw.writeInt32BE(0);
const start = dw.buffer.length;
dw.writeInt32BE(cnt);
for(let [key, val] of ent) {
if (typeof key !== 'string') {
throw new NoSQLArgumentError(`Invalid map or object key for \
field value: ${key}, must be a string`);
}
dw.writeString(key);
this.writeFieldValue(dw, val, opt);
}
dw.buffer.writeInt32BE(dw.buffer.length - start, lengthOffset);
}
static writeMap(dw, map, opt) {
this._writeMapEntries(dw, map.entries(), map.size, opt);
}
static writeObject(dw, obj, opt) {
const ent = Object.entries(obj);
this._writeMapEntries(dw, ent, ent.length, opt);
}
static writeVersion(dw, version) {
dw.writeBinary(version);
}
static writeOpCode(dw, opCode, serialVersion) {
dw.writeInt16BE(serialVersion);
dw.writeByte(opCode);
}
static writeSubOpCode(dw, opCode) {
dw.writeByte(opCode);
}
static writeFieldRange(dw, fr, opt) {
if (!fr) {
dw.writeBoolean(false);
return;
}
dw.writeBoolean(true);
dw.writeString(fr.fieldName);
if (fr.startWith) {
dw.writeBoolean(true);
this.writeFieldValue(dw, fr.startWith, opt);
dw.writeBoolean(true);
} else if (fr.startAfter) {
dw.writeBoolean(true);
this.writeFieldValue(dw, fr.startAfter, opt);
dw.writeBoolean(false);
} else {
dw.writeBoolean(false);
}
if (fr.endWith) {
dw.writeBoolean(true);
this.writeFieldValue(dw, fr.endWith, opt);
dw.writeBoolean(true);
} else if (fr.endBefore) {
dw.writeBoolean(true);
this.writeFieldValue(dw, fr.endBefore, opt);
dw.writeBoolean(false);
} else {
dw.writeBoolean(false);
}
}
static writeFieldValue(dw, val, opt) {
if (opt._replacer) {
val = opt._replacer(val, opt);
}
if (typeof val === 'function') {
//If the field specified as a function, we write its return value
val = val();
}
if (val === undefined) {
return dw.writeByte(Type.NULL);
}
if (val === null) {
return dw.writeByte(Type.JSON_NULL);
}
switch(typeof val) {
case 'boolean':
dw.writeByte(Type.BOOLEAN);
dw.writeBoolean(val);
break;
case 'string':
dw.writeByte(Type.STRING);
dw.writeString(val);
break;
case 'number':
if (Number.isSafeInteger(val)) {
if (isInt32(val)) {
dw.writeByte(Type.INTEGER);
dw.writeInt(val);
} else {
dw.writeByte(Type.LONG);
dw.writeLong(val);
}
} else {
dw.writeByte(Type.DOUBLE);
dw.writeDouble(val);
}
break;
case 'bigint':
dw.writeByte(Type.LONG);
dw.writeLong(val);
break;
case 'object':
if (Buffer.isBuffer(val)) {
dw.writeByte(Type.BINARY);
dw.writeBinary(val);
break;
} else if (val instanceof Date) {
dw.writeByte(Type.TIMESTAMP);
dw.writeDate(val);
break;
} else if (Array.isArray(val)) {
dw.writeByte(Type.ARRAY);
this.writeArray(dw, val, opt);
break;
} else if (opt._dbNumber != null &&
opt._dbNumber.isInstance(val)) {
dw.writeByte(Type.NUMBER);
dw.writeString(opt._dbNumber.stringValue(val));
break;
} else {
dw.writeByte(Type.MAP);
if (val instanceof Map) {
this.writeMap(dw, val, opt);
} else {
this.writeObject(dw, val, opt);
}
break;
}
default:
throw new NoSQLArgumentError('Unsupported value type ' +
`${typeof val} for value ${val.toString()}`);
}
}
static serializeRequest(dw, req) {
assert(req.opt.requestTimeout);
this.writeTimeout(dw, req.opt.requestTimeout);
}
static serializeReadRequest(dw, req) {
this.serializeRequest(dw, req);
dw.writeString(req.tableName);
this.writeConsistency(dw, req.opt.consistency);
}
static serializeWriteRequest(dw, req, serialVersion) {
this.serializeRequest(dw, req);
dw.writeString(req.tableName);
dw.writeBoolean(req.opt.returnExisting);
this.writeDurability(dw, req.opt.durability, serialVersion);
}
static durabilityToNum(dur) {
if (dur == null) {
return 0;
}
let val = dur.masterSync.ordinal;
val |= (dur.replicaSync.ordinal << 2);
val |= (dur.replicaAck.ordinal << 4);
return val;
}
static writeDurability(dw, dur, serialVersion) {
if (serialVersion < 3) {
return;
}
dw.writeByte(this.durabilityToNum(dur));
}
static writeMathContext(dw, opt) {
if (opt._dbNumber == null) {
return dw.writeByte(MathContext.DEFAULT);
}
dw.writeByte(MathContext.CUSTOM);
dw.writeInt32BE(opt._dbNumber.precision);
dw.writeInt32BE(opt._dbNumber.roundingMode);
}
//Deserialization.
static readArray(dr, opt) {
dr.readInt32BE(); //read total length
const len = dr.readInt32BE();
const array = new Array(len);
for(let i = 0; i < len; i++) {
array[i] = this.readFieldValue(dr, opt);
}
return array;
}
//For now, we will use readObject() for Map columns because currently
//both Record and Map columns are sent with the same type code and it is
//more natural to represent Record value as object. IMO, it is more
//adequate for now to represent Map value as object than to represent
//Record value as JavaScript Map.
static readMap(dr, opt) {
dr.readInt32BE(); //read total length
const size = dr.readInt32BE();
const map = new Map();
for(let i = 0; i < size; i++) {
const key = dr.readString();
const val = this.readFieldValue(dr, opt);
map.set(key, val);
}
return map;
}
static readObject(dr, opt) {
dr.readInt32BE(); //read total length
const size = dr.readInt32BE();
const obj = {};
for(let i = 0; i < size; i++) {
const key = dr.readString();
const val = this.readFieldValue(dr, opt);
obj[key] = val;
}
return obj;
}
static readFieldValue(dr, opt) {
const type = dr.readByte();
switch(type) {
case Type.ARRAY:
return this.readArray(dr, opt);
case Type.BINARY:
return dr.readBinary();
case Type.BOOLEAN:
return dr.readBoolean();
case Type.DOUBLE:
return dr.readDouble();
case Type.INTEGER:
return dr.readInt();
case Type.LONG:
return dr.readLong(opt.longAsBigInt);
case Type.MAP:
//Until Record type code is added to the protocol
return this.readObject(dr, opt);
case Type.STRING:
return dr.readString();
case Type.TIMESTAMP:
return dr.readDate();
case Type.NUMBER:
return (opt._dbNumber != null) ?
opt._dbNumber.create(dr.readString()) :
Number(dr.readString());
case Type.NULL:
return undefined;
case Type.JSON_NULL:
return null;
case Type.EMPTY:
return EMPTY_VALUE;
default:
throw new NoSQLProtocolError(`Unknown value type code: ${type}`);
}
}
static readRecord(dr, opt) {
const type = dr.readByte();
//Until Record type code is added to the protocol
if (type !== Type.MAP) {
throw new NoSQLProtocolError(`Unexpected type code for row: \
${type}, expecting ${Type.MAP} (map)`);
}
return this.readObject(dr, opt, true);
}
static readVersion(dr) {
return dr.readBinary();
}
static deserializeConsumedCapacity(dr, res, opt) {
const readUnits = dr.readInt();
const readKB = dr.readInt();
const writeKB = dr.readInt();
if (opt.serviceType !== ServiceType.KVSTORE) {
res.consumedCapacity = {
readUnits,
readKB,
writeUnits: writeKB,
writeKB
};
}
}
static deserializeWriteResponse(dr, opt, res, serialVersion) {
const returnInfo = dr.readBoolean();
if (returnInfo) {
res.existingRow = this.readRecord(dr, opt);
res.existingVersion = this.readVersion(dr);
if (serialVersion > 2) {
res.existingModificationTime = new Date(dr.readLong());
}
}
return res;
}
static deserializeWriteResponseWithId(dr, opt, res, serialVersion) {
this.deserializeWriteResponse(dr, opt, res, serialVersion);
if (dr.readBoolean()) { //has generated id column value
res.generatedValue = this.readFieldValue(dr, opt);
}
return res;
}
static deserializeTableResult(dr, opt, serialVersion) {
const res = {};
const hasInfo = dr.readBoolean();
if (hasInfo) {
const compartmentId = dr.readString();
if (opt.serviceType === ServiceType.CLOUD) {
res.compartmentId = compartmentId;
}
res.tableName = dr.readString();
res.tableState = TableState.fromOrdinal(dr.readByte());
}
const hasStaticState = dr.readBoolean();
if (hasStaticState) {
const readUnits = dr.readInt();
const writeUnits = dr.readInt();
const storageGB = dr.readInt();
let mode = CapacityMode.PROVISIONED;
if (serialVersion > 2) {
mode = CapacityMode.fromOrdinal(dr.readByte());
}
if (opt.serviceType !== ServiceType.KVSTORE) {
res.tableLimits = { readUnits, writeUnits, storageGB, mode };
}
res.schema = dr.readString();
}
res.operationId = dr.readString();
return res;
}
static readTopologyInfo(dr) {
const seqNum = dr.readInt();
if (seqNum < -1) {
throw new NoSQLProtocolError(
`Invalid topology sequence number: ${seqNum}`);
}
if (seqNum === -1) {
//No topology info sent by proxy
return null;
}
const shardIds = dr.readIntArray();
return {
seqNum,
shardIds
};
}
static mapError(rc, msg, req) {
let errCode;
try {
errCode = ErrorCode.fromOrdinal(rc);
} catch(err) {
return new NoSQLProtocolError(
'Received invalid error code: ' + rc, err, req);
}
const err = this._createError(errCode, msg, req);
err._rejectedByDriver = false; //for testing
return err;
}
static deserializeSystemResult(dr) {
const res = {};
res.state = AdminState.fromOrdinal(dr.readByte());
res.operationId = dr.readString();
res.statement = dr.readString();
res.output = dr.readString();
return res;
}
}
module.exports = Protocol;