UNPKG

snowflake-sdk

Version:
1,607 lines (1,404 loc) 49.6 kB
/* * Copyright (c) 2015-2024 Snowflake Computing Inc. All rights reserved. */ const { v4: uuidv4 } = require('uuid'); const Url = require('url'); const QueryString = require('querystring'); const EventEmitter = require('events').EventEmitter; const Util = require('../util'); const Result = require('./result/result'); const Parameters = require('../parameters'); const RowStream = require('./result/row_stream'); const Errors = require('../errors'); const ErrorCodes = Errors.codes; const Logger = require('../logger'); const NativeTypes = require('./result/data_types').NativeTypes; const FileTransferAgent = require('../file_transfer_agent/file_transfer_agent'); const Bind = require('./bind_uploader'); const RowMode = require('./../constants/row_mode'); const states = { FETCHING: 'fetching', COMPLETE: 'complete' }; const statementTypes = { ROW_PRE_EXEC: 'ROW_PRE_EXEC', ROW_POST_EXEC: 'ROW_POST_EXEC', FILE_PRE_EXEC: 'FILE_PRE_EXEC', FILE_POST_EXEC: 'FILE_POST_EXEC' }; const queryCodes = { QUERY_IN_PROGRESS: '333333', // GS code: the query is in progress QUERY_IN_PROGRESS_ASYNC: '333334' // GS code: the query is detached }; exports.createContext = function ( options, services, connectionConfig) { // create a statement context for a pre-exec statement const context = createContextPreExec( options, services, connectionConfig); context.type = statementTypes.FILE_PRE_EXEC; createStatement(options, context, services, connectionConfig); // add the result request headers to the context context.resultRequestHeaders = buildResultRequestHeadersFile(); return context; }; function createStatement( statementOptions, context, services, connectionConfig) { // call super BaseStatement.apply(this, [statementOptions, context, services, connectionConfig]); } /** * Check the type of command to execute. * * @param {Object} options * @param {Object} services * @param {Object} connectionConfig * * @returns {Object} */ exports.createStatementPreExec = function ( options, services, connectionConfig) { Logger.getInstance().debug('--createStatementPreExec'); // create a statement context for a pre-exec statement const context = createContextPreExec( options, services, connectionConfig); if (options.sqlText && (Util.isPutCommand(options.sqlText) || Util.isGetCommand(options.sqlText))) { if (options.fileStream) { context.fileStream = options.fileStream; options.fileStream = null; } return createFileStatementPreExec( options, context, services, connectionConfig); } const numBinds = countBinding(context.binds); Logger.getInstance().debug('numBinds = %d', numBinds); let threshold = Parameters.getValue(Parameters.names.CLIENT_STAGE_ARRAY_BINDING_THRESHOLD); if (connectionConfig.getbindThreshold()) { threshold = connectionConfig.getbindThreshold(); } Logger.getInstance().debug('threshold = %d', threshold); // check array binding, if (numBinds > threshold) { return createStageStatementpreExec(options, context, services, connectionConfig); } else { return createRowStatementPreExec( options, context, services, connectionConfig); } }; /** * Executes a statement and returns a statement object that can be used to fetch * its result. * * @param {Object} statementOptions * @param {Object} statementContext * @param {Object} services * @param {Object} connectionConfig * * @returns {Object} */ function createRowStatementPreExec( statementOptions, statementContext, services, connectionConfig) { // set the statement type statementContext.type = statementTypes.ROW_PRE_EXEC; return new RowStatementPreExec( statementOptions, statementContext, services, connectionConfig); } /** * Creates a statement object that can be used to fetch the result of a * previously executed statement. * * @param {Object} statementOptions * @param {Object} services * @param {Object} connectionConfig * * @returns {Object} */ exports.createStatementPostExec = function ( statementOptions, services, connectionConfig) { // check for missing options Errors.checkArgumentExists(Util.exists(statementOptions), ErrorCodes.ERR_CONN_FETCH_RESULT_MISSING_OPTIONS); // check for invalid options Errors.checkArgumentValid(Util.isObject(statementOptions), ErrorCodes.ERR_CONN_FETCH_RESULT_INVALID_OPTIONS); // check for missing query id Errors.checkArgumentExists(Util.exists(statementOptions.queryId), ErrorCodes.ERR_CONN_FETCH_RESULT_MISSING_QUERY_ID); // check for invalid query id Errors.checkArgumentValid(Util.isString(statementOptions.queryId), ErrorCodes.ERR_CONN_FETCH_RESULT_INVALID_QUERY_ID); // check for invalid complete callback const complete = statementOptions.complete; if (Util.exists(complete)) { Errors.checkArgumentValid(Util.isFunction(complete), ErrorCodes.ERR_CONN_FETCH_RESULT_INVALID_COMPLETE); } // check for invalid streamResult if (Util.exists(statementOptions.streamResult)) { Errors.checkArgumentValid(Util.isBoolean(statementOptions.streamResult), ErrorCodes.ERR_CONN_FETCH_RESULT_INVALID_STREAM_RESULT); } // check for invalid fetchAsString const fetchAsString = statementOptions.fetchAsString; if (Util.exists(fetchAsString)) { // check that the value is an array Errors.checkArgumentValid(Util.isArray(fetchAsString), ErrorCodes.ERR_CONN_FETCH_RESULT_INVALID_FETCH_AS_STRING); // check that all the array elements are valid const invalidValueIndex = NativeTypes.findInvalidValue(fetchAsString); Errors.checkArgumentValid(invalidValueIndex === -1, ErrorCodes.ERR_CONN_FETCH_RESULT_INVALID_FETCH_AS_STRING_VALUES, JSON.stringify(fetchAsString[invalidValueIndex])); } const rowMode = statementOptions.rowMode; if (Util.exists(rowMode)) { RowMode.checkRowModeValid(rowMode); } const cwd = statementOptions.cwd; if (Util.exists(cwd)) { Errors.checkArgumentValid(Util.isString(cwd), ErrorCodes.ERR_CONN_FETCH_RESULT_INVALID_CWD); } // validate non-user-specified arguments Errors.assertInternal(Util.isObject(services)); Errors.assertInternal(Util.isObject(connectionConfig)); // create a statement context const statementContext = createStatementContext(); statementContext.queryId = statementOptions.queryId; statementContext.complete = complete; statementContext.streamResult = statementOptions.streamResult; statementContext.fetchAsString = statementOptions.fetchAsString; statementContext.multiResultIds = statementOptions.multiResultIds; statementContext.multiCurId = statementOptions.multiCurId; statementContext.rowMode = statementOptions.rowMode; statementContext.cwd = statementOptions.cwd; // set the statement type statementContext.type = (statementContext.type === statementTypes.ROW_PRE_EXEC) ? statementTypes.ROW_POST_EXEC : statementTypes.FILE_POST_EXEC; return new StatementPostExec( statementOptions, statementContext, services, connectionConfig); }; /** * Creates a new statement context object. * * @returns {Object} */ function createStatementContext() { return new EventEmitter(); } /** * Creates a statement object that can be used to execute a PUT or GET file * operation. * * @param {Object} statementOptions * @param {Object} statementContext * @param {Object} services * @param {Object} connectionConfig * * @returns {Object} */ function createFileStatementPreExec( statementOptions, statementContext, services, connectionConfig) { // set the statement type statementContext.type = statementTypes.FILE_PRE_EXEC; return new FileStatementPreExec( statementOptions, statementContext, services, connectionConfig); } /** * Creates a statement object that can be used to execute stage binding * operation. * * @param {Object} statementOptions * @param {Object} statementContext * @param {Object} services * @param {Object} connectionConfig * * @returns {Object} */ function createStageStatementpreExec( statementOptions, statementContext, services, connectionConfig) { return new StageBindingStatementPreExec(statementOptions, statementContext, services, connectionConfig); } /** * Creates a statement context object for pre-exec statement. * * @param {Object} statementOptions * @param {Object} services * @param {Object} connectionConfig * * @returns {Object} */ function createContextPreExec( statementOptions, services, connectionConfig) { // check for missing options Errors.checkArgumentExists(Util.exists(statementOptions), ErrorCodes.ERR_CONN_EXEC_STMT_MISSING_OPTIONS); // check for invalid options Errors.checkArgumentValid(Util.isObject(statementOptions), ErrorCodes.ERR_CONN_EXEC_STMT_INVALID_OPTIONS); if (!Util.exists(statementOptions.requestId)) { // check for missing sql text Errors.checkArgumentExists(Util.exists(statementOptions.sqlText), ErrorCodes.ERR_CONN_EXEC_STMT_MISSING_SQL_TEXT); // check for invalid sql text Errors.checkArgumentValid(Util.isString(statementOptions.sqlText), ErrorCodes.ERR_CONN_EXEC_STMT_INVALID_SQL_TEXT); } // check for invalid complete callback const complete = statementOptions.complete; if (Util.exists(complete)) { Errors.checkArgumentValid(Util.isFunction(complete), ErrorCodes.ERR_CONN_EXEC_STMT_INVALID_COMPLETE); } // check for invalid streamResult if (Util.exists(statementOptions.streamResult)) { Errors.checkArgumentValid(Util.isBoolean(statementOptions.streamResult), ErrorCodes.ERR_CONN_EXEC_STMT_INVALID_STREAM_RESULT); } // check for invalid fetchAsString const fetchAsString = statementOptions.fetchAsString; if (Util.exists(fetchAsString)) { // check that the value is an array Errors.checkArgumentValid(Util.isArray(fetchAsString), ErrorCodes.ERR_CONN_EXEC_STMT_INVALID_FETCH_AS_STRING); // check that all the array elements are valid const invalidValueIndex = NativeTypes.findInvalidValue(fetchAsString); Errors.checkArgumentValid(invalidValueIndex === -1, ErrorCodes.ERR_CONN_EXEC_STMT_INVALID_FETCH_AS_STRING_VALUES, JSON.stringify(fetchAsString[invalidValueIndex])); } // check for invalid requestId if (Util.exists(statementOptions.requestId)) { Errors.checkArgumentValid(Util.isString(statementOptions.requestId), ErrorCodes.ERR_CONN_EXEC_STMT_INVALID_REQUEST_ID); } // if parameters are specified, make sure the specified value is an object if (Util.exists(statementOptions.parameters)) { Errors.checkArgumentValid(Util.isObject(statementOptions.parameters), ErrorCodes.ERR_CONN_EXEC_STMT_INVALID_PARAMETERS); } // if binds are specified const binds = statementOptions.binds; if (Util.exists(binds)) { // make sure the specified value is an array Errors.checkArgumentValid(Util.isArray(binds), ErrorCodes.ERR_CONN_EXEC_STMT_INVALID_BINDS); // make sure everything in the binds array is stringifiable for (let index = 0, length = binds.length; index < length; index++) { Errors.checkArgumentValid(JSON.stringify(binds[index]) !== undefined, ErrorCodes.ERR_CONN_EXEC_STMT_INVALID_BIND_VALUES, binds[index]); } } // if an internal option is specified, make sure it's boolean if (Util.exists(statementOptions.internal)) { Errors.checkArgumentValid(Util.isBoolean(statementOptions.internal), ErrorCodes.ERR_CONN_EXEC_STMT_INVALID_INTERNAL); } const rowMode = statementOptions.rowMode; if (Util.exists(rowMode)) { RowMode.checkRowModeValid(rowMode); } // if an asyncExec flag is specified, make sure it's boolean if (Util.exists(statementOptions.asyncExec)) { Errors.checkArgumentValid(Util.isBoolean(statementOptions.asyncExec), ErrorCodes.ERR_CONN_EXEC_STMT_INVALID_ASYNC_EXEC); } // create a statement context const statementContext = createStatementContext(); statementContext.sqlText = statementOptions.sqlText; statementContext.complete = complete; statementContext.streamResult = statementOptions.streamResult; statementContext.fetchAsString = statementOptions.fetchAsString; statementContext.multiResultIds = statementOptions.multiResultIds; statementContext.multiCurId = statementOptions.multiCurId; statementContext.rowMode = statementOptions.rowMode; statementContext.asyncExec = statementOptions.asyncExec; // if a binds array is specified, add it to the statement context if (Util.exists(statementOptions.binds)) { statementContext.binds = statementOptions.binds; } // if parameters are specified, add them to the statement context if (Util.exists(statementOptions.parameters)) { statementContext.parameters = statementOptions.parameters; } // if the internal flag is specified, add it to the statement context if (Util.exists(statementOptions.internal)) { statementContext.internal = statementOptions.internal; } if (Util.exists(statementOptions.cwd)) { statementContext.cwd = statementOptions.cwd; } // validate non-user-specified arguments Errors.assertInternal(Util.isObject(services)); Errors.assertInternal(Util.isObject(connectionConfig)); // use request id passed by user if (statementOptions.requestId) { statementContext.requestId = statementOptions.requestId; statementContext.resubmitRequest = true; } else { // use a random uuid for the statement request id statementContext.requestId = uuidv4(); } return statementContext; } /** * Creates a new BaseStatement. * * @param statementOptions * @param context * @param services * @param connectionConfig * @constructor */ function BaseStatement( statementOptions, context, services, connectionConfig) { // call super EventEmitter.call(this); // validate input Errors.assertInternal(Util.isObject(statementOptions)); Errors.assertInternal(Util.isObject(context)); context.services = services; context.connectionConfig = connectionConfig; context.isFetchingResult = true; context.rowMode = statementOptions.rowMode || connectionConfig.getRowMode(); // TODO: add the parameters map to the statement context const statement = this; /** * Returns this statement's SQL text. * * @returns {String} */ this.getSqlText = function () { return context.sqlText; }; /** * Returns the current status of this statement. * * @returns {String} */ this.getStatus = function () { return context.isFetchingResult ? states.FETCHING : states.COMPLETE; }; /** * Returns the columns produced by this statement. * * @returns {Object[]} */ this.getColumns = function () { return context.result ? context.result.getColumns() : undefined; }; /** * Given a column identifier, returns the corresponding column. The column * identifier can be either the column name (String) or the column index * (Number). If a column is specified and there is more than one column with * that name, the first column with the specified name will be returned. * * @param {String | Number} columnIdentifier * * @returns {Object} */ this.getColumn = function (columnIdentifier) { return context.result ? context.result.getColumn(columnIdentifier) : undefined; }; /** * Returns the number of rows returned by this statement. * * @returns {Number} */ this.getNumRows = function () { return context.result ? context.result.getReturnedRows() : undefined; }; /** * Returns the number of rows updated by this statement. * * @returns {Number} */ this.getNumUpdatedRows = function () { return context.result ? context.result.getNumUpdatedRows() : undefined; }; /** * Returns an object that contains information about the values of the * current warehouse, current database, etc., when this statement finished * executing. * * @returns {Object} */ this.getSessionState = function () { return context.result ? context.result.getSessionState() : undefined; }; /** * Returns the request id that was used when the statement was issued. * * @returns {String} */ this.getRequestId = function () { return context.requestId; }; /** * Returns the query id generated by the server for this statement. * If the statement is still executing and we don't know the query id * yet, this method will return undefined. * * Should use getQueryId instead. * @deprecated * @returns {String} */ this.getStatementId = function () { return context.queryId; }; /** * Returns the query id generated by the server for this statement. * If the statement is still executing and we don't know the query id * yet, this method will return undefined. * * @returns {String} */ this.getQueryId = function () { return context.queryId; }; /** * Cancels this statement if possible. * * @param {Function} [callback] */ this.cancel = function (callback) { sendCancelStatement(context, statement, callback); }; //Integration Testing purpose. this.getQueryContextCacheSize = function () { return services.sf.getQueryContextCacheSize(); }; this.getQueryContextDTOSize = function () { return services.sf.getQueryContextDTO().entries.length; }; /** * Issues a request to get the statement result again. * * @param {Function} callback */ context.refresh = function (callback) { // pick the appropriate function to get the result based on whether we // have the query id or request id (we should have at least one) const sendRequestFn = context.queryId ? sendRequestPostExec : sendRequestPreExec; // the current result error might be transient, // so issue a request to get the result again sendRequestFn(context, function (err, body) { // refresh the result context.onStatementRequestComp(err, body); // if a callback was specified, invoke it if (Util.isFunction(callback)) { callback(context); } }); }; /** * Called when the statement request is complete. * * @param err * @param body */ context.onStatementRequestComp = async function (err, body) { // if we already have a result or a result error, we invoked the complete // callback once, so don't invoke it again const suppressComplete = context.result || context.resultError; // clear the previous result error context.resultError = null; // if there was no error, call the success function if (!err) { await context.onStatementRequestSucc(body); } else { // save the error context.resultError = err; // if we don't have a query id and we got a response from GS, extract // the query id from the data if (!context.queryId && Errors.isOperationFailedError(err) && err.data) { context.queryId = err.data.queryId; } } // we're no longer fetching the result context.isFetchingResult = false; if (!suppressComplete) { // emit a complete event context.emit('statement-complete', Errors.externalize(err), statement); // if a complete function was specified, invoke it if (Util.exists(context.complete)) { invokeStatementComplete(statement, context); } } else { Logger.getInstance().debug('refreshed result of statement with %s', context.requestId ? Util.format('request id = %s', context.requestId) : Util.format('query id = %s', context.queryId)); } }; /** * Called when the statement request is successful. Subclasses must provide * their own implementation. */ context.onStatementRequestSucc = function () { }; } Util.inherits(BaseStatement, EventEmitter); /** * Invokes the statement complete callback. * * @param {Object} statement * @param {Object} context */ function invokeStatementComplete(statement, context) { // find out if the result will be streamed; // if a value is not specified, get it from the connection let streamResult = context.streamResult; if (!Util.exists(streamResult)) { streamResult = context.connectionConfig.getStreamResult(); } // if the result will be streamed later or in asyncExec mode, // invoke the complete callback right away if (streamResult) { context.complete(Errors.externalize(context.resultError), statement); } else if (context.asyncExec) { // return the result object with the query ID inside. context.complete(null, statement, context.result); } else { process.nextTick(function () { // aggregate all the rows into an array and pass this // array to the complete callback as the last argument const rows = []; statement.streamRows() .on('readable', function () { // read only when data is available let row; // while there are rows available to read, push row to results array while ((row = this.read()) !== null) { rows.push(row); } }) .on('end', function () { context.complete(null, statement, rows); }) .on('error', function (err) { context.complete(Errors.externalize(err), statement); }); }); } } /** * Creates a new RowStatementPreExec instance. * * @param {Object} statementOptions * @param {Object} context * @param {Object} services * @param {Object} connectionConfig * @constructor */ function RowStatementPreExec( statementOptions, context, services, connectionConfig) { Logger.getInstance().debug('RowStatementPreExec'); // call super BaseStatement.apply(this, [statementOptions, context, services, connectionConfig]); // add the result request headers to the context context.resultRequestHeaders = buildResultRequestHeadersRow(); /** * Called when the request to get the statement result is successful. * * @param {Object} body */ context.onStatementRequestSucc = createOnStatementRequestSuccRow(this, context); /** * Fetches the rows in this statement's result and invokes the each() * callback on each row. If start and end values are specified, the each() * callback will only be invoked on rows in the specified range. * * @param {Object} options */ this.fetchRows = createFnFetchRows(this, context); /** * Streams the rows in this statement's result. If start and end values are * specified, only rows in the specified range are streamed. * * @param {Object} options */ this.streamRows = createFnStreamRows(this, context); // send a request to execute the statement sendRequestPreExec(context, context.onStatementRequestComp); } Util.inherits(RowStatementPreExec, BaseStatement); /** * Creates a function that can be used by row statements to process the response * when the request is successful. * * @param statement * @param context * @returns {Function} */ function createOnStatementRequestSuccRow(statement, context) { return function (body) { // if we don't already have a result if (!context.result) { if (body.code === queryCodes.QUERY_IN_PROGRESS_ASYNC) { context.result = { queryId: body.data.queryId }; return; } if (body.data.resultIds !== undefined && body.data.resultIds.length > 0) { //multi statements this._resultIds = body.data.resultIds.split(','); context.isMulti = true; context.multiResultIds = this._resultIds; context.multiCurId = 0; context.queryId = this._resultIds[context.multiCurId]; exports.createStatementPostExec(context, context.services, context.connectionConfig); } else { // build a result from the response context.result = new Result( { response: body, statement: statement, services: context.services, connectionConfig: context.connectionConfig, rowMode: context.rowMode }); context.queryId = context.result.getQueryId(); this.services.sf.deserializeQueryContext(context.result.getQueryContext()); } } else { // refresh the existing result context.result.refresh(body); } if (context.isMulti == null || context.isMulti === false) { // only update the parameters if the statement isn't a post-exec statement if (context.type !== statementTypes.ROW_POST_EXEC || context.type !== statementTypes.FILE_POST_EXEC) { Parameters.update(context.result.getParametersArray()); } } }; } /** * Creates a new FileStatementPreExec instance. * * @param {Object} statementOptions * @param {Object} context * @param {Object} services * @param {Object} connectionConfig * @constructor */ function FileStatementPreExec( statementOptions, context, services, connectionConfig) { // call super BaseStatement.apply(this, [statementOptions, context, services, connectionConfig]); // add the result request headers to the context context.resultRequestHeaders = buildResultRequestHeadersFile(); /** * Called when the statement request is successful. * * @param {Object} body */ context.onStatementRequestSucc = async function (body) { context.fileMetadata = body; const fta = new FileTransferAgent(context); await fta.execute(); // build a result from the response const result = fta.result(); // init result and meta body.data.rowset = result.rowset; body.data.returned = body.data.rowset.length; body.data.rowtype = result.rowtype; body.data.parameters = []; context.result = new Result({ response: body, statement: this, services: context.services, connectionConfig: context.connectionConfig }); }; /** * Streams the rows in this statement's result. If start and end values are * specified, only rows in the specified range are streamed. * * @param {Object} options */ this.streamRows = createFnStreamRows(this, context); this.hasNext = hasNextResult(this, context); this.NextResult = createNextReuslt(this, context); /** * Returns the file metadata generated by the statement. * * @returns {Object} */ this.getFileMetadata = function () { return context.fileMetadata; }; // send a request to execute the file statement sendRequestPreExec(context, context.onStatementRequestComp); } Util.inherits(FileStatementPreExec, BaseStatement); /** * Creates a new StageBindingStatementPreExec instance. * * @param {Object} statementOptions * @param {Object} context * @param {Object} services * @param {Object} connectionConfig * @constructor */ function StageBindingStatementPreExec( statementOptions, context, services, connectionConfig) { // call super BaseStatement.apply(this, arguments); // add the result request headers to the context context.resultRequestHeaders = buildResultRequestHeadersFile(); /** * Called when the statement request is successful. Subclasses must provide * their own implementation. */ context.onStatementRequestSucc = function () { //do nothing }; /** * Called the stage binding request * * @param {Object} options * @param {Object} context * @param {Object} services * @param {Object} connectionConfig */ this.StageBindingRequest = async function (options, context, services, connectionConfig) { try { const bindUploaderRequestId = uuidv4(); const bind = new Bind.BindUploader(options, services, connectionConfig, bindUploaderRequestId); context.bindStage = Bind.GetStageName(bindUploaderRequestId); await bind.Upload(context.binds); return createRowStatementPreExec( options, context, services, connectionConfig); } catch (error) { context.bindStage = null; return createRowStatementPreExec( options, context, services, connectionConfig); } }; /** * Fetches the rows in this statement's result and invokes the each() * callback on each row. If start and end values are specified, the each() * callback will only be invoked on rows in the specified range. * * @param {Object} options */ this.fetchRows = createFnFetchRows(this, context); /** * Streams the rows in this statement's result. If start and end values are * specified, only rows in the specified range are streamed. * * @param {Object} options */ this.streamRows = createFnStreamRows(this, context); this.hasNext = hasNextResult(this, context); this.NextResult = createNextReuslt(this, context); this.StageBindingRequest(statementOptions, context, services, connectionConfig); } Util.inherits(StageBindingStatementPreExec, BaseStatement); /** * Creates a new StatementPostExec instance. * * @param {Object} statementOptions * @param {Object} context * @param {Object} services * @param {Object} connectionConfig * @constructor */ function StatementPostExec( statementOptions, context, services, connectionConfig) { // call super BaseStatement.apply(this, [statementOptions, context, services, connectionConfig]); // add the result request headers to the context context.resultRequestHeaders = buildResultRequestHeadersRow(); /** * Called when the statement request is successful. * * @param {Object} body */ context.onStatementRequestSucc = createOnStatementRequestSuccRow(this, context); /** * Fetches the rows in this statement's result and invokes the each() * callback on each row. If startIndex and endIndex values are specified, the * each() callback will only be invoked on rows in the requested range. The * end() callback will be invoked when either all the requested rows have been * successfully processed, or if an error was encountered while trying to * fetch the requested rows. * * @param {Object} options */ this.fetchRows = createFnFetchRows(this, context); /** * Streams the rows in this statement's result. If start and end values are * specified, only rows in the specified range are streamed. * * @param {Object} options */ this.streamRows = createFnStreamRows(this, context); this.hasNext = hasNextResult(this, context); this.NextResult = createNextReuslt(this, context); // send a request to fetch the result sendRequestPostExec(context, context.onStatementRequestComp); } Util.inherits(StatementPostExec, BaseStatement); /** * Creates a function that fetches the rows in a statement's result and * invokes the each() callback on each row. If start and end values are * specified, the each() callback will only be invoked on rows in the * specified range. * * @param statement * @param context */ function createFnFetchRows(statement, context) { return function (options) { // check for missing options Errors.checkArgumentExists(Util.exists(options), ErrorCodes.ERR_STMT_FETCH_ROWS_MISSING_OPTIONS); // check for invalid options Errors.checkArgumentValid(Util.isObject(options), ErrorCodes.ERR_STMT_FETCH_ROWS_INVALID_OPTIONS); // check for missing each() Errors.checkArgumentExists(Util.exists(options.each), ErrorCodes.ERR_STMT_FETCH_ROWS_MISSING_EACH); // check for invalid each() Errors.checkArgumentValid(Util.isFunction(options.each), ErrorCodes.ERR_STMT_FETCH_ROWS_INVALID_EACH); // check for missing end() Errors.checkArgumentExists(Util.exists(options.end), ErrorCodes.ERR_STMT_FETCH_ROWS_MISSING_END); // check for invalid end() Errors.checkArgumentValid(Util.isFunction(options.end), ErrorCodes.ERR_STMT_FETCH_ROWS_INVALID_END); const rowMode = options.rowMode; if (Util.exists(rowMode)) { RowMode.checkRowModeValid(rowMode); } // if we're still trying to fetch the result, create an error of our own // and invoke the end() callback if (context.isFetchingResult) { process.nextTick(function () { options.end(Errors.createClientError( ErrorCodes.ERR_STMT_FETCH_ROWS_FETCHING_RESULT).externalize(), statement); }); } else if (context.resultError) { // if there was an error the last time we tried to get the result // if we have a fatal error, end the fetch rows operation since we're not // going to be able to get any rows, either because the statement failed // or because the result's been purged if (Errors.isOperationFailedError(context.resultError) && context.resultError.sqlState) { process.nextTick(function () { endFetchRows(options, statement, context); }); } else { context.refresh(function () { // if there was no error, fetch rows from the result if (!context.resultError) { fetchRowsFromResult(options, statement, context); } else { // give up because it's unlikely we'll succeed if we retry again endFetchRows(options, statement, context); } }); } } else { fetchRowsFromResult(options, statement, context); } }; } /** * Creates a function that streams the rows in a statement's result. If start * and end values are specified, only rows in the specified range are streamed. * * @param statement * @param context */ function createFnStreamRows(statement, context) { return function (options) { // if some options are specified if (Util.exists(options)) { // check for invalid options Errors.checkArgumentValid(Util.isObject(options), ErrorCodes.ERR_STMT_FETCH_ROWS_INVALID_OPTIONS); // check for invalid start if (Util.exists(options.start)) { Errors.checkArgumentValid(Util.isNumber(options.start), ErrorCodes.ERR_STMT_STREAM_ROWS_INVALID_START); } // check for invalid end if (Util.exists(options.end)) { Errors.checkArgumentValid(Util.isNumber(options.end), ErrorCodes.ERR_STMT_STREAM_ROWS_INVALID_END); } // check for invalid fetchAsString const fetchAsString = options.fetchAsString; if (Util.exists(fetchAsString)) { // check that the value is an array Errors.checkArgumentValid(Util.isArray(fetchAsString), ErrorCodes.ERR_STMT_STREAM_ROWS_INVALID_FETCH_AS_STRING); // check that all the array elements are valid const invalidValueIndex = NativeTypes.findInvalidValue(fetchAsString); Errors.checkArgumentValid(invalidValueIndex === -1, ErrorCodes.ERR_STMT_STREAM_ROWS_INVALID_FETCH_AS_STRING_VALUES, JSON.stringify(fetchAsString[invalidValueIndex])); } const rowMode = context.rowMode; if (Util.exists(rowMode)) { RowMode.checkRowModeValid(rowMode); } } return new RowStream(statement, context, options); }; } /** * Ends the fetchRows() operation. * * @param {Object} options the options passed to fetchRows(). * @param {Object} statement * @param {Object} context */ function endFetchRows(options, statement, context) { options.end(Errors.externalize(context.resultError), statement); } /** * Fetches rows from the statement's result. * * @param {Object} options the options passed to fetchRows(). * @param {Object} statement * @param {Object} context */ function fetchRowsFromResult(options, statement, context) { let numInterrupts = 0; // forward to the result to get a FetchRowsOperation object const operation = context.result.fetchRows(options); // subscribe to the operation's 'complete' event operation.on('complete', function (err, continueCallback) { // we want to retry if the error is retryable and the // result stream hasn't been closed too many times if (Errors.isLargeResultSetError(err) && err.response && (err.response.statusCode === 403) && (numInterrupts < context.connectionConfig.getResultStreamInterrupts())) { // increment the interrupt counter numInterrupts++; // issue a request to fetch the result again sendRequestPostExec(context, function (err, body) { // refresh the result context.onStatementRequestComp(err, body); // if there was no error, continue from where we got interrupted if (!err) { continueCallback(); } }); } else { endFetchRows(options, statement, context); } }); } /** * Issues a request to cancel a statement. * * @param {Object} statementContext * @param {Object} statement * @param {Function} callback */ function sendCancelStatement(statementContext, statement, callback) { let url; let json; // use different rest endpoints based on whether the query id is available if (statementContext.queryId) { url = '/queries/' + statementContext.queryId + '/abort-request'; } else { url = '/queries/v1/abort-request'; json = { requestId: statementContext.requestId }; } // issue a request to cancel the statement statementContext.services.sf.request( { method: 'POST', url: url, json: json, callback: function (err) { // if a callback was specified, invoke it if (Util.isFunction(callback)) { callback(Errors.externalize(err), statement); } } }); } /** * Issues a request to get the result of a statement that hasn't been previously * executed. * * @param statementContext * @param onResultAvailable */ function sendRequestPreExec(statementContext, onResultAvailable) { // get the request headers const headers = statementContext.resultRequestHeaders; // build the basic json for the request const json = { disableOfflineChunks: false, }; json.sqlText = statementContext.sqlText; if (statementContext.resubmitRequest && !json.sqlText) { json.sqlText = `SELECT 'Error retrieving query results for request id: ${statementContext.requestId}, ` + 'please use RESULT_SCAN instead\' AS ErrorMessage;'; } Logger.getInstance().debug('context.bindStage=' + statementContext.bindStage); if (Util.exists(statementContext.bindStage)) { json.bindStage = statementContext.bindStage; } else if (Util.exists(statementContext.binds)) { // if binds are specified, build a binds map and include it in the request json.bindings = buildBindsMap(statementContext.binds); } // include statement parameters if a value was specified if (Util.exists(statementContext.parameters)) { json.parameters = statementContext.parameters; Logger.getInstance().debug('context.parameters=' + statementContext.parameters); } // include the internal flag if a value was specified if (Util.exists(statementContext.internal)) { json.isInternal = statementContext.internal; } if (!statementContext.disableQueryContextCache){ json.queryContextDTO = statementContext.services.sf.getQueryContextDTO(); } // include the asyncExec flag if a value was specified if (Util.exists(statementContext.asyncExec)) { json.asyncExec = statementContext.asyncExec; } // use the snowflake service to issue the request sendSfRequest(statementContext, { method: 'POST', headers: headers, url: Url.format( { pathname: '/queries/v1/query-request', search: QueryString.stringify( { requestId: statementContext.requestId }) }), json: json, callback: buildResultRequestCallback( statementContext, headers, onResultAvailable) }, true); } this.sendRequest = function (statementContext, onResultAvailable) { // get the request headers const headers = statementContext.resultRequestHeaders; // build the basic json for the request const json = { disableOfflineChunks: false, sqlText: statementContext.sqlText }; Logger.getInstance().debug('context.bindStage=' + statementContext.bindStage); if (Util.exists(statementContext.bindStage)) { json.bindStage = statementContext.bindStage; } else if (Util.exists(statementContext.binds)) { // if binds are specified, build a binds map and include it in the request json.bindings = buildBindsMap(statementContext.binds); } // include statement parameters if a value was specified if (Util.exists(statementContext.parameters)) { json.parameters = statementContext.parameters; } // include the internal flag if a value was specified if (Util.exists(statementContext.internal)) { json.isInternal = statementContext.internal; } if (!statementContext.disableQueryContextCache){ json.queryContextDTO = statementContext.services.sf.getQueryContextDTO(); } let options = { method: 'POST', headers: headers, url: Url.format( { pathname: '/queries/v1/query-request', search: QueryString.stringify( { requestId: statementContext.requestId }) }), json: json, callback: buildResultRequestCallback( statementContext, headers, onResultAvailable) }; const sf = statementContext.services.sf; // clone the options options = Util.apply({}, options); return new Promise((resolve) => { resolve(sf.postAsync(options)); }); }; /** * Converts a bind variables array to a map that can be included in the * POST-body when issuing a pre-exec statement request. * * @param bindsArray * * @returns {Object} */ function buildBindsMap(bindsArray) { const bindsMap = {}; const isArrayBinding = bindsArray.length > 0 && Util.isArray(bindsArray[0]); const singleArray = isArrayBinding ? bindsArray[0] : bindsArray; for (let index = 0, length = singleArray.length; index < length; index++) { let value = singleArray[index]; // pick the appropriate logical data type based on the bind value let type; if (Util.isBoolean(value)) { type = 'BOOLEAN'; } else if (Util.isObject(value) || Util.isArray(value)) { type = 'VARIANT'; } else if (Util.isNumber(value)) { if (Number(value) === value && value % 1 === 0) { // if value is integer type = 'FIXED'; } else { type = 'REAL'; } } else { type = 'TEXT'; } // convert non-null values to a string if necessary; we don't convert null // because the client might want to run something like // sql text = update t set name = :1 where id = 1;, binds = [null] // and converting null to a string would result in us executing // sql text = update t set name = 'null' where id = 1; // instead of // sql text = update t set name = null where id = 1; if (!isArrayBinding) { if (value !== null && !Util.isString(value)) { if (value instanceof Date) { value = value.toJSON(); } else { value = JSON.stringify(value); } } } else { value = []; for (let rowIndex = 0; rowIndex < bindsArray.length; rowIndex++) { let value0 = bindsArray[rowIndex][index]; if (value0 !== null && !Util.isString(value0)) { if (value0 instanceof Date) { value0 = value0.toJSON(); } else { value0 = JSON.stringify(value0); } } value.push(value0); } } // add an entry for the bind variable to the map bindsMap[index + 1] = { type: type, value: value }; } return bindsMap; } /** * Issues a request to get the result of a statement that has been previously * executed. * * @param statementContext * @param onResultAvailable */ function sendRequestPostExec(statementContext, onResultAvailable) { // get the request headers const headers = statementContext.resultRequestHeaders; // use the snowflake service to issue the request sendSfRequest(statementContext, { method: 'GET', headers: headers, url: Url.format( { pathname: '/queries/' + statementContext.queryId + '/result', search: QueryString.stringify( { disableOfflineChunks: false }) }), callback: buildResultRequestCallback( statementContext, headers, onResultAvailable) }); } /** * Issues a statement-related request using the Snowflake service. * * @param {Object} statementContext the statement context. * @param {Object} options the request options. * @param {Boolean} [appendQueryParamOnRetry] whether retry=true should be * appended to the url if the request is retried. */ function sendSfRequest(statementContext, options, appendQueryParamOnRetry) { const sf = statementContext.services.sf; const connectionConfig = statementContext.connectionConfig; // clone the options options = Util.apply({}, options); // get the original url and callback const urlOrig = options.url; const callbackOrig = options.callback; let numRetries = 0; const maxNumRetries = connectionConfig.getRetrySfMaxNumRetries(); let sleep = connectionConfig.getRetrySfStartingSleepTime(); let lastStatusCodeForRetry; // create a function to send the request const sendRequest = function () { // if this is a retry and a query parameter should be appended to the url on // retry, update the url if ((numRetries > 0) && appendQueryParamOnRetry) { const retryOption = { url: urlOrig, retryCount: numRetries, retryReason: lastStatusCodeForRetry, includeRetryReason: connectionConfig.getIncludeRetryReason(), }; options.url = Util.url.appendRetryParam(retryOption); } sf.request(options); }; // replace the specified callback with a new one that retries options.callback = async function (err) { // if we haven't exceeded the maximum number of retries yet and the server // came back with a retryable error code if (numRetries < maxNumRetries && err && Util.isRetryableHttpError( err.response, false // no retry for HTTP 403 )) { // increment the retry count numRetries++; lastStatusCodeForRetry = err.response ? err.response.statusCode : 0; // use exponential backoff with decorrelated jitter to compute the // next sleep time. const cap = connectionConfig.getRetrySfMaxSleepTime(); sleep = Util.nextSleepTime(1, cap, sleep); Logger.getInstance().debug( 'Retrying statement with request id %s, retry count = %s', statementContext.requestId, numRetries); // wait the appropriate amount of time before retrying the request setTimeout(sendRequest, sleep * 1000); } else { // invoke the original callback await callbackOrig.apply(this, arguments); } }; // issue the request sendRequest(); } /** * Builds a callback for use in an exec-statement or fetch-result request. * * @param statementContext * @param headers * @param onResultAvailable * * @returns {Function} */ function buildResultRequestCallback( statementContext, headers, onResultAvailable) { const callback = async function (err, body) { if (err) { await onResultAvailable.call(null, err, null); } else { // extract the query id from the response and save it statementContext.queryId = body.data.queryId; // if the result is not ready yet, extract the result url from the response // and issue a GET request to try to fetch the result again unless asyncExec is enabled. if (body && (body.code === queryCodes.QUERY_IN_PROGRESS || body.code === queryCodes.QUERY_IN_PROGRESS_ASYNC)) { if (statementContext.asyncExec) { await onResultAvailable.call(null, err, body); return; } // extract the result url from the response and try to get the result // again sendSfRequest(statementContext, { method: 'GET', headers: headers, url: body.data.getResultUrl, callback: callback }); } else { await onResultAvailable.call(null, err, body); } } }; return callback; } /** * Builds the request headers for a row statement request. * * @returns {Object} */ function buildResultRequestHeadersRow() { return { 'Accept': 'application/snowflake' }; } /** * Builds the request headers for a file statement request. * * @returns {Object} */ function buildResultRequestHeadersFile() { return { 'Accept': 'application/json' }; } /** * Count number of bindings * * @returns {int} */ function countBinding(binds) { if (!Util.isArray(binds)) { return 0; } Logger.getInstance().debug('-- binds.length= %d', binds.length); let count = 0; for (let index = 0; index < binds.length; index++) { if (binds[index] != null && Util.isArray(binds[index])) { count += binds[index].length; } } return count; } function hasNextResult(statement, context) { return function () { return (context.multiResultIds != null && context.multiCurId + 1 < context.multiResultIds.length); }; } function createNextReuslt(statement, context) { return function () { if (hasNextResult(statement, context)) { context.multiCurId++; context.queryId = context.multiResultIds[context.multiCurId]; exports.createStatementPostExec(context, context.services, context.connectionConfig); } }; }