diff --git a/lib/connection/connection_config.js b/lib/connection/connection_config.js index 74701be62..ede1870c2 100644 --- a/lib/connection/connection_config.js +++ b/lib/connection/connection_config.js @@ -11,6 +11,7 @@ const NativeTypes = require('./result/data_types').NativeTypes; const GlobalConfig = require('../global_config'); const authenticationTypes = require('../authentication/authentication').authenticationTypes; const stringSimilarity = require("string-similarity"); +const RowMode = require('./../constants/row_mode'); const WAIT_FOR_BROWSER_ACTION_TIMEOUT = 120000; const DEFAULT_PARAMS = [ @@ -33,6 +34,7 @@ const DEFAULT_PARAMS = 'database', 'schema', 'role', + 'rowMode', 'streamResult', 'fetchAsString', 'clientSessionKeepAlive', @@ -363,6 +365,11 @@ function ConnectionConfig(options, validateCredentials, qaMode, clientInfo) ErrorCodes.ERR_CONN_CREATE_INVALID_FETCH_AS_STRING_VALUES, JSON.stringify(fetchAsString[invalidValueIndex])); } + // Row mode is optional, can be undefined + const rowMode = options.rowMode; + if (Util.exists(rowMode)) { + RowMode.checkRowModeValid(rowMode); + } // check for invalid clientSessionKeepAlive var clientSessionKeepAlive = options.clientSessionKeepAlive; @@ -621,6 +628,15 @@ function ConnectionConfig(options, validateCredentials, qaMode, clientInfo) return fetchAsString; }; + /** + * Returns the rowMode string value ('array', 'object' or 'object_with_renamed_duplicated_columns'). Could be null or undefined. + * + * @returns {String} + */ + this.getRowMode = function () { + return rowMode ; + }; + /** * Returns the client type. * diff --git a/lib/connection/result/column.js b/lib/connection/result/column.js index 80f634bcc..9eafd3837 100644 --- a/lib/connection/result/column.js +++ b/lib/connection/result/column.js @@ -25,7 +25,7 @@ var NULL_UPPERCASE = 'NULL'; */ function Column(options, index, statementParameters, resultVersion) { - var name = options.name; + var name = options.overriddenName || options.name; var nullable = options.nullable; var scale = options.scale; var type = options.type; diff --git a/lib/connection/result/result.js b/lib/connection/result/result.js index cfbad81db..550fef02b 100644 --- a/lib/connection/result/result.js +++ b/lib/connection/result/result.js @@ -9,8 +9,9 @@ var Chunk = require('./chunk'); var ResultStream = require('./result_stream'); var ChunkCache = require('./chunk_cache'); var Column = require('./column'); -var Parameters = require('../../parameters'); var StatementType = require('./statement_type'); +const ColumnNamesCreator = require('./unique_column_name_creator'); +const RowMode = require('../../constants/row_mode'); /** * Creates a new Result. @@ -18,8 +19,7 @@ var StatementType = require('./statement_type'); * @param {Object} options * @constructor */ -function Result(options) -{ +function Result(options) { var data; var chunkHeaders; var parametersMap; @@ -56,8 +56,7 @@ function Result(options) // if no chunk headers were specified, but a query-result-master-key (qrmk) // was specified, build the chunk headers from the qrmk chunkHeaders = data.chunkHeaders; - if (!Util.isObject(chunkHeaders) && Util.isString(data.qrmk)) - { + if (!Util.isObject(chunkHeaders) && Util.isString(data.qrmk)) { chunkHeaders = { 'x-amz-server-side-encryption-customer-algorithm': 'AES256', @@ -74,8 +73,7 @@ function Result(options) // convert the parameters array to a map parametersMap = {}; parametersArray = data.parameters; - for (index = 0, length = parametersArray.length; index < length; index++) - { + for (index = 0, length = parametersArray.length; index < length; index++) { parameter = parametersArray[index]; parametersMap[parameter.name] = parameter.value; } @@ -95,8 +93,14 @@ function Result(options) // index map in which the keys are the column names and the values are the // indices of the columns with the corresponding names this._mapColumnNameToIndices = mapColumnNameToIndices = {}; - for (index = 0; index < numColumns; index++) - { + + const rowMode = options.rowMode; + if (rowMode === RowMode.OBJECT_WITH_RENAMED_DUPLICATED_COLUMNS) { + ColumnNamesCreator.addOverridenNamesForDuplicatedColumns(rowtype); + } + + for (let index = 0; index < numColumns; index++) { + // create a new column and add it to the columns array columns[index] = column = new Column(rowtype[index], index, parametersMap, version); diff --git a/lib/connection/result/row_stream.js b/lib/connection/result/row_stream.js index 7285c919b..e74302957 100644 --- a/lib/connection/result/row_stream.js +++ b/lib/connection/result/row_stream.js @@ -2,11 +2,12 @@ * Copyright (c) 2015-2019 Snowflake Computing Inc. All rights reserved. */ -var Readable = require('stream').Readable; -var Util = require('../../util'); -var Errors = require('../../errors'); -var ResultStream = require('./result_stream'); -var DataTypes = require('./data_types'); +const Readable = require('stream').Readable; +const Util = require('../../util'); +const Errors = require('../../errors'); +const ResultStream = require('./result_stream'); +const DataTypes = require('./data_types'); +const RowMode = require('./../../constants/row_mode'); /** * Creates a stream that can be used to read a statement result row by row. @@ -30,7 +31,7 @@ function RowStream(statement, context, options) }); // extract streaming options - var start, end, fetchAsString; + let start, end, fetchAsString, rowMode; if (Util.isObject(options)) { start = options.start; @@ -40,22 +41,23 @@ function RowStream(statement, context, options) // if a fetchAsString value is not specified in the stream options, try the // statement and connection options (in that order) - if (!Util.exists(fetchAsString)) - { + if (!Util.exists(fetchAsString)) { fetchAsString = context.fetchAsString; } - if (!Util.exists(fetchAsString)) - { + if (!Util.exists(fetchAsString)) { fetchAsString = context.connectionConfig.getFetchAsString(); } + if (!Util.exists(rowMode)) { + rowMode = context.rowMode || context.connectionConfig.getRowMode(); + } - var resultStream = null, numResultStreamInterrupts = 0; - var rowBuffer = null, rowIndex = 0; - var columns, mapColumnIdToExtractFnName; - var initialized = false; - var previousChunk = null; + let resultStream = null, numResultStreamInterrupts = 0; + let rowBuffer = null, rowIndex = 0; + let columns, mapColumnIdToExtractFnName; + let initialized = false; + let previousChunk = null; - var self = this; + const self = this; /** * Reads the next row in the result. @@ -185,7 +187,7 @@ function RowStream(statement, context, options) // if buffer has reached the threshold based on the highWaterMark value then // push() will return false and pause sending data to the buffer until the data is read from the buffer - if (!self.push(externalizeRow(row, columns, mapColumnIdToExtractFnName))) + if (!self.push(externalizeRow(row, columns, mapColumnIdToExtractFnName, rowMode))) { break; } @@ -202,7 +204,7 @@ function RowStream(statement, context, options) else // No more rows left in the buffer { // Push the last row in the buffer - self.push(externalizeRow(row, columns, mapColumnIdToExtractFnName)); + self.push(externalizeRow(row, columns, mapColumnIdToExtractFnName, rowMode)); } }); }; @@ -428,17 +430,19 @@ function buildMapColumnExtractFnNames(columns, fetchAsString) * @param {Object} row * @param {Object[]} columns * @param {Object} [mapColumnIdToExtractFnName] + * @param {String?} rowMode - string value ('array', 'object' or 'object_with_renamed_duplicated_columns'). Default is 'object' when parameter isn't set. * * @returns {Object} */ -function externalizeRow(row, columns, mapColumnIdToExtractFnName) -{ - var externalizedRow = {}; - for (var index = 0, length = columns.length; index < length; index++) - { - var column = columns[index]; - var extractFnName = mapColumnIdToExtractFnName[column.getId()]; - externalizedRow[column.getName()] = row[extractFnName](column.getId()); +function externalizeRow(row, columns, mapColumnIdToExtractFnName, rowMode) { + const isArrayRowMode = rowMode === RowMode.ARRAY; + + const externalizedRow = isArrayRowMode ? [] : {}; + + for (let index = 0, length = columns.length; index < length; index++) { + const column = columns[index]; + const extractFnName = mapColumnIdToExtractFnName[column.getId()]; + externalizedRow[isArrayRowMode ? index : column.getName()] = row[extractFnName](column.getId()); } return externalizedRow; diff --git a/lib/connection/result/unique_column_name_creator.js b/lib/connection/result/unique_column_name_creator.js new file mode 100644 index 000000000..23b6f80dd --- /dev/null +++ b/lib/connection/result/unique_column_name_creator.js @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2015-2023 Snowflake Computing Inc. All rights reserved. + */ + +const resultContainsDuplicatedColumns = (rowtype) => { + const columnNames = rowtype.map(rt => rt.name); + return columnNames.length !== new Set(columnNames).size; +}; + +function addOverriddenNamesForDuplicatedColumns(rowtype) { + + //Prepare renamed columns for duplicates if row mode was set to 'object_with_renamed_duplicated_columns' + if (resultContainsDuplicatedColumns(rowtype)) { + + const columnNames = new Set(rowtype.map(el => el.name)); + const quntityOfColumnNames = new Map(); + + for (let index = 0; index < rowtype.length; index++) { + const columnName = rowtype[index].name; + if (columnName) { + if (quntityOfColumnNames.has(columnName)) { + let times = quntityOfColumnNames.get(columnName) + 1; + let newColumnName = columnName + '_' + times; + while (columnNames.has(newColumnName)) { + times += 1; + newColumnName = columnName + '_' + times; + } + quntityOfColumnNames.set(columnName, times); + rowtype[index].overriddenName = newColumnName; + columnNames.add(newColumnName); + } else { + quntityOfColumnNames.set(columnName, 1); + } + } + } + } +} +exports.addOverridenNamesForDuplicatedColumns = addOverriddenNamesForDuplicatedColumns; \ No newline at end of file diff --git a/lib/connection/statement.js b/lib/connection/statement.js index 4ed59cf66..d4746be4b 100644 --- a/lib/connection/statement.js +++ b/lib/connection/statement.js @@ -18,6 +18,7 @@ var Logger = require('../logger'); var NativeTypes = require('./result/data_types').NativeTypes; var file_transfer_agent = require('.././file_transfer_agent/file_transfer_agent'); var Bind = require('./bind_uploader'); +const RowMode = require('./../constants/row_mode'); var states = { @@ -187,6 +188,11 @@ exports.createStatementPostExec = function ( JSON.stringify(fetchAsString[invalidValueIndex])); } + const rowMode = statementOptions.rowMode; + if (Util.exists(rowMode)) { + RowMode.checkRowModeValid(rowMode); + } + // validate non-user-specified arguments Errors.assertInternal(Util.isObject(services)); Errors.assertInternal(Util.isObject(connectionConfig)); @@ -200,6 +206,7 @@ exports.createStatementPostExec = function ( statementContext.fetchAsString = statementOptions.fetchAsString; statementContext.multiResultIds = statementOptions.multiResultIds; statementContext.multiCurId = statementOptions.multiCurId; + statementContext.rowMode = statementOptions.rowMode; // set the statement type statementContext.type = (statementContext.type == statementTypes.ROW_PRE_EXEC) ? statementTypes.ROW_POST_EXEC : statementTypes.FILE_POST_EXEC; @@ -353,6 +360,10 @@ function createContextPreExec( Errors.checkArgumentValid(Util.isBoolean(statementOptions.internal), ErrorCodes.ERR_CONN_EXEC_STMT_INVALID_INTERNAL); } + const rowMode = statementOptions.rowMode; + if (Util.exists(rowMode)) { + RowMode.checkRowModeValid(rowMode); + } // create a statement context var statementContext = createStatementContext(); @@ -363,6 +374,7 @@ function createContextPreExec( statementContext.fetchAsString = statementOptions.fetchAsString; statementContext.multiResultIds = statementOptions.multiResultIds; statementContext.multiCurId = statementOptions.multiCurId; + statementContext.rowMode = statementOptions.rowMode; // if a binds array is specified, add it to the statement context if (Util.exists(statementOptions.binds)) @@ -436,6 +448,7 @@ function BaseStatement( context.services = services; context.connectionConfig = connectionConfig; context.isFetchingResult = true; + context.rowMode = statementOptions.rowMode || connectionConfig.getRowMode(); // TODO: add the parameters map to the statement context @@ -659,21 +672,16 @@ function invokeStatementComplete(statement, context) // find out if the result will be streamed; // if a value is not specified, get it from the connection var streamResult = context.streamResult; - if (!Util.exists(streamResult)) - { + if (!Util.exists(streamResult)) { streamResult = context.connectionConfig.getStreamResult(); } // if the result will be streamed later, // invoke the complete callback right away - if (streamResult) - { + if (streamResult) { context.complete(Errors.externalize(context.resultError), statement); - } - else - { - process.nextTick(function () - { + } else { + process.nextTick(function () { // aggregate all the rows into an array and pass this // array to the complete callback as the last argument var rows = []; @@ -683,17 +691,14 @@ function invokeStatementComplete(statement, context) let row; // while there are rows available to read, push row to results array - while ((row = this.read()) !== null) - { + while ((row = this.read()) !== null) { rows.push(row); } }) - .on('end', function () - { + .on('end', function () { context.complete(null, statement, rows); }) - .on('error', function (err) - { + .on('error', function (err) { context.complete(Errors.externalize(err), statement); }); }); @@ -786,7 +791,8 @@ function createOnStatementRequestSuccRow(statement, context) response: body, statement: statement, services: context.services, - connectionConfig: context.connectionConfig + connectionConfig: context.connectionConfig, + rowMode: context.rowMode }); // save the statement id context.statementId = context.result.getStatementId(); @@ -1051,6 +1057,11 @@ function createFnFetchRows(statement, context) 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) @@ -1146,6 +1157,12 @@ function createFnStreamRows(statement, context) 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); diff --git a/lib/constants/error_messages.js b/lib/constants/error_messages.js index ffab36eb3..0b2887103 100644 --- a/lib/constants/error_messages.js +++ b/lib/constants/error_messages.js @@ -121,6 +121,7 @@ exports[411002] = 'Invalid start index. The specified value must be a number.'; exports[411003] = 'Invalid end index. The specified value must be a number.'; exports[411004] = 'Invalid fetchAsString value. The specified value must be an Array.'; exports[411005] = 'Invalid fetchAsString type: %s. The supported types are: String, Boolean, Number, Date, Buffer, and JSON.'; +exports[411006] = 'Invalid row mode value. The specified value should be array or object or object_with_renamed_duplicated_columns'; exports[412001] = 'Certificate is REVOKED.'; exports[412002] = 'Certificate status is UNKNOWN.'; diff --git a/lib/constants/row_mode.js b/lib/constants/row_mode.js new file mode 100644 index 000000000..f1054bab2 --- /dev/null +++ b/lib/constants/row_mode.js @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2015-2019 Snowflake Computing Inc. All rights reserved. + */ +const Errors = require('../errors'); + +const ErrorCodes = Errors.codes; +const ARRAY = 'array'; +const OBJECT = 'object'; +const OBJECT_WITH_RENAMED_DUPLICATED_COLUMNS = 'object_with_renamed_duplicated_columns'; + +const isValidRowMode = (rowMode) => [ARRAY, OBJECT, OBJECT_WITH_RENAMED_DUPLICATED_COLUMNS].includes(rowMode); + +const checkRowModeValid = (rowMode) => { + Errors.checkArgumentValid(isValidRowMode(rowMode), + ErrorCodes.ERR_STMT_STREAM_ROWS_INVALID_ROW_MODE, JSON.stringify(rowMode)); +}; + +exports.ARRAY = ARRAY; +exports.OBJECT = OBJECT; +exports.OBJECT_WITH_RENAMED_DUPLICATED_COLUMNS = OBJECT_WITH_RENAMED_DUPLICATED_COLUMNS; +exports.isValidRowMode = isValidRowMode; +exports.checkRowModeValid = checkRowModeValid; \ No newline at end of file diff --git a/lib/errors.js b/lib/errors.js index ec2a2e715..a91b63def 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -126,6 +126,7 @@ codes.ERR_STMT_STREAM_ROWS_INVALID_START = 411002; codes.ERR_STMT_STREAM_ROWS_INVALID_END = 411003; codes.ERR_STMT_STREAM_ROWS_INVALID_FETCH_AS_STRING = 411004; codes.ERR_STMT_STREAM_ROWS_INVALID_FETCH_AS_STRING_VALUES = 411005; +codes.ERR_STMT_STREAM_ROWS_INVALID_ROW_MODE = 411006; // 412001 codes.ERR_OCSP_REVOKED = 412001; diff --git a/test/integration/testRowMode.js b/test/integration/testRowMode.js new file mode 100644 index 000000000..b024f839a --- /dev/null +++ b/test/integration/testRowMode.js @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2015-2019 Snowflake Computing Inc. All rights reserved. + */ +const assert = require('assert'); +const testUtil = require('./testUtil'); +const RowMode = require('./../../lib/constants/row_mode'); + + +describe('Test row mode', function () { + this.timeout(5000); + let connection; + const sql = `select * + from (select 'a' as key, 1 as foo, 3 as name) as table1 + join (select 'a' as key, 2 as foo, 3 as name2) as table2 on table1.key = table2.key + join (select 'a' as key, 3 as foo) as table3 on table1.key = table3.key`; + + const expectedArray = ['a', 1, 3, 'a', 2, 3, 'a', 3]; + const expectedObject = {KEY: 'a', FOO: 3, NAME: 3, NAME2: 3}; + const expectedObjectWithRenamedDuplicatedColumns = {KEY: 'a', FOO: 1, NAME: 3, KEY_2: 'a', FOO_2: 2, NAME2: 3, KEY_3: 'a', FOO_3: 3}; + + const testCases = [ + { + connectionRowMode: RowMode.OBJECT, + statementRowModes: [ + { + rowMode: RowMode.ARRAY, + expected: expectedArray + }, + { + rowMode: RowMode.OBJECT_WITH_RENAMED_DUPLICATED_COLUMNS, + expected: expectedObjectWithRenamedDuplicatedColumns + }, + { + rowMode: undefined, + expected: expectedObject + } + ] + }, + { + connectionRowMode: RowMode.ARRAY, + statementRowModes: [ + { + rowMode: undefined, + expected: expectedArray + }, + { + rowMode: RowMode.OBJECT_WITH_RENAMED_DUPLICATED_COLUMNS, + expected: expectedObjectWithRenamedDuplicatedColumns + }, + { + rowMode: RowMode.OBJECT, + expected: expectedObject + }, + ] + }, + { + connectionRowMode: RowMode.OBJECT_WITH_RENAMED_DUPLICATED_COLUMNS, + statementRowModes: [ + { + rowMode: undefined, + expected: expectedObjectWithRenamedDuplicatedColumns + }, + { + rowMode: RowMode.ARRAY, + expected: expectedArray + }, + { + rowMode: RowMode.OBJECT, + expected: expectedObject + }, + ] + } + ]; + + + testCases.forEach(({connectionRowMode, statementRowModes }) => { + describe(`rowMode ${connectionRowMode} in connection`, function () { + before(function (done) { + connection = testUtil.createConnection({rowMode: connectionRowMode}); + testUtil.connect(connection, done); + }); + after(function (done) { + testUtil.destroyConnection(connection, done); + }); + + statementRowModes.forEach(({rowMode, expected}) => { + describe(`rowMode ${rowMode} in statement`, function () { + + it('stream rows', function (done) { + const stmt = connection.execute({ + sqlText: sql, + rowMode: rowMode, + streamResult: true + }); + stmt.streamRows() + .on('data', function (row) { + assert.deepStrictEqual(row, expected); + }) + .on('end', function () { + done(); + }) + .on('error', function (err) { + done(err); + }); + }); + + it('fetch rows', function (done) { + connection.execute({ + sqlText: sql, + rowMode: rowMode, + streamResult: false, + complete: function (err, stmt, rows) { + if (err) { + done(err); + } else { + assert.deepStrictEqual(rows[0], expected); + done(); + } + } + }); + }); + }); + }); + }); + }); + + describe('test incorect row mode - connection', function () { + it('test incorrect row mode', function (done) { + try { + connection = testUtil.createConnection({rowMode: 'invalid'}); + } catch (err) { + assert.strictEqual(err.code, 411006); + done(); + } + }); + }); + + describe('test incorect row mode - statement', function () { + before(function (done) { + connection = testUtil.createConnection(); + testUtil.connect(connection, done); + }); + after(function (done) { + testUtil.destroyConnection(connection, done); + }); + + it('test incorrect row mode', function (done) { + try { + connection.execute({ + rowMode: 'invalid', + sqlText: sql, + complete: function () {} + }); + } catch (err) { + assert.strictEqual(err.code, 411006); + done(); + } + }); + }); + + describe('test correctly named columns for duplicates', function () { + before(function (done) { + connection = testUtil.createConnection(); + testUtil.connect(connection, done); + }); + after(function (done) { + testUtil.destroyConnection(connection, done); + }); + + it('test duplicates', function (done) { + const expected = { + "KEY": "a", + "FOO": 1, + "KEY_1": "a1", + "FOO_2": 2, + "KEY_3": "a2", + "FOO_3": 3, + "KEY_2": "a3", + "FOO_4": 4, + "KEY_4": "a4", + "FOO_5": 5 + } + connection.execute({ + rowMode: RowMode.OBJECT_WITH_RENAMED_DUPLICATED_COLUMNS, + sqlText: `select * + from (select 'a' as key, 1 as foo, + 'a1' as key_1, 2 as foo, + 'a2' as key_3, 3 as foo_3, + 'a3' as key, 4 as foo, + 'a4' as key, 5 as foo) as table1 + `, + complete: function (err, stmt, rows) { + if (err) { + done(err); + } + assert.deepStrictEqual(rows[0], expected); + done(); + } + }); + }); + }); +}); diff --git a/test/integration/testStreamRows.js b/test/integration/testStreamRows.js index f48ab8750..c45c5c979 100644 --- a/test/integration/testStreamRows.js +++ b/test/integration/testStreamRows.js @@ -366,6 +366,7 @@ describe('Test Stream Rows API', function () fs.readFile(outputFileName, function(err, data) { testUtil.checkError(err); + // pragma: allowlist nextline secret assert.strictEqual(checksum(data), '52d6d6c7de1e882e448d5e615e6c2264'); done(); }) diff --git a/test/integration/testUtil.js b/test/integration/testUtil.js index 44d9e4366..aa05e6bf6 100644 --- a/test/integration/testUtil.js +++ b/test/integration/testUtil.js @@ -9,7 +9,7 @@ const fs = require('fs'); module.exports.createConnection = function (validConnectionOptionsOverride = {}) { return snowflake.createConnection({ ...connOptions.valid, - validConnectionOptionsOverride, + ...validConnectionOptionsOverride, }); }; diff --git a/test/unit/connection/result/result_test_duplicated_columns.js b/test/unit/connection/result/result_test_duplicated_columns.js new file mode 100644 index 000000000..e1e0d0c3a --- /dev/null +++ b/test/unit/connection/result/result_test_duplicated_columns.js @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2015-2023 Snowflake Computing Inc. All rights reserved. + */ + +const assert = require('assert'); +const ResultTestCommon = require('./result_test_common'); +const RowMode = require('./../../../../lib/constants/row_mode'); +const ColumnNamesCreator = require('../../../../lib/connection/result/unique_column_name_creator'); +const {addoverriddenNamesForDuplicatedColumns} = require('../../../../lib/connection/result/unique_column_name_creator'); + +describe('Unique column names', function () { + describe('result contains renamed columns depend on row mode', function () { + + const columnNames = ['KEY', 'FOO', 'KEY_1', 'FOO', 'KEY_3', 'FOO_3', 'KEY', 'FOO', 'KEY', 'FOO']; + const testCases = [ + { + title: 'should return renamed columns for duplicates if ro mode object_with_renamed_duplicated_columns', + rowMode: RowMode.OBJECT_WITH_RENAMED_DUPLICATED_COLUMNS, + expectedColumnNames: [ + 'KEY', + 'FOO', + 'KEY_1', + 'FOO_2', + 'KEY_3', + 'FOO_3', + 'KEY_2', + 'FOO_4', + 'KEY_4', + 'FOO_5' + ] + }, + { + title: 'should not rename if row mode object', + rowMode: RowMode.OBJECT, + expectedColumnNames: columnNames + }, + { + title: 'should not rename if row mode array', + rowMode: RowMode.ARRAY, + expectedColumnNames: columnNames + } + ]; + const responseWithColumns = (columnRowSet) => { + return { + 'data': { + 'parameters': [], + 'rowtype': columnRowSet, + 'rowset': [[]], + 'total': 1, + 'returned': 1 + }, + }; + }; + + testCases.forEach(({title, rowMode, expectedColumnNames}) => { + it(title, function (done) { + const response = responseWithColumns(columnNames.map(columnName => { + return {'name': columnName}; + })); + const resultOptions = ResultTestCommon.createResultOptions(response); + resultOptions['rowMode'] = rowMode; + + ResultTestCommon.testResult( + resultOptions, + function each() { + }, + function end(result) { + const columnNames = result.getColumns().map(col => col.getName()); + assert.deepStrictEqual(columnNames, expectedColumnNames); + done(); + } + ); + }); + }); + }); + + describe('create unique names for duplicated column names', function () { + + const testCases = [ + { + name: 'without overridden', + columns: [{name: 'COL1'}, {name: 'COL2'}], + expected: [{name: 'COL1'}, {name: 'COL2'}] + }, + { + name: 'single overridden column', + columns: [{name: 'COL1'}, {name: 'COL1'}], + expected: [{name: 'COL1'}, {name: 'COL1', overriddenName: 'COL1_2'}] + }, + { + name: 'works with empty column list', + columns: [], + expected: [] + }, + { + name: 'create unique suffixes if column name exists', + columns: [{name: 'COL1'}, {name: 'COL1'}, {name: 'COL1_2'}], + expected: [{name: 'COL1'}, {name: 'COL1', overriddenName: 'COL1_3'}, {name: 'COL1_2'}] + }, + { + name: 'create unique suffixes for multiple columns', + columns: [{name: 'COL1'}, {name: 'COL1'}, {name: 'COL2'}, {name: 'COL2'}, {name: 'COL2'}, {name: 'COL3'}, {name: 'COL3'}], + expected: [{name: 'COL1'}, {name: 'COL1', overriddenName: 'COL1_2'}, {name: 'COL2'}, { + name: 'COL2', + overriddenName: 'COL2_2' + }, {name: 'COL2', overriddenName: 'COL2_3'}, {name: 'COL3'}, {name: 'COL3', overriddenName: 'COL3_2'}] + }, + { + name: 'create unique suffixes for multiple columns despite of numeric suffixes', + columns: [{name: 'COL1'}, {name: 'COL1_2'}, {name: 'COL1_2'}, {name: 'COL1'}], + expected: [{name: 'COL1'}, {name: 'COL1_2'}, {name: 'COL1_2', overriddenName: 'COL1_2_2'}, { + name: 'COL1', + overriddenName: 'COL1_3' + }] + }, + { + name: 'not changed if empty names', + columns: [{name: ''}, {name: ''}], + expected: [{name: ''}, {name: ''}] + }, + { + name: 'not changed if undefined names', + columns: [{name: undefined}, {name: undefined}], + expected: [{name: undefined}, {name: undefined}] + }, + { + name: 'not changed if nulls as names', + columns: [{name: null}, {name: null}], + expected: [{name: null}, {name: null}] + } + ]; + + testCases.forEach(({name, columns, expected}) => { + it(name, function () { + ColumnNamesCreator.addOverridenNamesForDuplicatedColumns(columns); + assert.deepStrictEqual(columns, expected); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/unit/connection/statement_test.js b/test/unit/connection/statement_test.js index b9c82ee2a..235d3a6ea 100644 --- a/test/unit/connection/statement_test.js +++ b/test/unit/connection/statement_test.js @@ -377,6 +377,16 @@ describe('Statement.fetchResult()', function () connectionConfig: null }, errorCode: ErrorCodes.ERR_INTERNAL_ASSERT_FAILED + }, + { + name: 'fetchResult() invali row mode', + options: + { + statementOptions: {statementId: 'foo', rowMode: 'invalid'}, + services: {}, + connectionConfig: null + }, + errorCode: ErrorCodes.ERR_STMT_STREAM_ROWS_INVALID_ROW_MODE } ]; diff --git a/test/unit/snowflake_test.js b/test/unit/snowflake_test.js index 14ae00fd9..09d831a8e 100644 --- a/test/unit/snowflake_test.js +++ b/test/unit/snowflake_test.js @@ -259,6 +259,21 @@ describe('snowflake.createConnection() synchronous errors', function () proxyPort: 'proxyPort' }, errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_PROXY_PORT + }, + { + name: 'invalid row mode', + options: + { + username: 'username', + password: 'password', + account: 'account', + warehouse: 'warehouse', + database: 'database', + schema: 'schema', + role: 'role', + rowMode: 'unknown' + }, + errorCode: ErrorCodes.ERR_STMT_STREAM_ROWS_INVALID_ROW_MODE } ]; @@ -698,37 +713,14 @@ function testStatementFetchRows(statement) errorCode: ErrorCodes.ERR_STMT_FETCH_ROWS_MISSING_END }, { - name: 'fetchRows() undefined end()', - options: - { - each: function () - { - }, - end: undefined - }, - errorCode: ErrorCodes.ERR_STMT_FETCH_ROWS_MISSING_END - }, - { - name: 'fetchRows() null end()', + name: 'fetchRows() row mode invalid', options: { - each: function () - { - }, - end: null - }, - errorCode: ErrorCodes.ERR_STMT_FETCH_ROWS_MISSING_END - }, - { - name: 'fetchRows() invalid end()', - options: - { - each: function () - { - }, - end: '' + each: function () {}, + end: function () {}, + rowMode: 'invalid' }, - errorCode: ErrorCodes.ERR_STMT_FETCH_ROWS_INVALID_END + errorCode: ErrorCodes.ERR_STMT_STREAM_ROWS_INVALID_ROW_MODE } ];