UNPKG

snowflake-sdk

Version:
397 lines (348 loc) 11.1 kB
/* * Copyright (c) 2015-2024 Snowflake Computing Inc. All rights reserved. */ const EventEmitter = require('events').EventEmitter; const Util = require('../../util'); const Errors = require('../../errors'); /** * Creates a new Chunk. * * @param options * @constructor */ function Chunk(options) { // make sure the options object contains all the necessary information Errors.assertInternal(Util.isObject(options)); Errors.assertInternal(Util.isObject(options.statement)); Errors.assertInternal(Util.isObject(options.services)); Errors.assertInternal(Util.isNumber(options.startIndex)); Errors.assertInternal(Util.isArray(options.columns)); Errors.assertInternal(Util.isObject(options.mapColumnNameToIndices)); Errors.assertInternal(Util.isObject(options.statementParameters)); Errors.assertInternal(Util.isString(options.resultVersion)); Errors.assertInternal(Util.isNumber(options.rowCount)); // if the result is small (i.e. not persisted on S3/Blob), there's no // compressed and uncompressed size, so default to -1 this._compressedSize = options.compressedSize || -1; this._uncompressedSize = options.uncompressedSize || -1; // copy out other information from the options object and save it this._statement = options.statement; this._services = options.services; this._startIndex = options.startIndex; this._url = options.url; this._columns = options.columns; this._mapColumnNameToIndices = options.mapColumnNameToIndices; this._chunkHeaders = options.chunkHeaders; this._rowset = options.rowset; // use the start index and row count to compute the end index this._endIndex = this._startIndex + options.rowCount - 1; // use the start and end index to build an id for this chunk this._id = buildId(this._startIndex, this._endIndex); } Util.inherits(Chunk, EventEmitter); /** * Returns the compressed size of this chunk's contents on S3/Blob. * A value of -1 means the contents of this chunk are not stored on S3/Blob. * * @returns {Number} */ Chunk.prototype.getCompressedSize = function () { return this._compressedSize; }; /** * Returns the uncompressed size of this chunk's contents on S3/Blob. * A value of -1 means the contents of this chunk are not stored on S3/Blob. * * @returns {Number} */ Chunk.prototype.getUncompressedSize = function () { return this._uncompressedSize; }; /** * Returns the row index of the first row in this chunk. * * @returns {Number} */ Chunk.prototype.getStartIndex = function () { return this._startIndex; }; /** * Returns the row index of the last row in this chunk. * * @returns {Number} */ Chunk.prototype.getEndIndex = function () { return this._endIndex; }; /** * Returns the id of this chunk. * * @returns {String} */ Chunk.prototype.getId = function () { return this._id; }; /** * Determines if this chunk overlaps with a given window. * * @param {Number} start The start index of the window. * @param {Number} end The end index of the window. * * @returns {Boolean} */ Chunk.prototype.overlapsWithWindow = function (start, end) { const chunkStart = this._startIndex, chunkEnd = this._endIndex; // check if the window overlaps with the chunk from the left or // from the right or from both sides return ((end <= chunkEnd && end >= chunkStart) || (start >= chunkStart && start <= chunkEnd) || (start <= chunkStart && end >= chunkEnd)); }; /** * Returns the rows in this chunk. * * @returns {Object[]} */ Chunk.prototype.getRows = function () { // if we don't already have a valid value for the rows if (!Util.isArray(this._rows)) { // if we have a string version of the rowset if (Util.string.isNotNullOrEmpty(this._rowsetAsString)) { // add square brackets on either side and do a json-parse to get the // rowset this._rowset = JSON.parse('[' + this._rowsetAsString + ']'); // we don't need the string version of the rowset anymore this._rowsetAsString = undefined; } // if we have a valid value for the rowset if (Util.isArray(this._rowset)) { // convert the rowset to an array of rows this._rows = convertRowsetToRows( this._statement, this._startIndex, this._rowset, this._columns, this._mapColumnNameToIndices); // clear out the rowset because we don't need it anymore this._rowset = undefined; } } return this._rows; }; /** * Clears out the rows in this chunk if it has a valid url or if the force flag * is true. * * @param force */ Chunk.prototype.clearRows = function (force) { if (Util.string.isNotNullOrEmpty(this._url) || force) { // clear out all row and rowset related fields this._rowsetAsString = this._rowset = this._rows = undefined; } }; /** * Updates this chunk's url. * * @param url */ Chunk.prototype.setUrl = function (url) { this._url = url; }; /** * Returns true if this chunk's contents are loaded, false otherwise. * * @returns {Boolean} */ Chunk.prototype.isLoaded = function () { // the chunk is considered loaded if we have valid values for // _rowsetAsString, _rowset or _rows return Util.string.isNotNullOrEmpty(this._rowsetAsString) || Util.isArray(this._rowset) || Util.isArray(this._rows); }; /** * Returns true if this chunk is currently in the process of loading its * contents, false otherwise. * * @returns {Boolean} */ Chunk.prototype.isLoading = function () { return this._isLoading; }; /** * Loads the contents of this chunk. * * @param callback */ Chunk.prototype.load = function (callback) { // we've started loading this._isLoading = true; const self = this; /** * Completes the chunk load. * * @param err */ const completeLoad = function (err) { // we're done loading self._isLoading = false; // emit an event to notify subscribers self.emit('loadcomplete', err, self); // invoke the callback if one was specified if (Util.isFunction(callback)) { callback(err, self); } }; // If the chunk is already loaded, complete the load asynchronously. // This ensure that we are streaming chunks the right order even if the data was received before // another chunk. if (this.isLoaded()) { process.nextTick(completeLoad); } else { // issue a request to load the chunk's contents from S3/Blob this._services.largeResultSet.getObject( { url: this._url, headers: this._chunkHeaders, callback: function (err, body) { // if the request succeeded, save the // body as a string version of the rowset if (!err) { self._rowsetAsString = body; } // complete the load completeLoad(err); } }); } }; /** * Builds an id for a chunk from its start and end index. * * @param startIndex The row index of the first chunk row. * @param endIndex The row index of the last chunk row. * * @returns {String} * @private */ function buildId(startIndex, endIndex) { return Util.format('s=%d, e=%d', startIndex, endIndex); } /** * Converts a rowset to an array of records. * * @param statement * @param startIndex the chunk start index. * @param rowset * @param columns * @param mapColumnNameToIndices * * @returns {Array} * @private */ function convertRowsetToRows( statement, startIndex, rowset, columns, mapColumnNameToIndices) { // assert that rowset and columns are arrays Errors.assertInternal(Util.isArray(rowset)); Errors.assertInternal(Util.isArray(columns)); /////////////////////////////////////////////////////////////////////////// //// Create functions that will be used as row methods //// /////////////////////////////////////////////////////////////////////////// /** * Returns the index of this row in the result. * * @returns {Number} */ const getRowIndex = function () { return this.rowIndex; }; /** * Returns the statement that produced this row. * * @returns {*} */ const getStatement = function getStatement() { return statement; }; /** * Returns the value of a column. * * @param {String | Number} columnIdentifier this can be either the column * name or the column index. * * @returns {*} */ const getColumnValue = function getColumnValue(columnIdentifier) { // resolve the column identifier to the correct column if possible const column = resolveColumnIdentifierToColumn( columns, columnIdentifier, mapColumnNameToIndices); return column ? column.getRowValue(this) : undefined; }; /** * Returns the value of a column as a String. * * @param {String | Number} columnIdentifier this can be either the column * name or the column index. * * @returns {*} */ const getColumnValueAsString = function getColumnValueAsString(columnIdentifier) { // resolve the column identifier to the correct column if possible const column = resolveColumnIdentifierToColumn( columns, columnIdentifier, mapColumnNameToIndices); return column ? column.getRowValueAsString(this) : undefined; }; /////////////////////////////////////////////////////////////////////////// //// Convert the rowset to an array of row objects //// /////////////////////////////////////////////////////////////////////////// // create a new array to store the processed rows const length = rowset.length; const rows = new Array(length); for (let index = 0; index < length; index++) { // add a new item to the rows array rows[index] = { _arrayProcessedColumns: [], values: rowset[index], rowIndex: startIndex + index, getRowIndex: getRowIndex, getStatement: getStatement, getColumnValue: getColumnValue, getColumnValueAsString: getColumnValueAsString }; } return rows; } /** * Resolves a column identifier to the corresponding column if possible. The * column identifier can be a column name or a column index. If an invalid * column identifier is specified, we return undefined. * * @param {Object[]} columns * @param {String | Number} columnIdentifier * @param {Object} mapColumnNameToIndices * * @returns {*} */ function resolveColumnIdentifierToColumn( columns, columnIdentifier, mapColumnNameToIndices) { let columnIndex; // if the column identifier is a string, treat it as a column // name and use it to get the index of the specified column if (Util.isString(columnIdentifier)) { // if a valid column name was specified, get the index of the first column // with the specified name if (Object.prototype.hasOwnProperty.call(mapColumnNameToIndices, columnIdentifier)) { columnIndex = mapColumnNameToIndices[columnIdentifier][0]; } } else if (Util.isNumber(columnIdentifier)) { // if the column identifier is a number, treat it as a column index columnIndex = columnIdentifier; } return columns[columnIndex]; } module.exports = Chunk;