UNPKG

sequelize

Version:

Sequelize is a promise-based Node.js ORM tool for Postgres, MySQL, MariaDB, SQLite, Microsoft SQL Server, Amazon Redshift and Snowflake’s Data Cloud. It features solid transaction support, relations, eager and lazy loading, read replication and more.

682 lines (583 loc) 21.3 kB
'use strict'; const _ = require('lodash'); const Utils = require('../../utils'); const AbstractQueryGenerator = require('../abstract/query-generator'); const util = require('util'); const Op = require('../../operators'); const JSON_FUNCTION_REGEX = /^\s*((?:[a-z]+_){0,2}jsonb?(?:_[a-z]+){0,2})\([^)]*\)/i; const JSON_OPERATOR_REGEX = /^\s*(->>?|@>|<@|\?[|&]?|\|{2}|#-)/i; const TOKEN_CAPTURE_REGEX = /^\s*((?:([`"'])(?:(?!\2).|\2{2})*\2)|[\w\d\s]+|[().,;+-])/i; const FOREIGN_KEY_FIELDS = [ 'CONSTRAINT_NAME as constraint_name', 'CONSTRAINT_NAME as constraintName', 'CONSTRAINT_SCHEMA as constraintSchema', 'CONSTRAINT_SCHEMA as constraintCatalog', 'TABLE_NAME as tableName', 'TABLE_SCHEMA as tableSchema', 'TABLE_SCHEMA as tableCatalog', 'COLUMN_NAME as columnName', 'REFERENCED_TABLE_SCHEMA as referencedTableSchema', 'REFERENCED_TABLE_SCHEMA as referencedTableCatalog', 'REFERENCED_TABLE_NAME as referencedTableName', 'REFERENCED_COLUMN_NAME as referencedColumnName' ].join(','); /** * list of reserved words in Snowflake * source: https://docs.snowflake.com/en/sql-reference/reserved-keywords.html * * @private */ const SNOWFLAKE_RESERVED_WORDS = '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(','); const typeWithoutDefault = new Set(['BLOB', 'TEXT', 'GEOMETRY', 'JSON']); class SnowflakeQueryGenerator extends AbstractQueryGenerator { constructor(options) { super(options); this.OperatorMap = { ...this.OperatorMap, [Op.regexp]: 'REGEXP', [Op.notRegexp]: 'NOT REGEXP' }; } createDatabaseQuery(databaseName, options) { options = { charset: null, collate: null, ...options }; return Utils.joinSQLFragments([ 'CREATE DATABASE IF NOT EXISTS', this.quoteIdentifier(databaseName), options.charset && `DEFAULT CHARACTER SET ${this.escape(options.charset)}`, options.collate && `DEFAULT COLLATE ${this.escape(options.collate)}`, ';' ]); } dropDatabaseQuery(databaseName) { return `DROP DATABASE IF EXISTS ${this.quoteIdentifier(databaseName)};`; } createSchema() { return 'SHOW TABLES'; } showSchemasQuery() { return 'SHOW TABLES'; } versionQuery() { return 'SELECT CURRENT_VERSION()'; } createTableQuery(tableName, attributes, options) { options = { charset: null, rowFormat: null, ...options }; const primaryKeys = []; const foreignKeys = {}; const attrStr = []; for (const attr in attributes) { if (!Object.prototype.hasOwnProperty.call(attributes, attr)) continue; const dataType = attributes[attr]; let match; if (dataType.includes('PRIMARY KEY')) { primaryKeys.push(attr); if (dataType.includes('REFERENCES')) { match = dataType.match(/^(.+) (REFERENCES.*)$/); attrStr.push(`${this.quoteIdentifier(attr)} ${match[1].replace('PRIMARY KEY', '')}`); foreignKeys[attr] = match[2]; } else { attrStr.push(`${this.quoteIdentifier(attr)} ${dataType.replace('PRIMARY KEY', '')}`); } } else if (dataType.includes('REFERENCES')) { match = dataType.match(/^(.+) (REFERENCES.*)$/); attrStr.push(`${this.quoteIdentifier(attr)} ${match[1]}`); foreignKeys[attr] = match[2]; } else { attrStr.push(`${this.quoteIdentifier(attr)} ${dataType}`); } } const table = this.quoteTable(tableName); let attributesClause = attrStr.join(', '); const pkString = primaryKeys.map(pk => this.quoteIdentifier(pk)).join(', '); if (options.uniqueKeys) { _.each(options.uniqueKeys, (columns, indexName) => { if (columns.customIndex) { if (typeof indexName !== 'string') { indexName = `uniq_${tableName}_${columns.fields.join('_')}`; } attributesClause += `, UNIQUE ${this.quoteIdentifier(indexName)} (${columns.fields.map(field => this.quoteIdentifier(field)).join(', ')})`; } }); } if (pkString.length > 0) { attributesClause += `, PRIMARY KEY (${pkString})`; } for (const fkey in foreignKeys) { if (Object.prototype.hasOwnProperty.call(foreignKeys, fkey)) { attributesClause += `, FOREIGN KEY (${this.quoteIdentifier(fkey)}) ${foreignKeys[fkey]}`; } } return Utils.joinSQLFragments([ 'CREATE TABLE IF NOT EXISTS', table, `(${attributesClause})`, options.comment && typeof options.comment === 'string' && `COMMENT ${this.escape(options.comment)}`, options.charset && `DEFAULT CHARSET=${options.charset}`, options.collate && `COLLATE ${options.collate}`, options.rowFormat && `ROW_FORMAT=${options.rowFormat}`, ';' ]); } describeTableQuery(tableName, schema, schemaDelimiter) { const table = this.quoteTable( this.addSchema({ tableName, _schema: schema, _schemaDelimiter: schemaDelimiter }) ); return `SHOW FULL COLUMNS FROM ${table};`; } showTablesQuery(database) { return Utils.joinSQLFragments([ 'SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = \'BASE TABLE\'', database ? `AND TABLE_SCHEMA = ${this.escape(database)}` : 'AND TABLE_SCHEMA NOT IN ( \'INFORMATION_SCHEMA\', \'PERFORMANCE_SCHEMA\', \'SYS\')', ';' ]); } addColumnQuery(table, key, dataType) { return Utils.joinSQLFragments([ 'ALTER TABLE', this.quoteTable(table), 'ADD', this.quoteIdentifier(key), this.attributeToSQL(dataType, { context: 'addColumn', tableName: table, foreignKey: key }), ';' ]); } removeColumnQuery(tableName, attributeName) { return Utils.joinSQLFragments([ 'ALTER TABLE', this.quoteTable(tableName), 'DROP', this.quoteIdentifier(attributeName), ';' ]); } changeColumnQuery(tableName, attributes) { const query = (...subQuerys) => Utils.joinSQLFragments([ 'ALTER TABLE', this.quoteTable(tableName), 'ALTER COLUMN', ...subQuerys, ';' ]); const sql = []; for (const attributeName in attributes) { let definition = this.dataTypeMapping(tableName, attributeName, attributes[attributeName]); const attrSql = []; if (definition.includes('NOT NULL')) { attrSql.push(query(this.quoteIdentifier(attributeName), 'SET NOT NULL')); definition = definition.replace('NOT NULL', '').trim(); } else if (!definition.includes('REFERENCES')) { attrSql.push(query(this.quoteIdentifier(attributeName), 'DROP NOT NULL')); } if (definition.includes('DEFAULT')) { attrSql.push(query(this.quoteIdentifier(attributeName), 'SET DEFAULT', definition.match(/DEFAULT ([^;]+)/)[1])); definition = definition.replace(/(DEFAULT[^;]+)/, '').trim(); } else if (!definition.includes('REFERENCES')) { attrSql.push(query(this.quoteIdentifier(attributeName), 'DROP DEFAULT')); } if (definition.match(/UNIQUE;*$/)) { definition = definition.replace(/UNIQUE;*$/, ''); attrSql.push(query('ADD UNIQUE (', this.quoteIdentifier(attributeName), ')').replace('ALTER COLUMN', '')); } if (definition.includes('REFERENCES')) { definition = definition.replace(/.+?(?=REFERENCES)/, ''); attrSql.push(query('ADD FOREIGN KEY (', this.quoteIdentifier(attributeName), ')', definition).replace('ALTER COLUMN', '')); } else { attrSql.push(query(this.quoteIdentifier(attributeName), 'TYPE', definition)); } sql.push(attrSql.join('')); } return sql.join(''); } renameColumnQuery(tableName, attrBefore, attributes) { const attrString = []; for (const attrName in attributes) { const definition = attributes[attrName]; attrString.push(`'${attrBefore}' '${attrName}' ${definition}`); } return Utils.joinSQLFragments([ 'ALTER TABLE', this.quoteTable(tableName), 'RENAME COLUMN', attrString.join(' to '), ';' ]); } handleSequelizeMethod(attr, tableName, factory, options, prepend) { if (attr instanceof Utils.Json) { // Parse nested object if (attr.conditions) { const conditions = this.parseConditionObject(attr.conditions).map(condition => `${this.jsonPathExtractionQuery(condition.path[0], _.tail(condition.path))} = '${condition.value}'` ); return conditions.join(' AND '); } if (attr.path) { let str; // Allow specifying conditions using the sqlite json functions if (this._checkValidJsonStatement(attr.path)) { str = attr.path; } else { // Also support json property accessors const paths = _.toPath(attr.path); const column = paths.shift(); str = this.jsonPathExtractionQuery(column, paths); } if (attr.value) { str += util.format(' = %s', this.escape(attr.value)); } return str; } } else if (attr instanceof Utils.Cast) { if (/timestamp/i.test(attr.type)) { attr.type = 'datetime'; } else if (attr.json && /boolean/i.test(attr.type)) { // true or false cannot be casted as booleans within a JSON structure attr.type = 'char'; } else if (/double precision/i.test(attr.type) || /boolean/i.test(attr.type) || /integer/i.test(attr.type)) { attr.type = 'decimal'; } else if (/text/i.test(attr.type)) { attr.type = 'char'; } } return super.handleSequelizeMethod(attr, tableName, factory, options, prepend); } truncateTableQuery(tableName) { return Utils.joinSQLFragments([ 'TRUNCATE', this.quoteTable(tableName) ]); } deleteQuery(tableName, where, options = {}, model) { const table = this.quoteTable(tableName); let whereClause = this.getWhereConditions(where, null, model, options); const limit = options.limit && ` LIMIT ${this.escape(options.limit)}`; let primaryKeys = ''; let primaryKeysSelection = ''; if (whereClause) { whereClause = `WHERE ${whereClause}`; } if (limit) { if (!model) { throw new Error('Cannot LIMIT delete without a model.'); } const pks = Object.values(model.primaryKeys).map(pk => this.quoteIdentifier(pk.field)).join(','); primaryKeys = model.primaryKeyAttributes.length > 1 ? `(${pks})` : pks; primaryKeysSelection = pks; return Utils.joinSQLFragments([ 'DELETE FROM', table, 'WHERE', primaryKeys, 'IN (SELECT', primaryKeysSelection, 'FROM', table, whereClause, limit, ')', ';' ]); } return Utils.joinSQLFragments([ 'DELETE FROM', table, whereClause, ';' ]); } showIndexesQuery() { return 'SELECT \'\' FROM DUAL'; } showConstraintsQuery(table, constraintName) { const tableName = table.tableName || table; const schemaName = table.schema; return Utils.joinSQLFragments([ 'SELECT CONSTRAINT_CATALOG AS constraintCatalog,', 'CONSTRAINT_NAME AS constraintName,', 'CONSTRAINT_SCHEMA AS constraintSchema,', 'CONSTRAINT_TYPE AS constraintType,', 'TABLE_NAME AS tableName,', 'TABLE_SCHEMA AS tableSchema', 'from INFORMATION_SCHEMA.TABLE_CONSTRAINTS', `WHERE table_name='${tableName}'`, constraintName && `AND constraint_name = '${constraintName}'`, schemaName && `AND TABLE_SCHEMA = '${schemaName}'`, ';' ]); } removeIndexQuery(tableName, indexNameOrAttributes) { let indexName = indexNameOrAttributes; if (typeof indexName !== 'string') { indexName = Utils.underscore(`${tableName}_${indexNameOrAttributes.join('_')}`); } return Utils.joinSQLFragments([ 'DROP INDEX', this.quoteIdentifier(indexName), 'ON', this.quoteTable(tableName), ';' ]); } attributeToSQL(attribute, options) { if (!_.isPlainObject(attribute)) { attribute = { type: attribute }; } const attributeString = attribute.type.toString({ escape: this.escape.bind(this) }); let template = attributeString; if (attribute.allowNull === false) { template += ' NOT NULL'; } if (attribute.autoIncrement) { template += ' AUTOINCREMENT'; } // BLOB/TEXT/GEOMETRY/JSON cannot have a default value if (!typeWithoutDefault.has(attributeString) && attribute.type._binary !== true && Utils.defaultValueSchemable(attribute.defaultValue)) { template += ` DEFAULT ${this.escape(attribute.defaultValue)}`; } if (attribute.unique === true) { template += ' UNIQUE'; } if (attribute.primaryKey) { template += ' PRIMARY KEY'; } if (attribute.comment) { template += ` COMMENT ${this.escape(attribute.comment)}`; } if (attribute.first) { template += ' FIRST'; } if (attribute.after) { template += ` AFTER ${this.quoteIdentifier(attribute.after)}`; } if (attribute.references) { if (options && options.context === 'addColumn' && options.foreignKey) { const attrName = this.quoteIdentifier(options.foreignKey); const fkName = this.quoteIdentifier(`${options.tableName}_${attrName}_foreign_idx`); template += `, ADD CONSTRAINT ${fkName} FOREIGN KEY (${attrName})`; } template += ` REFERENCES ${this.quoteTable(attribute.references.model)}`; if (attribute.references.key) { template += ` (${this.quoteIdentifier(attribute.references.key)})`; } else { template += ` (${this.quoteIdentifier('id')})`; } if (attribute.onDelete) { template += ` ON DELETE ${attribute.onDelete.toUpperCase()}`; } if (attribute.onUpdate) { template += ` ON UPDATE ${attribute.onUpdate.toUpperCase()}`; } } return template; } attributesToSQL(attributes, options) { const result = {}; for (const key in attributes) { const attribute = attributes[key]; result[attribute.field || key] = this.attributeToSQL(attribute, options); } return result; } /** * Check whether the statmement is json function or simple path * * @param {string} stmt The statement to validate * @returns {boolean} true if the given statement is json function * @throws {Error} throw if the statement looks like json function but has invalid token * @private */ _checkValidJsonStatement(stmt) { if (typeof stmt !== 'string') { return false; } let currentIndex = 0; let openingBrackets = 0; let closingBrackets = 0; let hasJsonFunction = false; let hasInvalidToken = false; while (currentIndex < stmt.length) { const string = stmt.substr(currentIndex); const functionMatches = JSON_FUNCTION_REGEX.exec(string); if (functionMatches) { currentIndex += functionMatches[0].indexOf('('); hasJsonFunction = true; continue; } const operatorMatches = JSON_OPERATOR_REGEX.exec(string); if (operatorMatches) { currentIndex += operatorMatches[0].length; hasJsonFunction = true; continue; } const tokenMatches = TOKEN_CAPTURE_REGEX.exec(string); if (tokenMatches) { const capturedToken = tokenMatches[1]; if (capturedToken === '(') { openingBrackets++; } else if (capturedToken === ')') { closingBrackets++; } else if (capturedToken === ';') { hasInvalidToken = true; break; } currentIndex += tokenMatches[0].length; continue; } break; } // Check invalid json statement if (hasJsonFunction && (hasInvalidToken || openingBrackets !== closingBrackets)) { throw new Error(`Invalid json statement: ${stmt}`); } // return true if the statement has valid json function return hasJsonFunction; } dataTypeMapping(tableName, attr, dataType) { if (dataType.includes('PRIMARY KEY')) { dataType = dataType.replace('PRIMARY KEY', ''); } if (dataType.includes('SERIAL')) { if (dataType.includes('BIGINT')) { dataType = dataType.replace('SERIAL', 'BIGSERIAL'); dataType = dataType.replace('BIGINT', ''); } else if (dataType.includes('SMALLINT')) { dataType = dataType.replace('SERIAL', 'SMALLSERIAL'); dataType = dataType.replace('SMALLINT', ''); } else { dataType = dataType.replace('INTEGER', ''); } dataType = dataType.replace('NOT NULL', ''); } return dataType; } /** * Generates an SQL query that returns all foreign keys of a table. * * @param {object} table The table. * @param {string} schemaName The name of the schema. * @returns {string} The generated sql query. * @private */ getForeignKeysQuery(table, schemaName) { const tableName = table.tableName || table; return Utils.joinSQLFragments([ 'SELECT', FOREIGN_KEY_FIELDS, `FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE where TABLE_NAME = '${tableName}'`, `AND CONSTRAINT_NAME!='PRIMARY' AND CONSTRAINT_SCHEMA='${schemaName}'`, 'AND REFERENCED_TABLE_NAME IS NOT NULL', ';' ]); } /** * Generates an SQL query that returns the foreign key constraint of a given column. * * @param {object} table The table. * @param {string} columnName The name of the column. * @returns {string} The generated sql query. * @private */ getForeignKeyQuery(table, columnName) { const quotedSchemaName = table.schema ? wrapSingleQuote(table.schema) : ''; const quotedTableName = wrapSingleQuote(table.tableName || table); const quotedColumnName = wrapSingleQuote(columnName); return Utils.joinSQLFragments([ 'SELECT', FOREIGN_KEY_FIELDS, 'FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE', 'WHERE (', [ `REFERENCED_TABLE_NAME = ${quotedTableName}`, table.schema && `AND REFERENCED_TABLE_SCHEMA = ${quotedSchemaName}`, `AND REFERENCED_COLUMN_NAME = ${quotedColumnName}` ], ') OR (', [ `TABLE_NAME = ${quotedTableName}`, table.schema && `AND TABLE_SCHEMA = ${quotedSchemaName}`, `AND COLUMN_NAME = ${quotedColumnName}`, 'AND REFERENCED_TABLE_NAME IS NOT NULL' ], ')' ]); } /** * Generates an SQL query that removes a foreign key from a table. * * @param {string} tableName The name of the table. * @param {string} foreignKey The name of the foreign key constraint. * @returns {string} The generated sql query. * @private */ dropForeignKeyQuery(tableName, foreignKey) { return Utils.joinSQLFragments([ 'ALTER TABLE', this.quoteTable(tableName), 'DROP FOREIGN KEY', this.quoteIdentifier(foreignKey), ';' ]); } addLimitAndOffset(options) { let fragment = []; if (options.offset !== null && options.offset !== undefined && options.offset !== 0) { fragment = fragment.concat([' LIMIT ', this.escape(options.limit), ' OFFSET ', this.escape(options.offset)]); } else if ( options.limit !== null && options.limit !== undefined ) { fragment = [' LIMIT ', this.escape(options.limit)]; } return fragment.join(''); } /** * Quote identifier in sql clause * * @param {string} identifier * @param {boolean} force * * @returns {string} */ quoteIdentifier(identifier, force) { const optForceQuote = force || false; const optQuoteIdentifiers = this.options.quoteIdentifiers !== false; const rawIdentifier = Utils.removeTicks(identifier, '"'); if ( optForceQuote === true || optQuoteIdentifiers !== false || identifier.includes('.') || identifier.includes('->') || SNOWFLAKE_RESERVED_WORDS.includes(rawIdentifier.toLowerCase()) ) { // In 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 // this way. Hence, we strip quotes if we don't want case sensitivity. return Utils.addTicks(rawIdentifier, '"'); } return rawIdentifier; } } // private methods function wrapSingleQuote(identifier) { return Utils.addTicks(identifier, '\''); } module.exports = SnowflakeQueryGenerator;