From 76cc97fa6bc3787e41a773ed3ab4f9752aed89e3 Mon Sep 17 00:00:00 2001 From: Sourav Singh Rawat Date: Sat, 5 Nov 2022 22:45:55 +0530 Subject: [PATCH] feat(postgres): support for `ADD COLUMN IF NOT EXISTS` and `DROP COLUMN IF EXISTS` (#15119) * feat: `IF NOT EXISTS` support for Postgres * feat: added dialect support check * feat: added interface definitions * feat: added support for query generator and interface * feat: removed support for `addColumn` for MySQL * feat: added options check for all the dialects * tests: updated unit tests for query generator * fix: pass options as null if query options are not provided * fix: only send options if any of the options are defined * fix: pass correct options for column * feat: implemented 'DROP COLUMN IF EXISTS' * fix: short-circuit options to empty object if `null` * fix: revert sql check for mysql and mariadb * fix: fallback to options on options check Co-authored-by: Rik Smale <13023439+WikiRik@users.noreply.github.com> --- src/dialects/abstract/index.ts | 22 +++++++++++++ src/dialects/abstract/query-generator.d.ts | 22 +++++++++++++ src/dialects/abstract/query-generator.js | 2 ++ src/dialects/abstract/query-interface.d.ts | 10 ++++-- src/dialects/abstract/query-interface.js | 12 +++++-- src/dialects/db2/index.js | 6 ++++ src/dialects/db2/query-generator.js | 32 +++++++++++++++++-- src/dialects/ibmi/index.js | 6 ++++ src/dialects/ibmi/query-generator.js | 32 +++++++++++++++++-- src/dialects/mariadb/index.js | 6 ++++ src/dialects/mssql/index.js | 6 ++++ src/dialects/mssql/query-generator.js | 28 ++++++++++++++-- src/dialects/mysql/index.js | 6 ++++ src/dialects/mysql/query-generator.js | 32 +++++++++++++++++-- src/dialects/postgres/index.js | 6 ++++ src/dialects/postgres/query-generator.js | 14 +++++--- src/dialects/snowflake/index.js | 6 ++++ src/dialects/snowflake/query-generator.js | 31 ++++++++++++++++-- src/dialects/sqlite/index.js | 6 ++++ src/dialects/sqlite/query-generator.js | 29 +++++++++++++++-- .../query-generator/add-column-query.test.ts | 32 +++++++++++++++++++ .../remove-column-query.test.ts | 31 ++++++++++++++++++ 22 files changed, 353 insertions(+), 24 deletions(-) create mode 100644 test/unit/query-generator/add-column-query.test.ts create mode 100644 test/unit/query-generator/remove-column-query.test.ts diff --git a/src/dialects/abstract/index.ts b/src/dialects/abstract/index.ts index 9e9bff081f32..3553804aa25e 100644 --- a/src/dialects/abstract/index.ts +++ b/src/dialects/abstract/index.ts @@ -23,6 +23,22 @@ export type DialectSupports = { skipLocked: boolean, finalTable: boolean, + addColumn: { + /** + * Does this dialect support checking `IF NOT EXISTS` before adding column + * For instance, in Postgres, "ADD COLUMN IF NOT EXISTS" only adds the column if it does not exist + */ + ifNotExists: boolean, + }, + + dropColumn: { + /** + * Does this dialect support checking `IF EXISTS` before deleting/dropping column + * For instance, in Postgres, "DROP COLUMN IF EXISTS" only drops the column if it does exist + */ + ifExists: boolean, + }, + /* does the dialect support returning values for inserted/updated fields */ returnValues: false | { output: boolean, @@ -159,6 +175,12 @@ export abstract class AbstractDialect { lockOuterJoinFailure: false, skipLocked: false, finalTable: false, + addColumn: { + ifNotExists: false, + }, + dropColumn: { + ifExists: false, + }, returnValues: false, autoIncrement: { identityInsert: false, diff --git a/src/dialects/abstract/query-generator.d.ts b/src/dialects/abstract/query-generator.d.ts index 05b89fe030cf..b8164a657f81 100644 --- a/src/dialects/abstract/query-generator.d.ts +++ b/src/dialects/abstract/query-generator.d.ts @@ -1,5 +1,6 @@ // TODO: complete me - this file is a stub that will be completed when query-generator.ts is migrated to TS +import type { DataType } from '../../data-types.js'; import type { BuiltModelAttributeColumnOptions, FindOptions, @@ -86,6 +87,14 @@ export interface ListSchemasQueryOptions { skip?: string[]; } +export interface AddColumnQueryOptions { + ifNotExists?: boolean; +} + +export interface RemoveColumnQueryOptions { + ifExists?: boolean; +} + export class AbstractQueryGenerator { dialect: AbstractDialect; @@ -120,6 +129,19 @@ export class AbstractQueryGenerator { columnDefinitions?: { [columnName: string]: BuiltModelAttributeColumnOptions } ): string; + addColumnQuery( + table: TableName, + columnName: string, + columnDefinition: ModelAttributeColumnOptions | DataType, + options?: AddColumnQueryOptions, + ): string; + + removeColumnQuery( + table: TableName, + attributeName: string, + options?: RemoveColumnQueryOptions, + ): string; + updateQuery( tableName: TableName, attrValueHash: object, diff --git a/src/dialects/abstract/query-generator.js b/src/dialects/abstract/query-generator.js index 026c5b00326b..472c6df80b0b 100644 --- a/src/dialects/abstract/query-generator.js +++ b/src/dialects/abstract/query-generator.js @@ -29,6 +29,8 @@ const { _validateIncludedElements } = require('../../model-internals'); export const CREATE_DATABASE_QUERY_SUPPORTABLE_OPTION = new Set(['collate', 'charset', 'encoding', 'ctype', 'template']); export const CREATE_SCHEMA_QUERY_SUPPORTABLE_OPTION = new Set(['collate', 'charset']); export const LIST_SCHEMAS_QUERY_SUPPORTABLE_OPTION = new Set(['skip']); +export const ADD_COLUMN_QUERY_SUPPORTABLE_OPTION = new Set(['ifNotExists']); +export const REMOVE_COLUMN_QUERY_SUPPORTABLE_OPTION = new Set(['ifExists']); /** * Abstract Query Generator diff --git a/src/dialects/abstract/query-interface.d.ts b/src/dialects/abstract/query-interface.d.ts index 2f5935eb94d0..2967706fd615 100644 --- a/src/dialects/abstract/query-interface.d.ts +++ b/src/dialects/abstract/query-interface.d.ts @@ -16,7 +16,7 @@ import type { Sequelize, QueryRawOptions, QueryRawOptionsWithModel } from '../.. import type { Transaction } from '../../transaction'; import type { Fn, Literal } from '../../utils'; import type { SetRequired } from '../../utils/set-required'; -import type { AbstractQueryGenerator } from './query-generator.js'; +import type { AbstractQueryGenerator, AddColumnQueryOptions, RemoveColumnQueryOptions } from './query-generator.js'; interface Replaceable { /** @@ -249,6 +249,10 @@ export interface DatabaseDescription { name: string; } +export interface AddColumnOptions extends AddColumnQueryOptions, QueryRawOptions, Replaceable {} + +export interface RemoveColumnOptions extends RemoveColumnQueryOptions, QueryRawOptions, Replaceable {} + /** * The interface that Sequelize uses to talk to all databases. * @@ -369,7 +373,7 @@ export class QueryInterface { table: TableName, key: string, attribute: ModelAttributeColumnOptions | DataType, - options?: QiOptionsWithReplacements + options?: AddColumnOptions ): Promise; /** @@ -378,7 +382,7 @@ export class QueryInterface { removeColumn( table: TableName, attribute: string, - options?: QiOptionsWithReplacements + options?: RemoveColumnOptions, ): Promise; /** diff --git a/src/dialects/abstract/query-interface.js b/src/dialects/abstract/query-interface.js index f18f288c522d..699d1ae75bee 100644 --- a/src/dialects/abstract/query-interface.js +++ b/src/dialects/abstract/query-interface.js @@ -448,7 +448,10 @@ export class QueryInterface { options = options || {}; attribute = this.sequelize.normalizeAttribute(attribute); - return await this.sequelize.queryRaw(this.queryGenerator.addColumnQuery(table, key, attribute), options); + const { ifNotExists, ...rawQueryOptions } = options; + const addColumnQueryOptions = ifNotExists ? { ifNotExists } : null; + + return await this.sequelize.queryRaw(this.queryGenerator.addColumnQuery(table, key, attribute, addColumnQueryOptions), rawQueryOptions); } /** @@ -459,7 +462,12 @@ export class QueryInterface { * @param {object} [options] Query options */ async removeColumn(tableName, attributeName, options) { - return this.sequelize.queryRaw(this.queryGenerator.removeColumnQuery(tableName, attributeName), options); + options = options || {}; + + const { ifExists, ...rawQueryOptions } = options; + const removeColumnQueryOptions = ifExists ? { ifExists } : null; + + return this.sequelize.queryRaw(this.queryGenerator.removeColumnQuery(tableName, attributeName, removeColumnQueryOptions), rawQueryOptions); } normalizeAttribute(dataTypeOrOptions) { diff --git a/src/dialects/db2/index.js b/src/dialects/db2/index.js index ef38afa52cc2..40fcab1e407f 100644 --- a/src/dialects/db2/index.js +++ b/src/dialects/db2/index.js @@ -21,6 +21,12 @@ export class Db2Dialect extends AbstractDialect { alterColumn: { unique: false, }, + addColumn: { + ifNotExists: false, + }, + removeColumn: { + ifExists: false, + }, index: { collate: false, using: false, diff --git a/src/dialects/db2/query-generator.js b/src/dialects/db2/query-generator.js index 40c29a866d19..b935de17f59c 100644 --- a/src/dialects/db2/query-generator.js +++ b/src/dialects/db2/query-generator.js @@ -1,7 +1,11 @@ 'use strict'; import { rejectInvalidOptions, removeTrailingSemicolon } from '../../utils'; -import { CREATE_SCHEMA_QUERY_SUPPORTABLE_OPTION } from '../abstract/query-generator'; +import { + CREATE_SCHEMA_QUERY_SUPPORTABLE_OPTION, + ADD_COLUMN_QUERY_SUPPORTABLE_OPTION, + REMOVE_COLUMN_QUERY_SUPPORTABLE_OPTION, +} from '../abstract/query-generator'; const _ = require('lodash'); const Utils = require('../../utils'); @@ -11,6 +15,8 @@ const randomBytes = require('crypto').randomBytes; const { Op } = require('../../operators'); const CREATE_SCHEMA_SUPPORTED_OPTIONS = new Set(); +const ADD_COLUMN_QUERY_SUPPORTED_OPTIONS = new Set([]); +const REMOVE_COLUMN_QUERY_SUPPORTED_OPTIONS = new Set([]); /* istanbul ignore next */ function throwMethodUndefined(methodName) { @@ -214,7 +220,17 @@ export class Db2QueryGenerator extends AbstractQueryGenerator { return `SELECT name FROM sysibm.systables WHERE NAME = ${wrapSingleQuote(tableName)} AND CREATOR = ${wrapSingleQuote(schemaName)}`; } - addColumnQuery(table, key, dataType) { + addColumnQuery(table, key, dataType, options) { + if (options) { + rejectInvalidOptions( + 'addColumnQuery', + this.dialect.name, + ADD_COLUMN_QUERY_SUPPORTABLE_OPTION, + ADD_COLUMN_QUERY_SUPPORTED_OPTIONS, + options, + ); + } + dataType.field = key; const query = 'ALTER TABLE <%= table %> ADD <%= attribute %>;'; @@ -231,7 +247,17 @@ export class Db2QueryGenerator extends AbstractQueryGenerator { }); } - removeColumnQuery(tableName, attributeName) { + removeColumnQuery(tableName, attributeName, options) { + if (options) { + rejectInvalidOptions( + 'removeColumnQuery', + this.dialect.name, + REMOVE_COLUMN_QUERY_SUPPORTABLE_OPTION, + REMOVE_COLUMN_QUERY_SUPPORTED_OPTIONS, + options, + ); + } + const query = 'ALTER TABLE <%= tableName %> DROP COLUMN <%= attributeName %>;'; return _.template(query, this._templateSettings)({ diff --git a/src/dialects/ibmi/index.js b/src/dialects/ibmi/index.js index 0dd862ed84db..dfd1f4618c1e 100644 --- a/src/dialects/ibmi/index.js +++ b/src/dialects/ibmi/index.js @@ -26,6 +26,12 @@ export class IBMiDialect extends AbstractDialect { functionBased: true, collate: false, }, + addColumn: { + ifNotExists: false, + }, + removeColumn: { + ifExists: false, + }, constraints: { onUpdate: false, }, diff --git a/src/dialects/ibmi/query-generator.js b/src/dialects/ibmi/query-generator.js index 58409919dbae..1d99ebd3a28b 100644 --- a/src/dialects/ibmi/query-generator.js +++ b/src/dialects/ibmi/query-generator.js @@ -1,7 +1,11 @@ 'use strict'; import { rejectInvalidOptions, removeTrailingSemicolon } from '../../utils'; -import { CREATE_SCHEMA_QUERY_SUPPORTABLE_OPTION } from '../abstract/query-generator'; +import { + CREATE_SCHEMA_QUERY_SUPPORTABLE_OPTION, + ADD_COLUMN_QUERY_SUPPORTABLE_OPTION, + REMOVE_COLUMN_QUERY_SUPPORTABLE_OPTION, +} from '../abstract/query-generator'; const Utils = require('../../utils'); const util = require('util'); @@ -13,6 +17,8 @@ const SqlString = require('../../sql-string'); const typeWithoutDefault = new Set(['BLOB']); const CREATE_SCHEMA_SUPPORTED_OPTIONS = new Set(); +const ADD_COLUMN_QUERY_SUPPORTED_OPTIONS = new Set([]); +const REMOVE_COLUMN_QUERY_SUPPORTED_OPTIONS = new Set([]); export class IBMiQueryGenerator extends AbstractQueryGenerator { @@ -173,7 +179,17 @@ export class IBMiQueryGenerator extends AbstractQueryGenerator { return `SELECT TABLE_NAME FROM SYSIBM.SQLTABLES WHERE TABLE_TYPE = 'TABLE' AND TABLE_SCHEM = ${schema ? `'${schema}'` : 'CURRENT SCHEMA'}`; } - addColumnQuery(table, key, dataType) { + addColumnQuery(table, key, dataType, options) { + if (options) { + rejectInvalidOptions( + 'addColumnQuery', + this.dialect.name, + ADD_COLUMN_QUERY_SUPPORTABLE_OPTION, + ADD_COLUMN_QUERY_SUPPORTED_OPTIONS, + options, + ); + } + dataType.field = key; const definition = this.attributeToSQL(dataType, { context: 'addColumn', @@ -184,7 +200,17 @@ export class IBMiQueryGenerator extends AbstractQueryGenerator { return `ALTER TABLE ${this.quoteTable(table)} ADD ${this.quoteIdentifier(key)} ${definition}`; } - removeColumnQuery(tableName, attributeName) { + removeColumnQuery(tableName, attributeName, options) { + if (options) { + rejectInvalidOptions( + 'removeColumnQuery', + this.dialect.name, + REMOVE_COLUMN_QUERY_SUPPORTABLE_OPTION, + REMOVE_COLUMN_QUERY_SUPPORTED_OPTIONS, + options, + ); + } + return `ALTER TABLE ${this.quoteTable(tableName)} DROP COLUMN ${this.quoteIdentifier(attributeName)}`; } diff --git a/src/dialects/mariadb/index.js b/src/dialects/mariadb/index.js index abfe579bd754..2d524c993ab9 100644 --- a/src/dialects/mariadb/index.js +++ b/src/dialects/mariadb/index.js @@ -24,6 +24,12 @@ export class MariaDbDialect extends AbstractDialect { ignoreDuplicates: ' IGNORE', updateOnDuplicate: ' ON DUPLICATE KEY UPDATE', }, + addColumn: { + ifNotExists: false, + }, + removeColumn: { + ifExists: false, + }, index: { collate: false, length: true, diff --git a/src/dialects/mssql/index.js b/src/dialects/mssql/index.js index bec30fe9fa8e..c9ff8114f88e 100644 --- a/src/dialects/mssql/index.js +++ b/src/dialects/mssql/index.js @@ -25,6 +25,12 @@ export class MssqlDialect extends AbstractDialect { defaultValue: false, update: false, }, + addColumn: { + ifNotExists: false, + }, + removeColumn: { + ifExists: false, + }, alterColumn: { unique: false, }, diff --git a/src/dialects/mssql/query-generator.js b/src/dialects/mssql/query-generator.js index 5fb92dd0e159..c4928380e8be 100644 --- a/src/dialects/mssql/query-generator.js +++ b/src/dialects/mssql/query-generator.js @@ -4,6 +4,8 @@ import { rejectInvalidOptions } from '../../utils'; import { CREATE_DATABASE_QUERY_SUPPORTABLE_OPTION, CREATE_SCHEMA_QUERY_SUPPORTABLE_OPTION, + ADD_COLUMN_QUERY_SUPPORTABLE_OPTION, + REMOVE_COLUMN_QUERY_SUPPORTABLE_OPTION, } from '../abstract/query-generator'; const _ = require('lodash'); @@ -22,6 +24,8 @@ function throwMethodUndefined(methodName) { const CREATE_DATABASE_SUPPORTED_OPTIONS = new Set(['collate']); const CREATE_SCHEMA_SUPPORTED_OPTIONS = new Set(); +const ADD_COLUMN_QUERY_SUPPORTED_OPTIONS = new Set([]); +const REMOVE_COLUMN_QUERY_SUPPORTED_OPTIONS = new Set([]); export class MsSqlQueryGenerator extends AbstractQueryGenerator { createDatabaseQuery(databaseName, options) { @@ -287,7 +291,17 @@ export class MsSqlQueryGenerator extends AbstractQueryGenerator { ]); } - addColumnQuery(table, key, dataType) { + addColumnQuery(table, key, dataType, options) { + if (options) { + rejectInvalidOptions( + 'addColumnQuery', + this.dialect.name, + ADD_COLUMN_QUERY_SUPPORTABLE_OPTION, + ADD_COLUMN_QUERY_SUPPORTED_OPTIONS, + options, + ); + } + // TODO: attributeToSQL SHOULD be using attributes in addColumnQuery // but instead we need to pass the key along as the field here dataType.field = key; @@ -320,7 +334,17 @@ export class MsSqlQueryGenerator extends AbstractQueryGenerator { + `@level2type = N'Column', @level2name = ${this.quoteIdentifier(column)};`; } - removeColumnQuery(tableName, attributeName) { + removeColumnQuery(tableName, attributeName, options) { + if (options) { + rejectInvalidOptions( + 'removeColumnQuery', + this.dialect.name, + REMOVE_COLUMN_QUERY_SUPPORTABLE_OPTION, + REMOVE_COLUMN_QUERY_SUPPORTED_OPTIONS, + options, + ); + } + return Utils.joinSQLFragments([ 'ALTER TABLE', this.quoteTable(tableName), diff --git a/src/dialects/mysql/index.js b/src/dialects/mysql/index.js index 173abc7f51ae..29b57a32b51f 100644 --- a/src/dialects/mysql/index.js +++ b/src/dialects/mysql/index.js @@ -24,6 +24,12 @@ export class MysqlDialect extends AbstractDialect { ignoreDuplicates: ' IGNORE', updateOnDuplicate: ' ON DUPLICATE KEY UPDATE', }, + addColumn: { + ifNotExists: false, + }, + removeColumn: { + ifExists: false, + }, index: { collate: false, length: true, diff --git a/src/dialects/mysql/query-generator.js b/src/dialects/mysql/query-generator.js index 63258b09098d..2e23b1df824f 100644 --- a/src/dialects/mysql/query-generator.js +++ b/src/dialects/mysql/query-generator.js @@ -1,5 +1,11 @@ 'use strict'; +import { rejectInvalidOptions } from '../../utils'; +import { + ADD_COLUMN_QUERY_SUPPORTABLE_OPTION, + REMOVE_COLUMN_QUERY_SUPPORTABLE_OPTION, +} from '../abstract/query-generator'; + const _ = require('lodash'); const Utils = require('../../utils'); const { AbstractQueryGenerator } = require('../abstract/query-generator'); @@ -25,6 +31,8 @@ const FOREIGN_KEY_FIELDS = [ ].join(','); const typeWithoutDefault = new Set(['BLOB', 'TEXT', 'GEOMETRY', 'JSON']); +const ADD_COLUMN_QUERY_SUPPORTED_OPTIONS = new Set([]); +const REMOVE_COLUMN_QUERY_SUPPORTED_OPTIONS = new Set([]); export class MySqlQueryGenerator extends AbstractQueryGenerator { constructor(options) { @@ -188,7 +196,17 @@ export class MySqlQueryGenerator extends AbstractQueryGenerator { return `SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME = ${tableName} AND TABLE_SCHEMA = ${this.escape(this.sequelize.config.database)}`; } - addColumnQuery(table, key, dataType) { + addColumnQuery(table, key, dataType, options) { + if (options) { + rejectInvalidOptions( + 'addColumnQuery', + this.dialect.name, + ADD_COLUMN_QUERY_SUPPORTABLE_OPTION, + ADD_COLUMN_QUERY_SUPPORTED_OPTIONS, + options, + ); + } + return Utils.joinSQLFragments([ 'ALTER TABLE', this.quoteTable(table), @@ -203,7 +221,17 @@ export class MySqlQueryGenerator extends AbstractQueryGenerator { ]); } - removeColumnQuery(tableName, attributeName) { + removeColumnQuery(tableName, attributeName, options) { + if (options) { + rejectInvalidOptions( + 'removeColumnQuery', + this.dialect.name, + REMOVE_COLUMN_QUERY_SUPPORTABLE_OPTION, + REMOVE_COLUMN_QUERY_SUPPORTED_OPTIONS, + options, + ); + } + return Utils.joinSQLFragments([ 'ALTER TABLE', this.quoteTable(tableName), diff --git a/src/dialects/postgres/index.js b/src/dialects/postgres/index.js index 490f94249605..68508c4b2ded 100644 --- a/src/dialects/postgres/index.js +++ b/src/dialects/postgres/index.js @@ -22,6 +22,12 @@ export class PostgresDialect extends AbstractDialect { bulkDefault: true, schemas: true, databases: true, + addColumn: { + ifNotExists: true, + }, + removeColumn: { + ifExists: true, + }, lock: true, lockOf: true, lockKey: true, diff --git a/src/dialects/postgres/query-generator.js b/src/dialects/postgres/query-generator.js index 0c6a26aa12c0..f6a171b9b83c 100644 --- a/src/dialects/postgres/query-generator.js +++ b/src/dialects/postgres/query-generator.js @@ -295,14 +295,17 @@ export class PostgresQueryGenerator extends AbstractQueryGenerator { return super.handleSequelizeMethod.call(this, smth, tableName, factory, options, prepend); } - addColumnQuery(table, key, attribute) { + addColumnQuery(table, key, attribute, options) { + options = options || {}; + const dbDataType = this.attributeToSQL(attribute, { context: 'addColumn', table, key }); const dataType = attribute.type || attribute; const definition = this.dataTypeMapping(table, key, dbDataType); const quotedKey = this.quoteIdentifier(key); const quotedTable = this.quoteTable(this.extractTableDetails(table)); + const ifNotExists = options.ifNotExists ? ' IF NOT EXISTS' : ''; - let query = `ALTER TABLE ${quotedTable} ADD COLUMN ${quotedKey} ${definition};`; + let query = `ALTER TABLE ${quotedTable} ADD COLUMN ${ifNotExists} ${quotedKey} ${definition};`; if (dataType instanceof DataTypes.ENUM) { query = this.pgEnum(table, key, dataType) + query; @@ -313,11 +316,14 @@ export class PostgresQueryGenerator extends AbstractQueryGenerator { return query; } - removeColumnQuery(tableName, attributeName) { + removeColumnQuery(tableName, attributeName, options) { + options = options || {}; + const quotedTableName = this.quoteTable(this.extractTableDetails(tableName)); const quotedAttributeName = this.quoteIdentifier(attributeName); + const ifExists = options.ifExists ? ' IF EXISTS' : ''; - return `ALTER TABLE ${quotedTableName} DROP COLUMN ${quotedAttributeName};`; + return `ALTER TABLE ${quotedTableName} DROP COLUMN ${ifExists} ${quotedAttributeName};`; } changeColumnQuery(tableName, attributes) { diff --git a/src/dialects/snowflake/index.js b/src/dialects/snowflake/index.js index 58f89cf25ef1..d1b032d190f3 100644 --- a/src/dialects/snowflake/index.js +++ b/src/dialects/snowflake/index.js @@ -30,6 +30,12 @@ export class SnowflakeDialect extends AbstractDialect { type: true, using: 1, }, + addColumn: { + ifNotExists: false, + }, + removeColumn: { + ifExists: false, + }, constraints: { dropConstraint: false, check: false, diff --git a/src/dialects/snowflake/query-generator.js b/src/dialects/snowflake/query-generator.js index c40813979a7f..a36fd9df9944 100644 --- a/src/dialects/snowflake/query-generator.js +++ b/src/dialects/snowflake/query-generator.js @@ -3,7 +3,10 @@ import { rejectInvalidOptions } from '../../utils'; import { CREATE_DATABASE_QUERY_SUPPORTABLE_OPTION, - CREATE_SCHEMA_QUERY_SUPPORTABLE_OPTION, LIST_SCHEMAS_QUERY_SUPPORTABLE_OPTION, + CREATE_SCHEMA_QUERY_SUPPORTABLE_OPTION, + LIST_SCHEMAS_QUERY_SUPPORTABLE_OPTION, + ADD_COLUMN_QUERY_SUPPORTABLE_OPTION, + REMOVE_COLUMN_QUERY_SUPPORTABLE_OPTION, } from '../abstract/query-generator'; const _ = require('lodash'); @@ -43,6 +46,8 @@ const typeWithoutDefault = new Set(['BLOB', 'TEXT', 'GEOMETRY', 'JSON']); const CREATE_DATABASE_SUPPORTED_OPTIONS = new Set(['charset', 'collate']); const CREATE_SCHEMA_SUPPORTED_OPTIONS = new Set(); const LIST_SCHEMAS_SUPPORTED_OPTIONS = new Set(); +const ADD_COLUMN_QUERY_SUPPORTED_OPTIONS = new Set([]); +const REMOVE_COLUMN_QUERY_SUPPORTED_OPTIONS = new Set([]); export class SnowflakeQueryGenerator extends AbstractQueryGenerator { constructor(options) { @@ -227,7 +232,17 @@ export class SnowflakeQueryGenerator extends AbstractQueryGenerator { ]); } - addColumnQuery(table, key, dataType) { + addColumnQuery(table, key, dataType, options) { + if (options) { + rejectInvalidOptions( + 'addColumnQuery', + this.dialect.name, + ADD_COLUMN_QUERY_SUPPORTABLE_OPTION, + ADD_COLUMN_QUERY_SUPPORTED_OPTIONS, + options, + ); + } + return Utils.joinSQLFragments([ 'ALTER TABLE', this.quoteTable(table), @@ -242,7 +257,17 @@ export class SnowflakeQueryGenerator extends AbstractQueryGenerator { ]); } - removeColumnQuery(tableName, attributeName) { + removeColumnQuery(tableName, attributeName, options) { + if (options) { + rejectInvalidOptions( + 'removeColumnQuery', + this.dialect.name, + REMOVE_COLUMN_QUERY_SUPPORTABLE_OPTION, + REMOVE_COLUMN_QUERY_SUPPORTED_OPTIONS, + options, + ); + } + return Utils.joinSQLFragments([ 'ALTER TABLE', this.quoteTable(tableName), diff --git a/src/dialects/sqlite/index.js b/src/dialects/sqlite/index.js index 2f68bd096aa0..80634d2f40ca 100644 --- a/src/dialects/sqlite/index.js +++ b/src/dialects/sqlite/index.js @@ -21,6 +21,12 @@ export class SqliteDialect extends AbstractDialect { updateOnDuplicate: ' ON CONFLICT DO UPDATE SET', conflictFields: true, }, + addColumn: { + ifNotExists: false, + }, + removeColumn: { + ifExists: false, + }, index: { using: false, where: true, diff --git a/src/dialects/sqlite/query-generator.js b/src/dialects/sqlite/query-generator.js index be6ae97f6c92..d700530eb008 100644 --- a/src/dialects/sqlite/query-generator.js +++ b/src/dialects/sqlite/query-generator.js @@ -1,11 +1,17 @@ 'use strict'; +import { rejectInvalidOptions } from '../../utils'; +import { ADD_COLUMN_QUERY_SUPPORTABLE_OPTION, REMOVE_COLUMN_QUERY_SUPPORTABLE_OPTION } from '../abstract/query-generator'; + const Utils = require('../../utils'); const { Transaction } = require('../../transaction'); const _ = require('lodash'); const { MySqlQueryGenerator } = require('../mysql/query-generator'); const { AbstractQueryGenerator } = require('../abstract/query-generator'); +const ADD_COLUMN_QUERY_SUPPORTED_OPTIONS = new Set([]); +const REMOVE_COLUMN_QUERY_SUPPORTED_OPTIONS = new Set([]); + export class SqliteQueryGenerator extends MySqlQueryGenerator { createSchemaQuery() { throw new Error(`Schemas are not supported in ${this.dialect.name}.`); @@ -199,7 +205,17 @@ export class SqliteQueryGenerator extends MySqlQueryGenerator { return AbstractQueryGenerator.prototype.handleSequelizeMethod.call(this, smth, tableName, factory, options, prepend); } - addColumnQuery(table, key, dataType) { + addColumnQuery(table, key, dataType, options) { + if (options) { + rejectInvalidOptions( + 'addColumnQuery', + this.dialect.name, + ADD_COLUMN_QUERY_SUPPORTABLE_OPTION, + ADD_COLUMN_QUERY_SUPPORTED_OPTIONS, + options, + ); + } + const attributes = {}; attributes[key] = dataType; const fields = this.attributesToSQL(attributes, { context: 'addColumn' }); @@ -385,7 +401,16 @@ export class SqliteQueryGenerator extends MySqlQueryGenerator { return `SELECT sql FROM sqlite_master WHERE tbl_name='${tableName}';`; } - removeColumnQuery(tableName, attributes) { + removeColumnQuery(tableName, attributes, options) { + if (options) { + rejectInvalidOptions( + 'removeColumnQuery', + this.dialect.name, + REMOVE_COLUMN_QUERY_SUPPORTABLE_OPTION, + REMOVE_COLUMN_QUERY_SUPPORTED_OPTIONS, + options, + ); + } attributes = this.attributesToSQL(attributes); diff --git a/test/unit/query-generator/add-column-query.test.ts b/test/unit/query-generator/add-column-query.test.ts new file mode 100644 index 000000000000..62f159d8535c --- /dev/null +++ b/test/unit/query-generator/add-column-query.test.ts @@ -0,0 +1,32 @@ +import { DataTypes } from '@sequelize/core'; +import { buildInvalidOptionReceivedError } from '@sequelize/core/_non-semver-use-at-your-own-risk_/utils/check.js'; +import { expectsql, getTestDialect, sequelize } from '../../support'; + +const dialectName = getTestDialect(); + +describe('QueryGenerator#addColumnQuery', () => { + const queryGenerator = sequelize.getQueryInterface().queryGenerator; + + const User = sequelize.define('User', { + firstName: DataTypes.STRING, + }, { timestamps: false }); + + it('generates a ADD COLUMN query in supported dialects', () => { + expectsql(() => queryGenerator.addColumnQuery(User.tableName, 'age', { + type: DataTypes.INTEGER, + }), { + default: `ALTER TABLE [Users] ADD [age] INTEGER;`, + mssql: `ALTER TABLE [Users] ADD [age] INTEGER NULL;`, + postgres: `ALTER TABLE "public"."Users" ADD COLUMN "age" INTEGER;`, + }); + }); + + it('generates a ADD COLUMN IF NOT EXISTS query in supported dialects', () => { + expectsql(() => queryGenerator.addColumnQuery(User.tableName, 'age', { + type: DataTypes.INTEGER, + }, { ifNotExists: true }), { + default: buildInvalidOptionReceivedError('addColumnQuery', dialectName, ['ifNotExists']), + postgres: `ALTER TABLE "public"."Users" ADD COLUMN IF NOT EXISTS "age" INTEGER;`, + }); + }); +}); diff --git a/test/unit/query-generator/remove-column-query.test.ts b/test/unit/query-generator/remove-column-query.test.ts new file mode 100644 index 000000000000..8c78bd26b508 --- /dev/null +++ b/test/unit/query-generator/remove-column-query.test.ts @@ -0,0 +1,31 @@ +import { DataTypes } from '@sequelize/core'; +import { buildInvalidOptionReceivedError } from '@sequelize/core/_non-semver-use-at-your-own-risk_/utils/check.js'; +import { expectsql, getTestDialect, sequelize } from '../../support'; + +const dialectName = getTestDialect(); + +describe('QueryGenerator#removeColumnQuery', () => { + const queryGenerator = sequelize.getQueryInterface().queryGenerator; + + const User = sequelize.define('User', { + firstName: DataTypes.STRING, + age: DataTypes.INTEGER, + }, { timestamps: false }); + + it('generates a DROP COLUMN query in supported dialects', () => { + expectsql(() => queryGenerator.removeColumnQuery(User.tableName, 'age'), { + default: `ALTER TABLE [Users] DROP COLUMN [age];`, + postgres: `ALTER TABLE "public"."Users" DROP COLUMN "age";`, + snowflake: `ALTER TABLE "Users" DROP "age";`, + sqlite: 'CREATE TABLE IF NOT EXISTS `Users_backup` (`0` a, `1` g, `2` e);INSERT INTO `Users_backup` SELECT `0`, `1`, `2` FROM `Users`;DROP TABLE `Users`;ALTER TABLE `Users_backup` RENAME TO `Users`;', + 'mariadb mysql': 'ALTER TABLE `Users` DROP `age`;', + }); + }); + + it('generates a DROP COLUMN IF EXISTS query in supported dialects', () => { + expectsql(() => queryGenerator.removeColumnQuery(User.tableName, 'age', { ifExists: true }), { + default: buildInvalidOptionReceivedError('removeColumnQuery', dialectName, ['ifExists']), + postgres: `ALTER TABLE "public"."Users" DROP COLUMN IF EXISTS "age";`, + }); + }); +});