Skip to content

Commit

Permalink
feat(postgres): support for ADD COLUMN IF NOT EXISTS and `DROP COLU…
Browse files Browse the repository at this point in the history
…MN 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>
  • Loading branch information
frostzt and WikiRik committed Nov 5, 2022
1 parent 21a25df commit 76cc97f
Show file tree
Hide file tree
Showing 22 changed files with 353 additions and 24 deletions.
22 changes: 22 additions & 0 deletions src/dialects/abstract/index.ts
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
22 changes: 22 additions & 0 deletions 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,
Expand Down Expand Up @@ -86,6 +87,14 @@ export interface ListSchemasQueryOptions {
skip?: string[];
}

export interface AddColumnQueryOptions {
ifNotExists?: boolean;
}

export interface RemoveColumnQueryOptions {
ifExists?: boolean;
}

export class AbstractQueryGenerator {
dialect: AbstractDialect;

Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions src/dialects/abstract/query-generator.js
Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions src/dialects/abstract/query-interface.d.ts
Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -369,7 +373,7 @@ export class QueryInterface {
table: TableName,
key: string,
attribute: ModelAttributeColumnOptions | DataType,
options?: QiOptionsWithReplacements
options?: AddColumnOptions
): Promise<void>;

/**
Expand All @@ -378,7 +382,7 @@ export class QueryInterface {
removeColumn(
table: TableName,
attribute: string,
options?: QiOptionsWithReplacements
options?: RemoveColumnOptions,
): Promise<void>;

/**
Expand Down
12 changes: 10 additions & 2 deletions src/dialects/abstract/query-interface.js
Expand Up @@ -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);
}

/**
Expand All @@ -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) {
Expand Down
6 changes: 6 additions & 0 deletions src/dialects/db2/index.js
Expand Up @@ -21,6 +21,12 @@ export class Db2Dialect extends AbstractDialect {
alterColumn: {
unique: false,
},
addColumn: {
ifNotExists: false,
},
removeColumn: {
ifExists: false,
},
index: {
collate: false,
using: false,
Expand Down
32 changes: 29 additions & 3 deletions 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');
Expand All @@ -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) {
Expand Down Expand Up @@ -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 %>;';
Expand All @@ -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)({
Expand Down
6 changes: 6 additions & 0 deletions src/dialects/ibmi/index.js
Expand Up @@ -26,6 +26,12 @@ export class IBMiDialect extends AbstractDialect {
functionBased: true,
collate: false,
},
addColumn: {
ifNotExists: false,
},
removeColumn: {
ifExists: false,
},
constraints: {
onUpdate: false,
},
Expand Down
32 changes: 29 additions & 3 deletions 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');
Expand All @@ -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 {

Expand Down Expand Up @@ -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',
Expand All @@ -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)}`;
}

Expand Down
6 changes: 6 additions & 0 deletions src/dialects/mariadb/index.js
Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions src/dialects/mssql/index.js
Expand Up @@ -25,6 +25,12 @@ export class MssqlDialect extends AbstractDialect {
defaultValue: false,
update: false,
},
addColumn: {
ifNotExists: false,
},
removeColumn: {
ifExists: false,
},
alterColumn: {
unique: false,
},
Expand Down
28 changes: 26 additions & 2 deletions src/dialects/mssql/query-generator.js
Expand Up @@ -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');
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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),
Expand Down
6 changes: 6 additions & 0 deletions src/dialects/mysql/index.js
Expand Up @@ -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,
Expand Down

0 comments on commit 76cc97f

Please sign in to comment.