UNPKG

oracle-nosqldb

Version:

Node.js driver for Oracle NoSQL Database

305 lines (291 loc) 10 kB
/*- * Copyright (c) 2018, 2025 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 * * Please see LICENSE.txt file included in the top-level directory of the * appropriate download for a copy of the license and additional information. */ 'use strict'; const util = require('util'); const EMPTY_VALUE = require('../constants').EMPTY_VALUE; const isNumeric = require('./utils').isNumeric; const isPlainObject = require('../utils').isPlainObject; const compare = require('../utils').compare; const sortMapEntries = require('../utils').sortMapEntries; function _compVal(val1, val2) { if (typeof val1 === 'number') { //NaN is equal to itself and is greater than everything else if (isNaN(val1)) { return (typeof val2 === 'number' && isNaN(val2)) ? 0 : 1; } if (typeof val2 === 'number' && isNaN(val2)) { return -1; } } return compare(val1, val2); } //For comparison of different types we assume: //numerics < timestamps < strings < booleans //This function assumes the arguments are not null or EMPTY function compareNonNullAtomics(ctx, val1, val2) { switch(typeof val1) { case 'number': case 'bigint': if (typeof val2 === 'number' || typeof val2 === 'bigint') { return _compVal(val1, val2); } if (ctx._dbNumber != null && ctx._dbNumber.isInstance(val2)) { //3rd party number library may not support any operations with //bigint, going through string ensures precision is preserved. if (typeof val1 === 'bigint') { val1 = ctx._dbNumber.create(val1.toString()); } return -ctx._dbNumber.compare(val2, val1); } if (typeof val2 === 'string' || typeof val2 === 'boolean' || val2 instanceof Date) { return -1; } break; case 'string': case 'boolean': if (typeof val1 === typeof val2) { return _compVal(val1, val2); } if (typeof val2 === 'string' || isNumeric(ctx, val2) || val2 instanceof Date) { return 1; } if (typeof val2 === 'boolean') { return -1; } break; case 'object': if (val1 instanceof Date) { if (val2 instanceof Date) { return _compVal(val1.getTime(), val2.getTime()); } if (typeof val2 === 'string' || typeof val2 === 'boolean') { return -1; } if (isNumeric(ctx, val2)) { return 1; } break; } if (ctx._dbNumber != null && ctx._dbNumber.isInstance(val1)) { if (typeof val2 === 'bigint') { val2 = ctx._dbNumber.create(val2.toString()); } if (ctx._dbNumber.isInstance(val2) || typeof val2 === 'number') { return ctx._dbNumber.compare(val1, val2); } if (typeof val2 === 'string' || typeof val2 === 'boolean' || val2 instanceof Date) { return -1; } break; } default: throw ctx.unsupportedComp(val1); } throw ctx.unsupportedComp(val2); } //compare in ascending order only //compareRows() will reverse for descending order //undefined is used for SQL NULL, null for JSON NULL function compareAtomics(ctx, val1, val2, nullRank) { if (val1 === undefined) { switch(val2) { case undefined: return 0; case null: case EMPTY_VALUE: return 1; default: return nullRank; } } else if (val1 === null) { switch(val2) { case undefined: return -1; case null: return 0; case EMPTY_VALUE: return 1; default: return nullRank; } } else if (val1 === EMPTY_VALUE) { switch(val2) { case undefined: case null: return -1; case EMPTY_VALUE: return 0; default: return nullRank; } } else if (val2 == null || val2 === EMPTY_VALUE) { return -nullRank; } return compareNonNullAtomics(ctx, val1, val2); } //Same as compareAtomics() but allows comparison of binary values. See //compareFieldValuesTotalOrder(). function compareAtomicsTotalOrder(ctx, val1, val2, nullRank) { if (Buffer.isBuffer(val1)) { return Buffer.isBuffer(val2) ? Buffer.compare(val1, val2) : ((val2 == null || val2 === EMPTY_VALUE) ? -nullRank : 1); } if (Buffer.isBuffer(val2)) { return (val1 == null || val1 === EMPTY_VALUE) ? nullRank : -1; } return compareAtomics(ctx, val1, val2, nullRank); } function compareRows(ctx, row1, row2, sortSpecs) { for(let ss of sortSpecs) { let compRes = compareAtomics(ctx, row1[ss.fieldName], row2[ss.fieldName], ss.nullRank); if (ss.isDesc) { compRes = -compRes; } if (compRes) { return compRes; } } return 0; } function _compareArrays(ctx, arr1, arr2, nullRank) { const len = Math.min(arr1.length, arr2.length); for(let i = 0; i < len; i++) { const res = compareFieldValuesTotalOrder(ctx, arr1[i], arr2[i], nullRank); if (res !== 0) { return res; } } return compare(arr1.length, arr2.length); } function _compareMapEntries(ctx, ents1, ents2, nullRank) { ents1 = sortMapEntries(ents1); ents2 = sortMapEntries(ents2); const len = Math.min(ents1.length, ents2.length); for(let i = 0; i < len; i++) { let res = compare(ents1[i][0], ents2[i][0]); if (res !== 0) { return res; } res = compareFieldValuesTotalOrder(ctx, ents1[i][1], ents2[i][1], nullRank); if (res !== 0) { return res; } } return compare(ents1.length, ents2.length); } //Includes comparison of complex types, such as arrays and maps, as well as //binary. The order between different types is: //Arrays > Maps > Atomic values, and Binary values (which are atomic) are //greater than any other atomic values except special values (Null, Json Null //and Empty), for which nullRank is used. Other than that, the order is the //same as in compareAtomics(). function compareFieldValuesTotalOrder(ctx, val1, val2, nullRank = 1) { if (Array.isArray(val1)) { return Array.isArray(val2) ? _compareArrays(ctx, val1, val2, nullRank) : 1; } if (val1 instanceof Map || isPlainObject(val1)) { if (Array.isArray(val2)) { return -1; } if (!(val2 instanceof Map) && !isPlainObject(val2)) { return 1; } return _compareMapEntries(ctx, val1 instanceof Map ? val1.entries() : Object.entries(val1), val2 instanceof Map ? val2.entries() : Object.entries(val2), nullRank); } if (Array.isArray(val2) || val2 instanceof Map || isPlainObject(val2)) { return -1; } return compareAtomicsTotalOrder(ctx, val1, val2, nullRank); } //We assume the values can be either Map of object, although Map is not //currently used in protocol when reading field values. function _mapValuesEqual(ctx, val1, val2) { let ents1; let size1; if (val1 instanceof Map) { ents1 = val1.entries(); size1 = val1.size; } else { ents1 = Object.entries(val1); size1 = ents1.length; } const isMap2 = val2 instanceof Map; const size2 = isMap2 ? val2.size : Object.keys(val2).length; if (size1 !== size2) { return false; } for(let [k1, v1] of ents1) { const v2 = isMap2 ? val2.get(k1) : val2[k1]; if (!fieldValuesEqual(ctx, v1, v2)) { return false; } } return true; } //Compare field values for grouping in SFWIterator. //Note that for efficiency the type checking of val2 is not done, since these //are the values deserialized while reading records. function fieldValuesEqual(ctx, val1, val2) { if (val1 == null) { //NULL (undefined) or JSON NULL (null) return val1 === val2; } switch(typeof val1) { case 'number': case 'bigint': if (typeof val2 === 'number' || typeof val2 === 'bigint') { return val1 == val2; } if (ctx._dbNumber != null && ctx._dbNumber.isInstance(val2)) { if (typeof val1 === 'bigint') { val1 = ctx._dbNumber.create(val1.toString()); } return ctx._dbNumber.valuesEqual(val2, val1); } return false; case 'string': case 'boolean': return val1 === val2; case 'object': if (val1 instanceof Date) { return val2 instanceof Date && val1.getTime() === val2.getTime(); } if (Buffer.isBuffer(val1)) { return Buffer.isBuffer(val2) && val1.equals(val2); } if (ctx._dbNumber != null && ctx._dbNumber.isInstance(val1)) { if (typeof val2 === 'bigint') { val2 = ctx._dbNumber.create(val2.toString()); } return (ctx._dbNumber.isInstance(val2) || typeof val2 === 'number') && ctx._dbNumber.valuesEqual(val1, val2); } if (Array.isArray(val1)) { return Array.isArray(val2) && val1.reduce((acc, curr, idx) => acc && fieldValuesEqual(curr, val2[idx]), true); } return _mapValuesEqual(val1, val2); default: throw ctx.illegalState(`Unexpected field value for equality \ comparison: ${util.inspect(val1)}`); } } module.exports = { compareNonNullAtomics, compareAtomics, compareAtomicsTotalOrder, compareRows, compareFieldValuesTotalOrder, fieldValuesEqual };