Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dialect): snowflake dialect support #13406

Merged
merged 11 commits into from
Dec 3, 2021
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,25 @@ jobs:
run: yarn test-unit
- name: Integration Tests
run: yarn test-integration
test-snowflake:
strategy:
fail-fast: false
matrix:
node-version: [10, 16]
name: SNOWFLAKE (Node ${{ matrix.node-version }})
runs-on: ubuntu-latest
env:
DIALECT: snowflake
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: yarn install --frozen-lockfile --ignore-engines
- name: Unit Tests
run: yarn test-unit
# - name: Integration Tests
# run: yarn test-integration
release:
name: Release
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions lib/data-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -1056,6 +1056,7 @@ dialectMap.mysql = require('./dialects/mysql/data-types')(DataTypes);
dialectMap.mariadb = require('./dialects/mariadb/data-types')(DataTypes);
dialectMap.sqlite = require('./dialects/sqlite/data-types')(DataTypes);
dialectMap.mssql = require('./dialects/mssql/data-types')(DataTypes);
dialectMap.snowflake = require('./dialects/snowflake/data-types')(DataTypes);

const dialectList = Object.values(dialectMap);

Expand Down
14 changes: 11 additions & 3 deletions lib/dialects/abstract/query-generator/helpers/quote.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ const Utils = require('../../../../utils');
*/
const postgresReservedWords = 'all,analyse,analyze,and,any,array,as,asc,asymmetric,authorization,binary,both,case,cast,check,collate,collation,column,concurrently,constraint,create,cross,current_catalog,current_date,current_role,current_schema,current_time,current_timestamp,current_user,default,deferrable,desc,distinct,do,else,end,except,false,fetch,for,foreign,freeze,from,full,grant,group,having,ilike,in,initially,inner,intersect,into,is,isnull,join,lateral,leading,left,like,limit,localtime,localtimestamp,natural,not,notnull,null,offset,on,only,or,order,outer,overlaps,placing,primary,references,returning,right,select,session_user,similar,some,symmetric,table,tablesample,then,to,trailing,true,union,unique,user,using,variadic,verbose,when,where,window,with'.split(',');

/**
* list of reserved words in Snowflake
* source: https://docs.snowflake.com/en/sql-reference/reserved-keywords.html
*
* @private
*/
const snowflakeReservedWords = 'account,all,alter,and,any,as,between,by,case,cast,check,column,connect,connections,constraint,create,cross,current,current_date,current_time,current_timestamp,current_user,database,delete,distinct,drop,else,exists,false,following,for,from,full,grant,group,gscluster,having,ilike,in,increment,inner,insert,intersect,into,is,issue,join,lateral,left,like,localtime,localtimestamp,minus,natural,not,null,of,on,or,order,organization,qualify,regexp,revoke,right,rlike,row,rows,sample,schema,select,set,some,start,table,tablesample,then,to,trigger,true,try_cast,union,unique,update,using,values,view,when,whenever,where,with'.split(',');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be really cool if this could be moved over to the snowflake specific dialect files

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sdepold This is currently following the same practice as what postgres is doing for reserved words. Do you have a recommendation on where this can neatly be injected without refactoring all of the calls to this function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sdepold I will follow up in the dialect separation work with postgres together.


/**
*
* @param {string} dialect Dialect name
Expand All @@ -45,7 +53,7 @@ function quoteIdentifier(dialect, identifier, options) {
case 'mariadb':
case 'mysql':
return Utils.addTicks(Utils.removeTicks(identifier, '`'), '`');

case 'snowflake':
case 'postgres':
const rawIdentifier = Utils.removeTicks(identifier, '"');

Expand All @@ -54,9 +62,9 @@ function quoteIdentifier(dialect, identifier, options) {
options.quoteIdentifiers === false &&
!identifier.includes('.') &&
!identifier.includes('->') &&
!postgresReservedWords.includes(rawIdentifier.toLowerCase())
(dialect === 'postgres' && !postgresReservedWords.includes(rawIdentifier.toLowerCase()) || dialect === 'snowflake' && !snowflakeReservedWords.includes(rawIdentifier.toLowerCase()))
) {
// In Postgres, if tables or attributes are created double-quoted,
// In Postgres and Snowflake, if tables or attributes are created double-quoted,
// they are also case sensitive. If they contain any uppercase
// characters, they must always be double-quoted. This makes it
// impossible to write queries in portable SQL if tables are created in
Expand Down
151 changes: 151 additions & 0 deletions lib/dialects/snowflake/connection-manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
'use strict';

const AbstractConnectionManager = require('../abstract/connection-manager');
const SequelizeErrors = require('../../errors');
const { logger } = require('../../utils/logger');
const DataTypes = require('../../data-types').snowflake;
const debug = logger.debugContext('connection:snowflake');
const parserStore = require('../parserStore')('snowflake');

/**
* Snowflake Connection Manager
*
* Get connections, validate and disconnect them.
*
* @private
*/
class ConnectionManager extends AbstractConnectionManager {
constructor(dialect, sequelize) {
sequelize.config.port = sequelize.config.port || 3306;
super(dialect, sequelize);
this.lib = this._loadDialectModule('snowflake-sdk');
this.refreshTypeParser(DataTypes);
}

_refreshTypeParser(dataType) {
parserStore.refresh(dataType);
}

_clearTypeParser() {
parserStore.clear();
}

static _typecast(field, next) {
if (parserStore.get(field.type)) {
return parserStore.get(field.type)(field, this.sequelize.options, next);
}
return next();
}

/**
* Connect with a snowflake database based on config, Handle any errors in connection
* Set the pool handlers on connection.error
* Also set proper timezone once connection is connected.
*
* @param {object} config
* @returns {Promise<Connection>}
* @private
*/
async connect(config) {
const connectionConfig = {
account: config.host,
username: config.username,
password: config.password,
database: config.database,
warehouse: config.warehouse,
role: config.role,
/*
flags: '-FOUND_ROWS',
timezone: this.sequelize.options.timezone,
typeCast: ConnectionManager._typecast.bind(this),
bigNumberStrings: false,
supportBigNumbers: true,
*/
...config.dialectOptions
};

try {

const connection = await new Promise((resolve, reject) => {
this.lib.createConnection(connectionConfig).connect((err, conn) => {
if (err) {
console.log(err);
reject(err);
} else {
resolve(conn);
}
});
});

debug('connection acquired');

if (!this.sequelize.config.keepDefaultTimezone) {
// default value is '+00:00', put a quick workaround for it.
const tzOffset = this.sequelize.options.timezone === '+00:00' ? 'Etc/UTC' : this.sequelize.options.timezone;
const isNamedTzOffset = /\//.test(tzOffset);
if ( isNamedTzOffset ) {
await new Promise((resolve, reject) => {
connection.execute({
sqlText: `ALTER SESSION SET timezone = '${tzOffset}'`,
jesse23 marked this conversation as resolved.
Show resolved Hide resolved
complete(err) {
if (err) {
console.log(err);
reject(err);
} else {
resolve();
}
}
});
});
} else {
throw Error('only support time zone name for snowflake!');
}
}

return connection;
} catch (err) {
switch (err.code) {
case 'ECONNREFUSED':
throw new SequelizeErrors.ConnectionRefusedError(err);
case 'ER_ACCESS_DENIED_ERROR':
throw new SequelizeErrors.AccessDeniedError(err);
case 'ENOTFOUND':
throw new SequelizeErrors.HostNotFoundError(err);
case 'EHOSTUNREACH':
throw new SequelizeErrors.HostNotReachableError(err);
case 'EINVAL':
throw new SequelizeErrors.InvalidConnectionError(err);
default:
throw new SequelizeErrors.ConnectionError(err);
}
}
}

async disconnect(connection) {
// Don't disconnect connections with CLOSED state
if (connection._closing) {
debug('connection tried to disconnect but was already at CLOSED state');
return;
}

return new Promise((resolve, reject) => {
connection.destroy(err => {
if (err) {
console.error(`Unable to disconnect: ${err.message}`);
reject(err);
} else {
console.log(`Disconnected connection with id: ${connection.getId()}`);
resolve(connection.getId());
}
});
});
}

validate(connection) {
return connection.isUp();
}
}

module.exports = ConnectionManager;
module.exports.ConnectionManager = ConnectionManager;
module.exports.default = ConnectionManager;
102 changes: 102 additions & 0 deletions lib/dialects/snowflake/data-types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
'use strict';

const moment = require('moment-timezone');
module.exports = BaseTypes => {
BaseTypes.ABSTRACT.prototype.dialectTypes = 'https://dev.snowflake.com/doc/refman/5.7/en/data-types.html';

/**
* types: [buffer_type, ...]
*
* @see buffer_type here https://dev.snowflake.com/doc/refman/5.7/en/c-api-prepared-statement-type-codes.html
* @see hex here https://github.com/sidorares/node-mysql2/blob/master/lib/constants/types.js
*/

BaseTypes.DATE.types.snowflake = ['DATETIME'];
BaseTypes.STRING.types.snowflake = ['VAR_STRING'];
BaseTypes.CHAR.types.snowflake = ['STRING'];
BaseTypes.TEXT.types.snowflake = ['BLOB'];
BaseTypes.TINYINT.types.snowflake = ['TINY'];
BaseTypes.SMALLINT.types.snowflake = ['SHORT'];
BaseTypes.MEDIUMINT.types.snowflake = ['INT24'];
BaseTypes.INTEGER.types.snowflake = ['LONG'];
BaseTypes.BIGINT.types.snowflake = ['LONGLONG'];
BaseTypes.FLOAT.types.snowflake = ['FLOAT'];
BaseTypes.TIME.types.snowflake = ['TIME'];
BaseTypes.DATEONLY.types.snowflake = ['DATE'];
BaseTypes.BOOLEAN.types.snowflake = ['TINY'];
BaseTypes.BLOB.types.snowflake = ['TINYBLOB', 'BLOB', 'LONGBLOB'];
BaseTypes.DECIMAL.types.snowflake = ['NEWDECIMAL'];
BaseTypes.UUID.types.snowflake = false;
// Enum is not supported
// https://docs.snowflake.com/en/sql-reference/data-types-unsupported.html
BaseTypes.ENUM.types.snowflake = false;
BaseTypes.REAL.types.snowflake = ['DOUBLE'];
BaseTypes.DOUBLE.types.snowflake = ['DOUBLE'];
BaseTypes.GEOMETRY.types.snowflake = ['GEOMETRY'];
BaseTypes.JSON.types.snowflake = ['JSON'];

class DATE extends BaseTypes.DATE {
toSql() {
return 'TIMESTAMP';
}
_stringify(date, options) {
date = this._applyTimezone(date, options);
if (this._length) {
return date.format('YYYY-MM-DD HH:mm:ss.SSS');
}
return date.format('YYYY-MM-DD HH:mm:ss');
}
static parse(value, options) {
value = value.string();
if (value === null) {
return value;
}
if (moment.tz.zone(options.timezone)) {
value = moment.tz(value, options.timezone).toDate();
}
else {
value = new Date(`${value} ${options.timezone}`);
}
return value;
}
}

class DATEONLY extends BaseTypes.DATEONLY {
static parse(value) {
return value.string();
}
}
class UUID extends BaseTypes.UUID {
toSql() {
// https://community.snowflake.com/s/question/0D50Z00009LH2fl/what-is-the-best-way-to-store-uuids
return 'VARCHAR(36)';
}
}

class TEXT extends BaseTypes.TEXT {
toSql() {
return 'TEXT';
}
}

class BOOLEAN extends BaseTypes.BOOLEAN {
toSql() {
return 'BOOLEAN';
}
}

class JSONTYPE extends BaseTypes.JSON {
_stringify(value, options) {
return options.operation === 'where' && typeof value === 'string' ? value : JSON.stringify(value);
}
}

return {
TEXT,
DATE,
BOOLEAN,
DATEONLY,
UUID,
JSON: JSONTYPE
};
};
66 changes: 66 additions & 0 deletions lib/dialects/snowflake/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
'use strict';

const _ = require('lodash');
const AbstractDialect = require('../abstract');
const ConnectionManager = require('./connection-manager');
const Query = require('./query');
const QueryGenerator = require('./query-generator');
const DataTypes = require('../../data-types').snowflake;
const { SnowflakeQueryInterface } = require('./query-interface');

class SnowflakeDialect extends AbstractDialect {
constructor(sequelize) {
super();
this.sequelize = sequelize;
this.connectionManager = new ConnectionManager(this, sequelize);
this.queryGenerator = new QueryGenerator({
_dialect: this,
sequelize
});
this.queryInterface = new SnowflakeQueryInterface(sequelize, this.queryGenerator);
}
}

SnowflakeDialect.prototype.supports = _.merge(_.cloneDeep(AbstractDialect.prototype.supports), {
'VALUES ()': true,
'LIMIT ON UPDATE': true,
lock: true,
forShare: 'LOCK IN SHARE MODE',
settingIsolationLevelDuringTransaction: false,
inserts: {
ignoreDuplicates: ' IGNORE',
// disable for now, but could be enable by approach below
// https://stackoverflow.com/questions/54828745/how-to-migrate-on-conflict-do-nothing-from-postgresql-to-snowflake
updateOnDuplicate: false
},
index: {
collate: false,
length: true,
parser: true,
type: true,
using: 1
},
constraints: {
dropConstraint: false,
check: false
},
indexViaAlter: true,
indexHints: true,
NUMERIC: true,
// disable for now, need more work to enable the GEOGRAPHY MAPPING
GEOMETRY: false,
jesse23 marked this conversation as resolved.
Show resolved Hide resolved
JSON: false,
REGEXP: true,
schemas: true
});

SnowflakeDialect.prototype.defaultVersion = '5.7.0';
SnowflakeDialect.prototype.Query = Query;
SnowflakeDialect.prototype.QueryGenerator = QueryGenerator;
SnowflakeDialect.prototype.DataTypes = DataTypes;
SnowflakeDialect.prototype.name = 'snowflake';
SnowflakeDialect.prototype.TICK_CHAR = '"';
SnowflakeDialect.prototype.TICK_CHAR_LEFT = SnowflakeDialect.prototype.TICK_CHAR;
SnowflakeDialect.prototype.TICK_CHAR_RIGHT = SnowflakeDialect.prototype.TICK_CHAR;

module.exports = SnowflakeDialect;