snowflake-sdk
Version:
Node.js driver for Snowflake
697 lines (625 loc) • 18.9 kB
JavaScript
/*
* Copyright (c) 2015-2024 Snowflake Computing Inc. All rights reserved.
*/
const Util = require('../../util');
const Errors = require('../../errors');
const BigNumber = require('bignumber.js');
const GlobalConfig = require('../../global_config');
const Logger = require('../../logger');
const SfTimestamp = require('./sf_timestamp');
const DataTypes = require('./data_types');
const SqlTypes = require('./data_types').SqlTypes;
const bigInt = require('big-integer');
/**
* Creates a new Column.
*
* @param {Object} options
* @param {Number} index
* @param {Object} statementParameters
* @param {String} resultVersion
*
* @constructor
*/
function Column(options, index, statementParameters, resultVersion) {
const name = options.overriddenName || options.name;
const nullable = options.nullable;
const scale = options.scale;
const type = options.type;
const precision = options.precision;
/**
* Returns the name of this column.
*
* @return {String}
*/
this.getName = function () {
return name;
};
/**
* Returns the index of this column.
*
* @return {Number}
*/
this.getIndex = function () {
return index;
};
/**
* Returns the id of this column.
*
* @return {Number}
*/
this.getId = function () {
// use the index as the id for now
return index;
};
/**
* Determines if this column is nullable.
*
* @returns {Boolean}
*/
this.isNullable = function () {
return nullable;
};
/**
* Returns the scale associated with this column.
*
* @returns {Number}
*/
this.getScale = function () {
return scale;
};
/**
* Returns the type associated with this column.
*
* @returns {String}
*/
this.getType = function () {
return type;
};
/**
* Returns the precision associated with this column
*
* @returns {Number}
*/
this.getPrecision = function () {
return precision;
};
// add methods that make it easy to check if the column is of a specific type
this.isString = createFnIsColumnOfType(type, SqlTypes.isString, SqlTypes);
this.isBinary = createFnIsColumnOfType(type, SqlTypes.isBinary, SqlTypes);
this.isNumber = createFnIsColumnOfType(type, SqlTypes.isNumber, SqlTypes);
this.isBoolean = createFnIsColumnOfType(type, SqlTypes.isBoolean, SqlTypes);
this.isDate = createFnIsColumnOfType(type, SqlTypes.isDate, SqlTypes);
this.isTime = createFnIsColumnOfType(type, SqlTypes.isTime, SqlTypes);
this.isTimestamp = createFnIsColumnOfType(type, SqlTypes.isTimestamp, SqlTypes);
this.isTimestampLtz = createFnIsColumnOfType(type, SqlTypes.isTimestampLtz, SqlTypes);
this.isTimestampNtz = createFnIsColumnOfType(type, SqlTypes.isTimestampNtz, SqlTypes);
this.isTimestampTz = createFnIsColumnOfType(type, SqlTypes.isTimestampTz, SqlTypes);
this.isVariant = createFnIsColumnOfType(type, SqlTypes.isVariant, SqlTypes);
this.isObject = createFnIsColumnOfType(type, SqlTypes.isObject, SqlTypes);
this.isArray = createFnIsColumnOfType(type, SqlTypes.isArray, SqlTypes);
let convert;
let toString;
let toValue;
let format;
if (this.isNumber()) {
const integerAs = statementParameters['JS_TREAT_INTEGER_AS_BIGINT'];
if (!integerAs) {
convert = convertRawNumber;
} else {
if (this.getScale() > 0 || this.getType() === SqlTypes.values.REAL) {
convert = convertRawNumber;
} else {
// This is an integer so represent it as a big int
convert = convertRawBigInt;
}
}
toValue = toValueFromNumber;
toString = toStringFromNumber;
} else if (this.isTime()) {
convert = convertRawTime;
toValue = toValueFromTime;
toString = toStringFromTime;
format = statementParameters['TIME_OUTPUT_FORMAT'];
} else {
toValue = noop;
if (this.isBoolean()) {
convert = convertRawBoolean;
toString = toStringFromBoolean;
} else if (this.isDate()) {
convert = convertRawDate;
toString = toStringFromDate;
format = statementParameters['DATE_OUTPUT_FORMAT'];
} else if (this.isTimestamp()) {
if (this.isTimestampLtz()) {
convert = convertRawTimestampLtz;
toString = toStringFromTimestamp;
format = statementParameters['TIMESTAMP_LTZ_OUTPUT_FORMAT'];
} else if (this.isTimestampNtz()) {
convert = convertRawTimestampNtz;
toString = toStringFromTimestamp;
format = statementParameters['TIMESTAMP_NTZ_OUTPUT_FORMAT'];
} else if (this.isTimestampTz()) {
convert = convertRawTimestampTz;
toString = toStringFromTimestamp;
format = statementParameters['TIMESTAMP_TZ_OUTPUT_FORMAT'];
}
// if we don't have a type-specific timezone, use the default format
if (!format) {
format = statementParameters['TIMESTAMP_OUTPUT_FORMAT'];
}
} else if (this.isBinary()) {
convert = convertRawBinary;
toString = toStringFromBinary;
format = statementParameters['BINARY_OUTPUT_FORMAT'];
} else if (this.isVariant()) {
convert = convertRawVariant;
toString = toStringFromVariant;
} else {
// column is of type string, so leave value as is
convert = noop;
toString = toStringFromString;
}
}
// create a private context to pass to the extract function
const context =
{
convert: convert,
toValue: toValue,
toString: toString,
format: format,
resultVersion: resultVersion,
statementParameters: statementParameters
};
/**
* Returns the value of this column in a row.
*
* @param {Object} row
*
* @returns {*}
*/
this.getRowValue = function (row) {
return extractFromRow.call(this, row, context, false);
};
/**
* Returns the value of this in a row as a String.
*
* @param {Object} row
*
* @returns {String}
*/
this.getRowValueAsString = function (row) {
return extractFromRow.call(this, row, context, true);
};
}
/**
* Returns a function that can be used to determine if a column is of a given
* type.
*
* @param {String} columnType the column type.
* @param {Function} columnComparisonFn the column comparison function.
* @param {Object} scope the scope in which to invoke the column comparison
* function.
*
* @returns {Function}
*/
function createFnIsColumnOfType(columnType, columnComparisonFn, scope) {
return function () {
return columnComparisonFn.call(scope, columnType);
};
}
/**
* Converts a raw column value of type Number. The returned value is an object
* that contains the raw string version of the value as well as the
* post-processed version of the value obtained after casting to Number.
*
* @param {String} rawColumnValue
*
* @returns {Object}
*/
function convertRawNumber(rawColumnValue) {
return {
raw: rawColumnValue,
processed: Number(rawColumnValue)
};
}
/**
* Converts a raw column value that is an integer. The returned value is an object
* that contains the raw string version of the value as well as the post-processed
* version of the value obtained after casting to bigInt
*
* @param rawColumnValue
* @returns {{processed: bigInt.BigInteger, raw: *}}
*/
function convertRawBigInt(rawColumnValue) {
return {
raw: rawColumnValue,
processed: bigInt(rawColumnValue)
};
}
/**
* Converts a raw column value of type Boolean to a boolean (true, false,
* or null).
*
* @param {String} rawColumnValue
*
* @returns {Boolean}
*/
function convertRawBoolean(rawColumnValue) {
let ret;
if ((rawColumnValue === '1') || (rawColumnValue === 'TRUE')) {
ret = true;
} else if ((rawColumnValue === '0') || (rawColumnValue === 'FALSE')) {
ret = false;
}
return ret;
}
/**
* Converts a raw column value of type Date to a Snowflake Date.
*
* @param {String} rawColumnValue
* @param {Object} column
* @param {Object} context
*
* @returns {Date}
*/
function convertRawDate(rawColumnValue, column, context) {
return new SfTimestamp(
Number(rawColumnValue) * 86400, // convert to seconds
0, // no nano seconds
0, // no scale required
'UTC', // use utc as the timezone
context.format).toSfDate();
}
/**
* Converts a raw column value of type Time to a Snowflake Time.
*
* @param {String} rawColumnValue
* @param {Object} column
* @param {Object} context
*
* @returns {Object}
*/
function convertRawTime(rawColumnValue, column, context) {
const columnScale = column.getScale();
// the values might be big so use BigNumber to do arithmetic
const valFracSecsBig =
new BigNumber(rawColumnValue).times(Math.pow(10, columnScale));
return convertRawTimestampHelper(
valFracSecsBig,
columnScale,
'UTC',
context.format).toSfTime();
}
/**
* Converts a raw column value of type TIMESTAMP_LTZ to a Snowflake Date.
*
* @param {String} rawColumnValue
* @param {Object} column
* @param {Object} context
*
* @returns {Date}
*/
function convertRawTimestampLtz(rawColumnValue, column, context) {
const columnScale = column.getScale();
// the values might be big so use BigNumber to do arithmetic
const valFracSecsBig =
new BigNumber(rawColumnValue).times(Math.pow(10, columnScale));
// create a new snowflake date
return convertRawTimestampHelper(
valFracSecsBig,
columnScale,
context.statementParameters['TIMEZONE'],
context.format).toSfDate();
}
/**
* Converts a raw column value of type TIMESTAMP_NTZ to a Snowflake Date.
*
* @param {String} rawColumnValue
* @param {Object} column
* @param {Object} context
*
* @returns {Date}
*/
function convertRawTimestampNtz(rawColumnValue, column, context) {
const columnScale = column.getScale();
// the values might be big so use BigNumber to do arithmetic
const valFracSecsBig =
new BigNumber(rawColumnValue).times(Math.pow(10, columnScale));
// create a new snowflake date
return convertRawTimestampHelper(
valFracSecsBig,
columnScale,
'UTC', // it's _ntz, so use UTC for timezone
context.format).toSfDate();
}
/**
* Converts a raw column value of type TIMESTAMP_TZ to a Snowflake Date.
*
* @param {String} rawColumnValue
* @param {Object} column
* @param {Object} context
*
* @returns {Date}
*/
function convertRawTimestampTz(rawColumnValue, column, context) {
let valFracSecsBig;
let valFracSecsWithTzBig;
let timezoneBig;
let timezone;
let timestampAndTZIndex;
// compute the scale factor
const columnScale = column.getScale();
const scaleFactor = Math.pow(10, columnScale);
const resultVersion = context.resultVersion;
if (resultVersion === '0' || resultVersion === undefined) {
// the values might be big so use BigNumber to do arithmetic
valFracSecsBig =
new BigNumber(rawColumnValue).times(scaleFactor);
// for _tz, the timezone is baked into the value
valFracSecsWithTzBig = valFracSecsBig;
// extract everything but the lowest 14 bits to get the fractional seconds
valFracSecsBig =
valFracSecsWithTzBig.dividedBy(16384).integerValue(BigNumber.ROUND_FLOOR);
// extract the lowest 14 bits to get the timezone
if (valFracSecsWithTzBig.isGreaterThanOrEqualTo(0)) {
timezoneBig = valFracSecsWithTzBig.modulo(16384);
} else {
timezoneBig =
valFracSecsWithTzBig.modulo(16384).plus(16384);
}
} else {
// split the value into number of seconds and timezone index
timestampAndTZIndex = rawColumnValue.split(' ');
// the values might be big so use BigNumber to do arithmetic
valFracSecsBig =
new BigNumber(timestampAndTZIndex[0]).times(scaleFactor);
timezoneBig = new BigNumber(timestampAndTZIndex[1]);
}
timezone = timezoneBig.toNumber();
// assert that timezone is valid
Errors.assertInternal(timezone >= 0 && timezone <= 2880);
// subtract 24 hours from the timezone to map [0, 48] to
// [-24, 24], and convert the result to a number
timezone = timezone - 1440;
// create a new snowflake date
return convertRawTimestampHelper(
valFracSecsBig,
columnScale,
timezone,
context.format).toSfDate();
}
/**
* Helper function for the convertRawTimestamp*() functions.
* Returns an instance of SfTimestamp.
*
* @param {Object} epochFracSecsBig
* @param {Number} scale
* @param {String | Number} timezone
* @param {String} format
*
* @returns {Object}
*/
function convertRawTimestampHelper(
epochFracSecsBig,
scale,
timezone,
format) {
// compute the scale factor
const scaleFactor = Math.pow(10, scale);
// split the value into epoch seconds + nanoseconds; for example,
// 1365148923.123456789 will be split into 1365148923 (epoch seconds)
// and 123456789 (nano seconds)
const valSecBig = epochFracSecsBig.dividedBy(scaleFactor).integerValue(BigNumber.ROUND_FLOOR);
const fractionsBig = epochFracSecsBig.minus(valSecBig.times(scaleFactor));
const valSecNanoBig = fractionsBig.times(Math.pow(10, 9 - scale));
// create a new snowflake date from the information
return new SfTimestamp(
valSecBig.toNumber(),
valSecNanoBig.toNumber(),
scale,
timezone,
format);
}
/**
* Converts a raw column value of type Variant to a JavaScript value.
*
* @param {String} rawColumnValue
*
* @returns {Object | Array}
*/
function convertRawVariant(rawColumnValue) {
// if the input is a non-empty string, convert it to a json object
if (Util.string.isNotNullOrEmpty(rawColumnValue)) {
try {
return GlobalConfig.jsonColumnVariantParser(rawColumnValue);
} catch (jsonParseError) {
try {
return GlobalConfig.xmlColumnVariantParser(rawColumnValue);
} catch (xmlParseError) {
Logger.getInstance().debug('Variant cannot be parsed neither as JSON: %s nor as XML: %s', jsonParseError.message, xmlParseError.message);
throw new Errors.VariantParseError(jsonParseError, xmlParseError);
}
}
}
}
/**
* Converts a raw column value of type Binary to a Buffer.
*
* @param {String} rawColumnValue
* @param {Object} column
* @param {Object} context
*
* @returns {Buffer}
*/
function convertRawBinary(rawColumnValue, column, context) {
// Ensure the format is valid.
const format = context.format.toUpperCase();
Errors.assertInternal(format === 'HEX' || format === 'BASE64');
// Decode hex string sent by GS.
const buffer = Buffer.from(rawColumnValue, 'HEX');
if (format === 'HEX') {
buffer.toStringSf = function () {
// The raw value is already an uppercase hex string, so just return it.
// Note that buffer.toString("HEX") returns a lowercase hex string, but we
// want upper case.
return rawColumnValue;
};
} else {
buffer.toStringSf = function () {
return this.toString('BASE64');
};
}
buffer.getFormat = function () {
return format;
};
return buffer;
}
/**
* Returns the input value as is.
*
* @param {*} value
*
* @returns {*}
*/
function noop(value) {
return value;
}
/**
* The toValue() function for a column of type Number.
*
* @param {*} columnValue
*
* @returns {Number}
*/
function toValueFromNumber(columnValue) {
return columnValue ? columnValue.processed : columnValue;
}
/**
* The toValue() function for a column of type Time.
*
* @param {*} columnValue
*
* @returns {String}
*/
function toValueFromTime(columnValue) {
// there's no native javascript type that can be used to represent time, so
// just convert to string
return toStringFromTime(columnValue);
}
/**
* The toString() function for a column of type Number.
*
* @param {Number} columnValue
*
* @returns {String}
*/
function toStringFromNumber(columnValue) {
return (columnValue !== null) ? columnValue.raw : DataTypes.getNullValue();
}
/**
* The toString() function for a column of type Boolean.
*
* @param {Boolean} columnValue
*
* @returns {String}
*/
function toStringFromBoolean(columnValue) {
return (columnValue !== null) ? String(columnValue).toUpperCase() :
DataTypes.getNullValue();
}
/**
* The toString() function for a column of type Date.
*
* @param {Date} columnValue
*
* @returns {String}
*/
function toStringFromDate(columnValue) {
return (columnValue !== null) ? columnValue.toJSON() : DataTypes.getNullValue();
}
/**
* The toString() function for a column of type Time.
*
* @param {Object} columnValue
*
* @returns {String}
*/
function toStringFromTime(columnValue) {
return (columnValue !== null) ? columnValue.toJSON() : DataTypes.getNullValue();
}
/**
* The toString() function for a column of type Timestamp.
*
* @param {Date} columnValue
*
* @returns {String}
*/
function toStringFromTimestamp(columnValue) {
return (columnValue !== null) ? columnValue.toJSON() : DataTypes.getNullValue();
}
/**
* The toString() function for a column of type Variant.
*
* @param {Object} columnValue
*
* @returns {String}
*/
function toStringFromVariant(columnValue) {
return (columnValue !== null) ? JSON.stringify(columnValue) : DataTypes.getNullValue();
}
/**
* The toString() function for a column of type String.
*
* @param {String} columnValue
*
* @returns {String}
*/
function toStringFromString(columnValue) {
return (columnValue !== null) ? columnValue : DataTypes.getNullValue();
}
/**
* The toString() function for a column of type Binary.
*
* @param {Buffer} columnValue
*
* @returns {String}
*/
function toStringFromBinary(columnValue) {
return (columnValue !== null) ? columnValue.toStringSf() : DataTypes.getNullValue();
}
/**
* Extracts the value of a column from a given row.
*
* @param {Object} row
* @param {Object} context
* @param {Boolean} asString
*
* @returns {*}
*/
function extractFromRow(row, context, asString) {
const map = row._arrayProcessedColumns;
const values = row.values;
// get the value
const columnIndex = this.getIndex();
let ret = values[columnIndex];
// if we want the value as a string, and the column is of type variant, and we
// haven't already processed the value before, we don't need to process the
// value, so only process if none of the aforementioned conditions are true
if (!(asString && this.isVariant() && !map[columnIndex])) {
// if the column value has not been processed yet, process it, put it back
// in the values array, and remember that the value has been processed
if (!map[columnIndex]) {
if (ret !== null) {
ret = values[columnIndex] =
context.convert(values[columnIndex], this, context);
}
map[columnIndex] = true;
}
// use the appropriate extraction function depending on whether
// we want the value or a string representation of the value
const extractFn = !asString ? context.toValue : context.toString;
ret = extractFn(ret);
}
return ret;
}
module.exports = Column;