diff --git a/test/integration/testExecuteAsync.js b/test/integration/testExecuteAsync.js new file mode 100644 index 000000000..4c58da97b --- /dev/null +++ b/test/integration/testExecuteAsync.js @@ -0,0 +1,245 @@ +/* + * Copyright (c) 2015-2019 Snowflake Computing Inc. All rights reserved. + */ + +const assert = require('assert'); +const async = require('async'); +const testUtil = require('./testUtil'); +const ErrorCodes = require('../../lib/errors').codes; +const QueryStatus = require('../../lib/constants/query_status').code; + +describe('ExecuteAsync test', function () { + let connection; + + before(function (done) { + connection = testUtil.createConnection(); + testUtil.connect(connection, done); + }); + + after(function (done) { + testUtil.destroyConnection(connection, done); + }); + + it('testAsyncQueryWithPromise', function (done) { + const expectedSeconds = 3; + const sqlText = `CALL SYSTEM$WAIT(${expectedSeconds}, 'SECONDS')`; + let queryId; + + async.series( + [ + // Execute query in async mode + function (callback) { + connection.execute({ + sqlText: sqlText, + asyncExec: true, + complete: async function (err, stmt) { + assert.ok(!err); + queryId = stmt.getQueryId(); + const status = await connection.getQueryStatus(queryId); + assert.ok(connection.isStillRunning(status)); + callback(); + } + }); + }, + // Get results using query id + async function () { + const statement = await connection.getResultsFromQueryId({ queryId: queryId }); + + await new Promise((resolve, reject) => { + statement.streamRows() + .on('error', (err) => reject(err)) + .on('data', (row) => assert.strictEqual(row['SYSTEM$WAIT'], `waited ${expectedSeconds} seconds`)) + .on('end', async () => { + const status = await connection.getQueryStatus(queryId); + assert.strictEqual(QueryStatus[status], QueryStatus.SUCCESS); + resolve(); + }); + }); + } + ], + done + ); + }); + + it('testAsyncQueryWithCallback', function (done) { + const expectedSeconds = 3; + const sqlText = `CALL SYSTEM$WAIT(${expectedSeconds}, 'SECONDS')`; + let queryId; + + async.series( + [ + // Execute query in async mode + function (callback) { + connection.execute({ + sqlText: sqlText, + asyncExec: true, + complete: async function (err, stmt) { + assert.ok(!err); + queryId = stmt.getQueryId(); + const status = await connection.getQueryStatus(queryId); + assert.ok(connection.isStillRunning(status)); + callback(); + } + }); + }, + // Get results using query id + function (callback) { + connection.getResultsFromQueryId({ + queryId: queryId, + complete: async function (err, _stmt, rows) { + assert.ok(!err); + const status = await connection.getQueryStatus(queryId); + assert.strictEqual(QueryStatus[status], QueryStatus.SUCCESS); + assert.strictEqual(rows[0]['SYSTEM$WAIT'], `waited ${expectedSeconds} seconds`); + callback(); + } + }); + } + ], + done + ); + }); + + it('testFailedQueryThrowsError', function (done) { + const sqlText = 'select * from fakeTable'; + const timeoutInMs = 1000; // 1 second + let queryId; + + async.series( + [ + // Execute query in async mode + function (callback) { + connection.execute({ + sqlText: sqlText, + asyncExec: true, + complete: async function (err, stmt) { + assert.ok(!err); + queryId = stmt.getQueryId(); + callback(); + } + }); + }, + async function () { + // Wait for query to finish executing + while (connection.isStillRunning(await connection.getQueryStatus(queryId))) { + await new Promise((resolve) => { + setTimeout(() => resolve(), timeoutInMs); + }); + } + + // Check query status failed + const status = await connection.getQueryStatus(queryId); + assert.strictEqual(QueryStatus[status], QueryStatus.FAILED_WITH_ERROR); + assert.ok(connection.isAnError(status)); + + // Check getting the query status throws an error + try { + await connection.getQueryStatusThrowIfError(queryId); + assert.fail(); + } catch (err) { + assert.ok(err); + } + + // Check getting the results throws an error + try { + await connection.getResultsFromQueryId({ queryId: queryId }); + assert.fail(); + } catch (err) { + assert.ok(err); + } + } + ], + done + ); + }); + + it('testMixedSyncAndAsyncQueries', function (done) { + const expectedSeconds = '3'; + const sqlTextForAsync = `CALL SYSTEM$WAIT(${expectedSeconds}, 'SECONDS')`; + const sqlTextForSync = 'select 1'; + let queryId; + + async.series( + [ + // Execute query in async mode + function (callback) { + connection.execute({ + sqlText: sqlTextForAsync, + asyncExec: true, + complete: async function (err, stmt) { + assert.ok(!err); + queryId = stmt.getQueryId(); + const status = await connection.getQueryStatus(queryId); + assert.ok(connection.isStillRunning(status)); + callback(); + } + }); + }, + // Execute a different query in non-async mode + function (callback) { + testUtil.executeCmd(connection, sqlTextForSync, callback); + }, + // Get results using query id + function (callback) { + connection.getResultsFromQueryId({ + queryId: queryId, + complete: async function (err, stmt, rows) { + const status = await connection.getQueryStatus(queryId); + assert.strictEqual(QueryStatus[status], QueryStatus.SUCCESS); + assert.strictEqual(rows[0]['SYSTEM$WAIT'], `waited ${expectedSeconds} seconds`); + callback(); + } + }); + } + ], + done + ); + }); + + it('testGetStatusOfInvalidQueryId', async function () { + const fakeQueryId = 'fakeQueryId'; + + // Get the query status using an invalid query id + try { + // Should fail from invalid uuid + await connection.getQueryStatus(fakeQueryId); + assert.fail(); + } catch (err) { + assert.strictEqual(err.code, ErrorCodes.ERR_GET_RESPONSE_QUERY_INVALID_UUID); + } + }); + + it('testGetResultsOfInvalidQueryId', async function () { + const fakeQueryId = 'fakeQueryId'; + + // Get the queryresults using an invalid query id + try { + // Should fail from invalid uuid + await connection.getResultsFromQueryId({ queryId: fakeQueryId }); + assert.fail(); + } catch (err) { + assert.strictEqual(err.code, ErrorCodes.ERR_GET_RESPONSE_QUERY_INVALID_UUID); + } + }); + + it('testGetStatusOfUnknownQueryId', async function () { + const unknownQueryId = '12345678-1234-4123-A123-123456789012'; + + // Get the query status using an unknown query id + const status = await connection.getQueryStatus(unknownQueryId); + assert.strictEqual(QueryStatus[status], QueryStatus.NO_DATA); + }); + + it('testGetResultsOfUnknownQueryId', async function () { + const unknownQueryId = '12345678-1234-4123-A123-123456789012'; + + // Get the query results using an unknown query id + try { + // Should fail from exceeding NO_DATA retry count + await connection.getResultsFromQueryId({ queryId: unknownQueryId }); + assert.fail(); + } catch (err) { + assert.strictEqual(err.code, ErrorCodes.ERR_GET_RESULTS_QUERY_ID_NO_DATA); + } + }); +}); diff --git a/test/unit/mock/mock_http_client.js b/test/unit/mock/mock_http_client.js index 05d3c346b..49c56b2b3 100644 --- a/test/unit/mock/mock_http_client.js +++ b/test/unit/mock/mock_http_client.js @@ -77,6 +77,36 @@ MockHttpClient.prototype.request = function (request) }, delay); }; +/** + * Issues an async request. + * + * @param {Object} request the request options. + */ +MockHttpClient.prototype.requestAsync = function (request) { + // build the request-to-output map if this is the first request + if (!this._mapRequestToOutput) { + this._mapRequestToOutput = + buildRequestToOutputMap(buildRequestOutputMappings(this._clientInfo)); + } + + // Closing a connection includes a requestID as a query parameter in the url + // Example: http://fake504.snowflakecomputing.com/session?delete=true&requestId=a40454c6-c3bb-4824-b0f3-bae041d9d6a2 + if (request.url.includes('session?delete=true')) { + // Remove the requestID query parameter for the mock HTTP client + request.url = request.url.substring(0, request.url.indexOf('&requestId=')); + } + + // get the output of the specified request from the map + const requestOutput = this._mapRequestToOutput[serializeRequest(request)]; + + Errors.assertInternal(Util.isObject(requestOutput), + 'no response available for: ' + serializeRequest(request)); + + const response = JSON.parse(JSON.stringify(requestOutput.response)); + + return response; +}; + /** * Builds a map in which the keys are requests (or rather, serialized versions * of the requests) and the values are the outputs of the corresponding request @@ -993,6 +1023,39 @@ function buildRequestOutputMappings(clientInfo) } } }, + { + request: + { + method: 'GET', + url: 'http://fakeaccount.snowflakecomputing.com/monitoring/queries/00000000-0000-0000-0000-000000000000', + headers: + { + 'Accept': 'application/json', + 'Authorization': 'Snowflake Token="SESSION_TOKEN"', + 'Content-Type': 'application/json' + //"CLIENT_APP_VERSION": clientInfo.version, + //"CLIENT_APP_ID": "JavaScript" + } + }, + output: + { + err: null, + response: + { + statusCode: 200, + statusMessage: "OK", + body: + { + code: null, + data: { + queries: [{ status: 'RESTARTED' }] + }, + message: null, + success: true + } + } + } + }, { request: { diff --git a/test/unit/snowflake_test.js b/test/unit/snowflake_test.js index f95e7d071..f83cd6eed 100644 --- a/test/unit/snowflake_test.js +++ b/test/unit/snowflake_test.js @@ -5,6 +5,7 @@ var Util = require('./../../lib/util'); var ErrorCodes = require('./../../lib/errors').codes; var MockTestUtil = require('./mock/mock_test_util'); +const QueryStatus = require('./../../lib/constants/query_status').code; var assert = require('assert'); var async = require('async'); @@ -1481,6 +1482,316 @@ describe('statement.cancel()', function () }); }); +describe('connection.getResultsFromQueryId() asynchronous errors', function () { + const queryId = '00000000-0000-0000-0000-000000000000'; + + it('not success status', function (done) { + const connection = snowflake.createConnection(connectionOptions); + async.series( + [ + function (callback) { + connection.connect(function (err) { + assert.ok(!err, JSON.stringify(err)); + callback(); + }); + }, + async function () { + try { + await connection.getResultsFromQueryId({ queryId: queryId }); + assert.fail(); + } catch (err) { + assert.strictEqual(err.code, ErrorCodes.ERR_GET_RESULTS_QUERY_ID_NOT_SUCCESS_STATUS); + } + }, + function (callback) { + connection.destroy(function (err) { + assert.ok(!err, JSON.stringify(err)); + callback(); + }); + } + ], + done + ); + }); +}); + +describe('connection.getResultsFromQueryId() synchronous errors', function () { + const connection = snowflake.createConnection(connectionOptions); + + const testCases = + [ + { + name: 'missing queryId', + options: {}, + errorCode: ErrorCodes.ERR_CONN_FETCH_RESULT_MISSING_QUERY_ID + }, + { + name: 'undefined queryId', + options: { queryId: undefined }, + errorCode: ErrorCodes.ERR_CONN_FETCH_RESULT_MISSING_QUERY_ID + }, + { + name: 'null queryId', + options: { queryId: null }, + errorCode: ErrorCodes.ERR_CONN_FETCH_RESULT_MISSING_QUERY_ID + }, + { + name: 'non-string queryId', + options: { queryId: 123 }, + errorCode: ErrorCodes.ERR_GET_RESPONSE_QUERY_INVALID_UUID + }, + { + name: 'invalid queryId', + options: { queryId: 'invalidQueryId' }, + errorCode: ErrorCodes.ERR_GET_RESPONSE_QUERY_INVALID_UUID + } + ]; + + testCases.forEach(testCase => { + it(testCase.name, async function () { + try { + await connection.getResultsFromQueryId(testCase.options); + } catch (err) { + assert.strictEqual(err.code, testCase.errorCode); + } + }); + }); +}); + +describe('connection.getQueryStatus() synchronous errors', function () { + const connection = snowflake.createConnection(connectionOptions); + + const testCases = + [ + { + name: 'undefined queryId', + queryId: undefined, + errorCode: ErrorCodes.ERR_CONN_FETCH_RESULT_MISSING_QUERY_ID + }, + { + name: 'null queryId', + queryId: null, + errorCode: ErrorCodes.ERR_CONN_FETCH_RESULT_MISSING_QUERY_ID + }, + { + name: 'non-string queryId', + queryId: 123, + errorCode: ErrorCodes.ERR_GET_RESPONSE_QUERY_INVALID_UUID + }, + { + name: 'invalid queryId', + queryId: 'invalidQueryId', + errorCode: ErrorCodes.ERR_GET_RESPONSE_QUERY_INVALID_UUID + } + ]; + + testCases.forEach(testCase => { + it(testCase.name, async function () { + try { + await connection.getQueryStatus(testCase.queryId); + } catch (err) { + assert.strictEqual(err.code, testCase.errorCode); + } + }); + }); +}); + +describe('connection.getQueryStatusThrowIfError() synchronous errors', function () { + const connection = snowflake.createConnection(connectionOptions); + + const testCases = + [ + { + name: 'undefined queryId', + queryId: undefined, + errorCode: ErrorCodes.ERR_CONN_FETCH_RESULT_MISSING_QUERY_ID + }, + { + name: 'null queryId', + queryId: null, + errorCode: ErrorCodes.ERR_CONN_FETCH_RESULT_MISSING_QUERY_ID + }, + { + name: 'non-string queryId', + queryId: 123, + errorCode: ErrorCodes.ERR_GET_RESPONSE_QUERY_INVALID_UUID + }, + { + name: 'invalid queryId', + queryId: 'invalidQueryId', + errorCode: ErrorCodes.ERR_GET_RESPONSE_QUERY_INVALID_UUID + } + ]; + + testCases.forEach(testCase => { + it(testCase.name, async function () { + try { + await connection.getQueryStatusThrowIfError(testCase.queryId); + } catch (err) { + assert.strictEqual(err.code, testCase.errorCode); + } + }); + }); +}); + +describe('snowflake.isStillRunning()', function () { + const connection = snowflake.createConnection(connectionOptions); + + const testCases = + [ + { + name: 'Running', + status: QueryStatus.RUNNING, + expectedValue: true + }, + { + name: 'Aborting', + status: QueryStatus.ABORTING, + expectedValue: false + }, + { + name: 'Success', + status: QueryStatus.SUCCESS, + expectedValue: false + }, + { + name: 'Failed with error', + status: QueryStatus.FAILED_WITH_ERROR, + expectedValue: false + }, + { + name: 'Aborted', + status: QueryStatus.ABORTED, + expectedValue: false + }, + { + name: 'Queued', + status: QueryStatus.QUEUED, + expectedValue: true + }, + { + name: 'Failed with incident', + status: QueryStatus.FAILED_WITH_INCIDENT, + expectedValue: false + }, + { + name: 'Disconnected', + status: QueryStatus.DISCONNECTED, + expectedValue: false + }, + { + name: 'Resuming warehouse', + status: QueryStatus.RESUMING_WAREHOUSE, + expectedValue: true + }, + { + name: 'Queued repairing warehouse', + status: QueryStatus.QUEUED_REPARING_WAREHOUSE, + expectedValue: true + }, + { + name: 'Restarted', + status: QueryStatus.RESTARTED, + expectedValue: false + }, + { + name: 'Blocked', + status: QueryStatus.BLOCKED, + expectedValue: false + }, + { + name: 'No data', + status: QueryStatus.NO_DATA, + expectedValue: true + }, + ]; + + testCases.forEach(testCase => { + it(testCase.name, function () { + assert.strictEqual(testCase.expectedValue, connection.isStillRunning(testCase.status)); + }); + }); +}); + +describe('snowflake.isAnError()', function () { + const connection = snowflake.createConnection(connectionOptions); + + const testCases = + [ + { + name: 'Running', + status: QueryStatus.RUNNING, + expectedValue: false + }, + { + name: 'Aborting', + status: QueryStatus.ABORTING, + expectedValue: true + }, + { + name: 'Success', + status: QueryStatus.SUCCESS, + expectedValue: false + }, + { + name: 'Failed with error', + status: QueryStatus.FAILED_WITH_ERROR, + expectedValue: true + }, + { + name: 'Aborted', + status: QueryStatus.ABORTED, + expectedValue: true + }, + { + name: 'Queued', + status: QueryStatus.QUEUED, + expectedValue: false + }, + { + name: 'Failed with incident', + status: QueryStatus.FAILED_WITH_INCIDENT, + expectedValue: true + }, + { + name: 'Disconnected', + status: QueryStatus.DISCONNECTED, + expectedValue: true + }, + { + name: 'Resuming warehouse', + status: QueryStatus.RESUMING_WAREHOUSE, + expectedValue: false + }, + { + name: 'Queued repairing warehouse', + status: QueryStatus.QUEUED_REPARING_WAREHOUSE, + expectedValue: false + }, + { + name: 'Restarted', + status: QueryStatus.RESTARTED, + expectedValue: false + }, + { + name: 'Blocked', + status: QueryStatus.BLOCKED, + expectedValue: true + }, + { + name: 'No data', + status: QueryStatus.NO_DATA, + expectedValue: false + }, + ]; + + testCases.forEach(testCase => { + it(testCase.name, function () { + assert.strictEqual(testCase.expectedValue, connection.isAnError(testCase.status)); + }); + }); +}); + describe('connection.destroy()', function () { it('destroy without connecting', function (done)