UNPKG

oracle-nosqldb

Version:

Node.js driver for Oracle NoSQL Database

417 lines (375 loc) 14.1 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/ */ 'use strict'; const assert = require('assert'); const QueryOp = require('../ops').QueryOp; const NoSQLError = require('../error').NoSQLError; const BinaryProtocol = require('../binary_protocol/protocol'); const DataWriter = require('../binary_protocol/writer'); const PlanIterator = require('./common').PlanIterator; const DistributionKind = require('./common').DistributionKind; const MinHeap = require('./min_heap'); const compareRows = require('./compare').compareRows; const resBuf2MapKey = require('./utils').resBuf2MapKey; const sizeof = require('./utils').sizeof; const convertEmptyToNull = require('./utils').convertEmptyToNull; function hasLocalResults(res) { return res.rows && res._idx != null && res._idx < res.rows.length; } function compPartShardIds(res1, res2) { return res1._partId != null ? (res1._partId < res2._partId ? -1 : 1) : (res1._shardId < res2._shardId ? -1 : 1); } /** * ReceiveIterator requests and receives results from the proxy. For sorting * queries, it performs a merge sort of the received results. It also * performs duplicate elimination for queries that require it (note: * a query can do both sorting and dup elimination). */ class ReceiveIterator extends PlanIterator { constructor(qpExec, step) { super(qpExec, step); if (step.pkFields) { this._dup = new Set(); this._dupMem = 0; this._dw = new DataWriter(); } if (step.sortSpecs) { const cmp = this._compareRes.bind(this); if (step.distKind === DistributionKind.ALL_SHARDS) { const topoInfo = qpExec._baseTopo; if (topoInfo == null) { throw this.badProto( 'Missing topology information for all-shard query'); } assert(topoInfo.shardIds && topoInfo.shardIds.length); //seed empty shard results for sortingNext() loop this._spRes = new MinHeap(cmp, topoInfo.shardIds.map( _shardId => ({ _shardId }))); } else if (step.distKind === DistributionKind.ALL_PARTITIONS) { this._spRes = new MinHeap(cmp); this._allPartSort = true; this._allPartSortPhase1 = true; this._totalRows = 0; this._totalMem = 0; } } } _compareRes(res1, res2) { if (!hasLocalResults(res1)) { return hasLocalResults(res2) ? -1 : compPartShardIds(res1, res2); } if (!hasLocalResults(res2)) { return 1; } const compRes = compareRows(this, res1.rows[res1._idx], res2.rows[res2._idx], this._step.sortSpecs); return compRes ? compRes : compPartShardIds(res1, res2); } _getLimitFromMem() { const maxMem = this._qpExec.maxMem; const memPerRow = this._totalMem / this._totalRows; let limit = (maxMem - this._dupMem) / memPerRow; limit = Math.min(Math.floor(limit), 2048); if (limit <= 0) { throw this.memoryExceeded(`Cannot make another request because \ set memory limit of ${this._qpExec.maxMemMB} MB will be exceeded`); } return limit; } _setMemStats(res) { res._mem = sizeof(this, res.rows); this._totalRows += res.rows.length; this._totalMem += res._mem; this._qpExec.incMem(res._mem); } _validatePhase1Res(res) { //Check that we are really in sort phase 1 if (res._contAllPartSortPhase1 == null) { throw this.badProto('First response to ALL_PARTITIONS query is \ not a phase 1 response'); } if (res._contAllPartSortPhase1 && !res.continuationKey) { throw this.badProto('ALL_PARTITIONS query: missing continuation \ key needed to continue phase 1'); } if (!res._partIds) { res._partIds = []; } if (!res._partIds.length) { //Empty result case, do validation and return if (res.rows && res.rows.length) { throw this.badProto('ALL_PARTITIONS query phase 1: received \ rows but no partition ids'); } if (res._numResultsPerPartId && res._numResultsPerPartId.length) { throw this.badProto('ALL_PARTITIONS query phase 1: received \ numResultsPerPartitionId array but no partition ids'); } } else { const numResPerPartCnt = res._numResultsPerPartId ? res._numResultsPerPartId.length : 0; if (numResPerPartCnt !== res._partIds.length) { throw this.badProto(`ALL_PARTITIONS query phase 1: received \ mismatched arrays of partitionIds of length ${res._partIds.length} and \ numResultsPerPartitionId of length ${numResPerPartCnt}`); } } } //Group results by partition id and put them into the MinHeap _addPartResults(res) { let rowIdx = 0; for(let i = 0; i < res._partIds.length; i++) { const end = rowIdx + res._numResultsPerPartId[i]; if (end > res.rows.length) { throw this.badProto(`ALL PARTITIONS query phase 1: exceeded \ row count ${res.rows.length} while getting rows for partition id \ ${res._partId[i]}, expected index range [${rowIdx}, ${end})`); } const pRes = { rows: res.rows.slice(rowIdx, end), continuationKey: res._partContKeys[i], _partId: res._partIds[i], _idx: 0 }; this._spRes.add(pRes); this._setMemStats(pRes); rowIdx = end; } if (rowIdx !== res.rows.length) { throw this.badProto(`ALL PARTITIONS query phase 1: received per \ partition row counts (total ${rowIdx}) did not match total row count \ ${res.rows.length}`); } } //We have to convert PKs to string because JS Map and Set only do value //comparison for primitives, objects (etc. Buffer) are compared by //reference only _pk2MapKey(row) { this._dw.reset(); for(let fldName of this._step.pkFields) { BinaryProtocol.writeFieldValue(this._dw, row[fldName], this._qpExec.opt); } return resBuf2MapKey(this._dw.buffer); } _chkDup(row) { let key = this._pk2MapKey(row); if (this._dup.has(key)) { return true; } this._dup.add(key); const size = sizeof(this, key); this._dupMem += size; this._qpExec.incMem(size); return false; } _handleVirtualScans(vScans) { if (this._currVSID == null) { const topoInfo = this._qpExec._baseTopo; assert(topoInfo && topoInfo.shardIds && topoInfo.shardIds.length); //shardIds are sorted this._currVSID = topoInfo.shardIds[topoInfo.shardIds.length - 1] + 1; } for(const vs of vScans) { this._spRes.add({ _shardId: this._currVSID++, _vScan: vs }); } } async _fetch(ck, shardId, limit, vScan) { assert(this._qpExec._req); const opt = this._qpExec._req.opt; const req = { api: this._qpExec._client.query, prepStmt: this._qpExec._prepStmt, opt: Object.assign({}, opt), _queryInternal: true, _topoInfo: this._qpExec._baseTopo, _shardId: shardId }; if (vScan != null) { req._vScan = vScan; } req.opt.continuationKey = ck; if (limit) { req.opt.limit = opt.limit ? Math.min(opt.limit, limit) : limit; } const res = await this._qpExec._client._execute(QueryOp, req); assert(Array.isArray(res.rows)); //Virtual scans can only be sent for ALL_SHARDS query. if (res._vScans != null && this._step.distKind !== DistributionKind.ALL_SHARDS) { throw this.badProto(`Received virtual scans for non-shard query \ type: ${this._step.distKind}`); } res._idx = 0; //initialize index to iterate res._shardId = shardId; //set shard id if any //We only make one internal request per user's query() call, //so the same consumed capacity will be returned to the user this._qpExec._cc = res.consumedCapacity; this._qpExec._fetchDone = true; assert(res._reachedLimit || !res.continuationKey || this._allPartSortPhase1); if (res.queryTraces) { this._qpExec.addTraces(res.queryTraces); } return res; } //Returns true if phase 1 is completed async _doAllPartSortPhase1() { //have to postpone phase 1 to the next query() call if (this._qpExec._fetchDone) { assert(this._qpExec._needUserCont); return false; } /* * Create and execute a request to get at least one result from * the partition whose id is specified in theContinuationKey and * from any other partition that is co-located with that partition. */ const res = await this._fetch(this._allPartSortPhase1CK); this._validatePhase1Res(res); this._allPartSortPhase1 = res._contAllPartSortPhase1; this._allPartSortPhase1CK = res.continuationKey; this._addPartResults(res); if (this._allPartSortPhase1) { //need more phase 1 results this._qpExec._needUserCont = true; return false; } return true; } async _simpleNext() { for(;;) { const res = this._res; if (res) { assert(res.rows && res._idx != null); if (res._idx < res.rows.length) { const row = res.rows[res._idx++]; if (this._dup && this._chkDup(row)) { continue; } this.result = row; return true; } if (!res.continuationKey) { return false; } } if (this._qpExec._fetchDone) { break; } this._res = await this._fetch(res ? res.continuationKey : null); } assert(this._res); if (this._res.continuationKey) { this._qpExec._needUserCont = true; } return false; } async _sortingFetch(fromRes) { let limit; if (this._allPartSort) { //We only limit number of rows for ALL_PARTITIONS query limit = this._getLimitFromMem(); //For ALL_PARTITIONS query, decrement memory from previous result this._qpExec.decMem(fromRes._mem); } let res; try { res = await this._fetch(fromRes.continuationKey, fromRes._shardId, limit, fromRes._vScan); } catch(err) { if ((err instanceof NoSQLError) && err.retryable) { //add original result to retry later this._spRes.add(fromRes); } throw err; } this._spRes.add(res); if (this._allPartSort) { this._setMemStats(res); } else { if (fromRes._vScan != null) { fromRes._vScan.isInfoSent = true; } if (res._vScans != null) { this._handleVirtualScans(res._vScans); res._vScans = undefined; } } } _localNext(res) { const row = res.rows[res._idx]; res.rows[res._idx++] = null; //release memory for the row //more cached results or more remote results if (res._idx < res.rows.length || res.continuationKey) { this._spRes.add(res); } if (this._dup && this._chkDup(row)) { return false; } convertEmptyToNull(row); this.result = row; return true; } async _sortingNext() { if (this._allPartSortPhase1 && !(await this._doAllPartSortPhase1())) { return false; } let res; while ((res = this._spRes.pop())) { if (res.rows) { //we have real result assert(res._idx != null); if (res._idx < res.rows.length) { if (this._localNext(res)) { return true; } continue; } if (!res.continuationKey) { //no more results for this shard or partition continue; } } //remote fetch is needed if (this._qpExec._fetchDone) { //We limit to 1 fetch per query() call break; } else { await this._sortingFetch(res); } } if (res) { //another fetch needs to be performed on next query() call if (res.rows) { assert(res.continuationKey); //optimization to release array memory before next //query() call res.rows = null; } this._spRes.add(res); this._qpExec._needUserCont = true; } return false; } next() { return this._spRes ? this._sortingNext() : this._simpleNext(); } //should not be called reset() { throw this.illegalState( 'Reset should not be called for ReceiveIterator'); } } ReceiveIterator._isAsync = true; module.exports = ReceiveIterator;