diff --git a/dev/db2/11.5/.env_list b/dev/db2/11.5/.env_list index 2cf451f91a7e..2e70ed2ccb87 100644 --- a/dev/db2/11.5/.env_list +++ b/dev/db2/11.5/.env_list @@ -1,7 +1,6 @@ LICENSE=accept DB2INSTANCE=db2inst1 DB2INST1_PASSWORD=password -DBNAME=testdb BLU=false ENABLE_ORACLE_COMPATIBILITY=false UPDATEAVAIL=NO @@ -12,4 +11,4 @@ PERSISTENT_HOME=false HADR_ENABLED=false ETCD_ENDPOINT= ETCD_USERNAME= -ETCD_PASSWORD= \ No newline at end of file +ETCD_PASSWORD= diff --git a/dev/db2/11.5/start.sh b/dev/db2/11.5/start.sh index 0073be832e87..fa5916635091 100644 --- a/dev/db2/11.5/start.sh +++ b/dev/db2/11.5/start.sh @@ -1,5 +1,9 @@ cd dev/db2/11.5 + export DIALECT=db2 +SEQ_DB="${SEQ_DB:-testdb}" +# db2 db names must be uppercase +SEQ_DB=$(echo "$SEQ_DB" | awk '{print toupper($0)}') mkdir -p Docker if [ ! "$(sudo docker ps -q -f name=db2server)" ]; then @@ -9,7 +13,7 @@ if [ ! "$(sudo docker ps -q -f name=db2server)" ]; then sudo docker rm -f db2server sudo rm -rf /Docker fi - sudo docker run -h db2server --name db2server --restart=always --detach --privileged=true -p 50000:50000 --env-file .env_list -v /Docker:/database ibmcom/db2-amd64:11.5.6.0a + sudo docker run -h db2server --name db2server --restart=always --detach --privileged=true -p 50000:50000 --env "DBNAME=$SEQ_DB" --env-file .env_list -v /Docker:/database ibmcom/db2-amd64:11.5.7.0 count=1 while true do @@ -18,7 +22,7 @@ if [ ! "$(sudo docker ps -q -f name=db2server)" ]; then sudo docker exec db2server bash -c "su db2inst1 & disown" break fi - if ($count -gt 30); then + if [ $count -gt 30 ]; then echo "Error: Db2 docker setup has not completed in 10 minutes." break fi diff --git a/src/dialects/abstract/query-generator.js b/src/dialects/abstract/query-generator.js index 97a5535adee2..91abc63445d9 100644 --- a/src/dialects/abstract/query-generator.js +++ b/src/dialects/abstract/query-generator.js @@ -613,18 +613,14 @@ export class AbstractQueryGenerator { options.where = this.whereQuery(options.where); } - if (typeof tableName === 'string') { - tableName = this.quoteIdentifiers(tableName); - } else { - tableName = this.quoteTable(tableName); - } + const escapedTableName = typeof tableName === 'string' ? this.quoteIdentifiers(tableName) : this.quoteTable(tableName); const concurrently = this._dialect.supports.index.concurrently && options.concurrently ? 'CONCURRENTLY' : undefined; let ind; if (this._dialect.supports.indexViaAlter) { ind = [ 'ALTER TABLE', - tableName, + escapedTableName, concurrently, 'ADD', ]; @@ -632,13 +628,24 @@ export class AbstractQueryGenerator { ind = ['CREATE']; } + // DB2 incorrectly scopes the index if we don't specify the schema name, + // which will cause it to error if another schema contains a table that uses an index with an identical name + const escapedIndexName = tableName.schema && this.dialect === 'db2' + // 'quoteTable' isn't the best name: it quotes any identifier. + // in this case, the goal is to produce '"schema_name"."index_name"' to scope the index in this schema + ? this.quoteTable({ + schema: tableName.schema, + tableName: options.name, + }) + : this.quoteIdentifiers(options.name); + ind = ind.concat( options.unique ? 'UNIQUE' : '', options.type, 'INDEX', !this._dialect.supports.indexViaAlter ? concurrently : undefined, - this.quoteIdentifiers(options.name), + escapedIndexName, this._dialect.supports.index.using === 1 && options.using ? `USING ${options.using}` : '', - !this._dialect.supports.indexViaAlter ? `ON ${tableName}` : undefined, + !this._dialect.supports.indexViaAlter ? `ON ${escapedTableName}` : undefined, this._dialect.supports.index.using === 2 && options.using ? `USING ${options.using}` : '', `(${fieldsSql.join(', ')})`, this._dialect.supports.index.parser && options.parser ? `WITH PARSER ${options.parser}` : undefined, diff --git a/src/dialects/db2/query-generator.js b/src/dialects/db2/query-generator.js index 7cb38dd55cad..efbc969653a1 100644 --- a/src/dialects/db2/query-generator.js +++ b/src/dialects/db2/query-generator.js @@ -33,17 +33,23 @@ export class Db2QueryGenerator extends AbstractQueryGenerator { ].join(' '); } + _errorTableCount = 0; + dropSchema(schema) { // DROP SCHEMA Can't drop schema if it is not empty. // DROP SCHEMA Can't drop objects belonging to the schema // So, call the admin procedure to drop schema. - const query = `CALL SYSPROC.ADMIN_DROP_SCHEMA(${wrapSingleQuote(schema.trim())}, NULL, $sequelize_1, $sequelize_2)`; + const query = `CALL SYSPROC.ADMIN_DROP_SCHEMA(${wrapSingleQuote(schema.trim())}, NULL, $sequelize_errorSchema, $sequelize_errorTable)`; + + if (this._errorTableCount >= Number.MAX_SAFE_INTEGER) { + this._errorTableCount = 0; + } return { query, bind: { - sequelize_1: { ParamType: 'INOUT', Data: 'ERRORSCHEMA' }, - sequelize_2: { ParamType: 'INOUT', Data: 'ERRORTABLE' }, + sequelize_errorSchema: { ParamType: 'INOUT', Data: 'ERRORSCHEMA' }, + sequelize_errorTable: { ParamType: 'INOUT', Data: `ERRORTABLE${this._errorTableCount++}` }, }, }; } @@ -58,7 +64,7 @@ export class Db2QueryGenerator extends AbstractQueryGenerator { } createTableQuery(tableName, attributes, options) { - const query = 'CREATE TABLE <%= table %> (<%= attributes %>)'; + const query = 'CREATE TABLE IF NOT EXISTS <%= table %> (<%= attributes %>)'; const primaryKeys = []; const foreignKeys = {}; const attrStr = []; @@ -185,15 +191,6 @@ export class Db2QueryGenerator extends AbstractQueryGenerator { return 'SELECT TABNAME AS "tableName", TRIM(TABSCHEMA) AS "tableSchema" FROM SYSCAT.TABLES WHERE TABSCHEMA = USER AND TYPE = \'T\' ORDER BY TABSCHEMA, TABNAME'; } - dropTableQuery(tableName) { - const query = 'DROP TABLE <%= table %>'; - const values = { - table: this.quoteTable(tableName), - }; - - return `${_.template(query, this._templateSettings)(values).trim()};`; - } - addColumnQuery(table, key, dataType) { dataType.field = key; diff --git a/src/dialects/db2/query-interface.js b/src/dialects/db2/query-interface.js index a02df9802a02..7b7048575ffe 100644 --- a/src/dialects/db2/query-interface.js +++ b/src/dialects/db2/query-interface.js @@ -1,5 +1,6 @@ 'use strict'; +import { AggregateError, DatabaseError } from '../../errors'; import { assertNoReservedBind } from '../../utils/sql'; const _ = require('lodash'); @@ -84,6 +85,61 @@ export class Db2QueryInterface extends QueryInterface { return [result, undefined]; } + async dropSchema(schema, options) { + const outParams = new Map(); + + // DROP SCHEMA works in a weird way in DB2: + // Its query uses ADMIN_DROP_SCHEMA, which stores the error message in a table + // specified by two IN-OUT parameters. + // If the returned values for these parameters is not null, then an error occurred. + const response = await super.dropSchema(schema, { + ...options, + // TODO: db2 supports out parameters. We don't have a proper API for it yet + // for now, this temporary API will have to do. + _unsafe_db2Outparams: outParams, + }); + + const errorTable = outParams.get('sequelize_errorTable'); + if (errorTable != null) { + const errorSchema = outParams.get('sequelize_errorSchema'); + + const errorData = await this.sequelize.queryRaw(`SELECT * FROM "${errorSchema}"."${errorTable}"`, { + type: QueryTypes.SELECT, + }); + + // replicate the data ibm_db adds on an error object + const error = new Error(errorData[0].DIAGTEXT); + error.sqlcode = errorData[0].SQLCODE; + error.sql = errorData[0].STATEMENT; + error.state = errorData[0].SQLSTATE; + + const wrappedError = new DatabaseError(error); + + try { + await this.dropTable({ + tableName: errorTable, + schema: errorSchema, + }); + } catch (dropError) { + throw new AggregateError([ + wrappedError, + new Error(`An error occurred while cleaning up table ${errorSchema}.${errorTable}`, { cause: dropError }), + ]); + } + + // -204 is "name is undefined" (schema does not exist) + // 'queryInterface.dropSchema' is supposed to be DROP SCHEMA IF EXISTS + // so we can ignore this error + if (error.sqlcode === -204 && error.state === '42704') { + return response; + } + + throw wrappedError; + } + + return response; + } + async createTable(tableName, attributes, options, model) { let sql = ''; @@ -122,4 +178,35 @@ export class Db2QueryInterface extends QueryInterface { return await this.sequelize.queryRaw(sql, options); } + async addConstraint(tableName, options) { + try { + return await super.addConstraint(tableName, options); + } catch (error) { + if (!error.cause) { + throw error; + } + + // Operation not allowed for reason code "7" on table "DB2INST1.users". SQLSTATE=57007 + if (error.cause.sqlcode !== -668 || error.cause.state !== '57007') { + throw error; + } + + // https://www.ibm.com/support/pages/how-verify-and-resolve-sql0668n-reason-code-7-when-accessing-table + await this.executeTableReorg(tableName); + await super.addConstraint(tableName, options); + } + } + + /** + * DB2 can put tables in the "reorg pending" state after a structure change (e.g. ALTER) + * Other changes cannot be done to these tables until the reorg has been completed. + * + * This method forces a reorg to happen now. + * + * @param {TableName} tableName - The name of the table to reorg + */ + async executeTableReorg(tableName) { + // https://www.ibm.com/support/pages/sql0668n-operating-not-allowed-reason-code-7-seen-when-querying-or-viewing-table-db2-warehouse-cloud-and-db2-cloud + return await this.sequelize.query(`CALL SYSPROC.ADMIN_CMD('REORG TABLE ${this.queryGenerator.quoteTable(tableName)}')`); + } } diff --git a/src/dialects/db2/query.js b/src/dialects/db2/query.js index b2d65db65f60..61c291030e6e 100644 --- a/src/dialects/db2/query.js +++ b/src/dialects/db2/query.js @@ -95,6 +95,8 @@ export class Db2Query extends AbstractQuery { const SQL = this.sql.toUpperCase(); let newSql = this.sql; + + // TODO: move this to Db2QueryGenerator if ((this.isSelectQuery() || _.startsWith(SQL, 'SELECT ')) && !SQL.includes(' FROM ', 8)) { if (this.sql.charAt(this.sql.length - 1) === ';') { @@ -112,6 +114,17 @@ export class Db2Query extends AbstractQuery { stmt.execute(params, (err, result, outparams) => { debug(`executed(${this.connection.uuid || 'default'}):${newSql} ${parameters ? util.inspect(parameters, { compact: true, breakLength: Infinity }) : ''}`); + // map the INOUT parameters to the name provided by the dev + // this is an internal API, not yet ready for dev consumption, hence the _unsafe_ prefix. + if (outparams && this.options.bindParameterOrder && this.options._unsafe_db2Outparams) { + for (let i = 0; i < this.options.bindParameterOrder.length; i++) { + const paramName = this.options.bindParameterOrder[i]; + const paramValue = outparams[i]; + + this.options._unsafe_db2Outparams.set(paramName, paramValue); + } + } + if (benchmark) { this.sequelize.log(`Executed (${this.connection.uuid || 'default'}): ${newSql} ${parameters ? util.inspect(parameters, { compact: true, breakLength: Infinity }) : ''}`, Date.now() - queryBegin, this.options); } @@ -190,51 +203,10 @@ export class Db2Query extends AbstractQuery { } filterSQLError(err, sql, connection) { - if (err.message.search('SQL0204N') !== -1 && _.startsWith(sql, 'DROP ')) { - err = null; // Ignore table not found error for drop table. - } else if (err.message.search('SQL0443N') !== -1) { - if (this.isDropSchemaQuery()) { - // Delete ERRORSCHEMA.ERRORTABLE if it exist. - connection.querySync('DROP TABLE ERRORSCHEMA.ERRORTABLE;'); - // Retry deleting the schema - connection.querySync(this.sql); - } - - err = null; // Ignore drop schema error. - } else if (err.message.search('SQL0601N') !== -1) { - const match = err.message.match(/SQL0601N {2}The name of the object to be created is identical to the existing name "(.*)" of type "(.*)"./); - if (match && match.length > 1 && match[2] === 'TABLE') { - let table; - const mtarray = match[1].split('.'); - if (mtarray[1]) { - table = `"${mtarray[0]}"."${mtarray[1]}"`; - } else { - table = `"${mtarray[0]}"`; - } - - if (connection.dropTable !== false) { - connection.querySync(`DROP TABLE ${table}`); - err = connection.querySync(sql); - } else { - err = null; - } - } else { - err = null; // Ignore create schema error. - } - } else if (err.message.search('SQL0911N') !== -1) { - if (err.message.search('Reason code "2"') !== -1) { - err = null; // Ignore deadlock error due to program logic. - } - } else if (err.message.search('SQL0605W') !== -1) { - err = null; // Ignore warning. - } else if (err.message.search('SQL0668N') !== -1 - && _.startsWith(sql, 'ALTER TABLE ')) { - connection.querySync(`CALL SYSPROC.ADMIN_CMD('REORG TABLE ${sql.slice(12).split(' ')[0]}')`); - err = connection.querySync(sql); - } - - if (err && err.length === 0) { - err = null; + // This error is safe to ignore: + // [IBM][CLI Driver][DB2/LINUXX8664] SQL0605W The index was not created because an index "x" with a matching definition already exists. SQLSTATE=01550 + if (err.message.search('SQL0605W') !== -1) { + return null; } return err; diff --git a/src/sequelize.js b/src/sequelize.js index 08a55a5a5491..c0d4bf8f365e 100644 --- a/src/sequelize.js +++ b/src/sequelize.js @@ -526,9 +526,10 @@ Only bind parameters can be provided, in the dialect-specific syntax. Use Sequelize#query if you wish to use replacements.`); } - options = { ...this.options.query, ...options }; + options = { ...this.options.query, ...options, bindParameterOrder: null }; let bindParameters; + let bindParameterOrder; if (options.bind != null) { const isBindArray = Array.isArray(options.bind); if (!isPlainObject(options.bind) && !isBindArray) { @@ -549,6 +550,8 @@ Use Sequelize#query if you wish to use replacements.`); sql = mappedResult.sql; + // used by dialects that support "INOUT" parameters to map the OUT parameters back the the name the dev used. + options.bindParameterOrder = mappedResult.bindOrder; if (mappedResult.bindOrder == null) { bindParameters = options.bind; } else { diff --git a/test/integration/model.test.js b/test/integration/model.test.js index 75a2131eea6a..4d28da40806d 100644 --- a/test/integration/model.test.js +++ b/test/integration/model.test.js @@ -2145,7 +2145,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { const expectedLengths = { mssql: 2, postgres: 2, - db2: 10, + db2: 2, mariadb: 3, mysql: 1, sqlite: 1, diff --git a/test/integration/model/schema.test.js b/test/integration/model/schema.test.js index 90fe9292d79f..863930f9f81b 100644 --- a/test/integration/model/schema.test.js +++ b/test/integration/model/schema.test.js @@ -481,73 +481,6 @@ describe(Support.getTestDialectTeaser('Model'), () => { } }); }); - - describe('regressions', () => { - it('should be able to sync model with schema', async function () { - const User = this.sequelize.define('User1', { - name: DataTypes.STRING, - value: DataTypes.INTEGER, - }, { - schema: SCHEMA_ONE, - indexes: [ - { - name: 'test_slug_idx', - fields: ['name'], - }, - ], - }); - - const Task = this.sequelize.define('Task2', { - name: DataTypes.STRING, - value: DataTypes.INTEGER, - }, { - schema: SCHEMA_TWO, - indexes: [ - { - name: 'test_slug_idx', - fields: ['name'], - }, - ], - }); - - await User.sync({ force: true }); - await Task.sync({ force: true }); - - const [user, task] = await Promise.all([ - this.sequelize.queryInterface.describeTable(User.tableName, SCHEMA_ONE), - this.sequelize.queryInterface.describeTable(Task.tableName, SCHEMA_TWO), - ]); - - expect(user).to.be.ok; - expect(task).to.be.ok; - }); - - // TODO: this should work with MSSQL / MariaDB too - // Need to fix addSchema return type - if (dialect.startsWith('postgres')) { - it('defaults to schema provided to sync() for references #11276', async function () { - const User = this.sequelize.define('UserXYZ', { - uid: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true, - allowNull: false, - }, - }); - const Task = this.sequelize.define('TaskXYZ', {}); - - Task.belongsTo(User); - - await User.sync({ force: true, schema: SCHEMA_ONE }); - await Task.sync({ force: true, schema: SCHEMA_ONE }); - const user0 = await User.schema(SCHEMA_ONE).create({}); - const task = await Task.schema(SCHEMA_ONE).create({}); - await task.setUserXYZ(user0); - const user = await task.getUserXYZ({ schema: SCHEMA_ONE }); - expect(user).to.be.ok; - }); - } - }); }); } }); diff --git a/test/integration/model/sync.test.js b/test/integration/model/sync.test.js index dc4f8f73fc10..283048988e89 100644 --- a/test/integration/model/sync.test.js +++ b/test/integration/model/sync.test.js @@ -6,7 +6,7 @@ const { sequelize, getTestDialect, getTestDialectTeaser } = require('../support' const dialect = getTestDialect(); -describe(getTestDialectTeaser('Model.sync & Sequelize.sync'), () => { +describe(getTestDialectTeaser('Model.sync & Sequelize#sync'), () => { it('removes a column if it exists in the databases schema but not the model', async () => { const User = sequelize.define('testSync', { name: DataTypes.STRING, @@ -423,6 +423,90 @@ describe(getTestDialectTeaser('Model.sync & Sequelize.sync'), () => { expect(getIndexFields(out2[0])).to.deep.eq(['email']); expect(out2[0].unique).to.eq(true, 'index should not be unique'); }); + + const SCHEMA_ONE = 'schema_one'; + const SCHEMA_TWO = 'schema_two'; + + if (sequelize.dialect.supports.schemas) { + it('can create two identically named indexes in different schemas', async () => { + await Promise.all([ + sequelize.createSchema(SCHEMA_ONE), + sequelize.createSchema(SCHEMA_TWO), + ]); + + const User = sequelize.define('User1', { + name: DataTypes.STRING, + }, { + schema: SCHEMA_ONE, + indexes: [ + { + name: 'test_slug_idx', + fields: ['name'], + }, + ], + }); + + const Task = sequelize.define('Task2', { + name: DataTypes.STRING, + }, { + schema: SCHEMA_TWO, + indexes: [ + { + name: 'test_slug_idx', + fields: ['name'], + }, + ], + }); + + await User.sync({ force: true }); + await Task.sync({ force: true }); + + const [userIndexes, taskIndexes] = await Promise.all([ + getNonPrimaryIndexes(User), + getNonPrimaryIndexes(Task), + ]); + + expect(userIndexes).to.have.length(1); + expect(taskIndexes).to.have.length(1); + + expect(userIndexes[0].name).to.eq('test_slug_idx'); + expect(taskIndexes[0].name).to.eq('test_slug_idx'); + }); + } + + // TODO: this should work with MSSQL / MariaDB too + // Need to fix addSchema return type + if (dialect.startsWith('postgres')) { + it('defaults to schema provided to sync() for references #11276', async function () { + await Promise.all([ + sequelize.createSchema(SCHEMA_ONE), + sequelize.createSchema(SCHEMA_TWO), + ]); + + const User = this.sequelize.define('UserXYZ', { + uid: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + }, + }); + const Task = this.sequelize.define('TaskXYZ', {}); + + Task.belongsTo(User); + + // TODO: do we really want to keep this? Shouldn't model schemas be defined and fixed? + await User.sync({ force: true, schema: SCHEMA_ONE }); + await Task.sync({ force: true, schema: SCHEMA_ONE }); + const user0 = await User.withSchema(SCHEMA_ONE).create({}); + const task = await Task.withSchema(SCHEMA_ONE).create({}); + await task.setUserXYZ(user0); + + // TODO: do we really want to keep this? Shouldn't model schemas be defined and fixed? + const user = await task.getUserXYZ({ schema: SCHEMA_ONE }); + expect(user).to.be.ok; + }); + } }); async function getNonPrimaryIndexes(model) { diff --git a/test/integration/query-interface.test.js b/test/integration/query-interface.test.js index d4c5dc63b232..4f4f5c1ca74c 100644 --- a/test/integration/query-interface.test.js +++ b/test/integration/query-interface.test.js @@ -41,7 +41,16 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { it('should not contain views', async function () { async function cleanup(sequelize) { if (dialect === 'db2') { - await sequelize.query('DROP VIEW V_Fail'); + // DB2 does not support DROP VIEW IF EXISTS + try { + await sequelize.query('DROP VIEW V_Fail'); + } catch (error) { + // -204 means V_Fail does not exist + // https://www.ibm.com/docs/en/db2-for-zos/11?topic=sec-204 + if (error.cause.sqlcode !== -204) { + throw error; + } + } } else { await sequelize.query('DROP VIEW IF EXISTS V_Fail'); } @@ -609,6 +618,7 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { type: DataTypes.STRING, allowNull: false, }); + await this.queryInterface.addConstraint('users', { fields: ['username'], type: 'PRIMARY KEY', @@ -625,6 +635,39 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { constraints = constraints.map(constraint => constraint.constraintName); expect(constraints).to.not.include(expectedConstraintName); }); + + // TODO: addConstraint does not support schemas yet. + it.skip('can add a constraint to a table in a non-default schema', async function () { + const tableName = { + tableName: 'users', + schema: 'archive', + }; + + await this.queryInterface.createTable(tableName, { + id: { + type: DataTypes.INTEGER, + }, + }); + + // changeColumn before addConstraint puts the DB2 table in "reorg pending state" + // addConstraint will be forced to execute a REORG TABLE command, which checks that it is done properly when using schemas. + await this.queryInterface.changeColumn(tableName, 'id', { + type: DataTypes.BIGINT, + }); + + await this.queryInterface.addConstraint(tableName, { + type: 'PRIMARY KEY', + fields: ['id'], + }); + + const constraints = await this.queryInterface.showConstraint(tableName); + + expect(constraints).to.deep.eq([{ + constraintName: 'users_username_pk', + schemaName: tableName.schema, + tableName: tableName.tableName, + }]); + }); }); describe('foreign key', () => { @@ -634,10 +677,12 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { type: DataTypes.STRING, allowNull: false, }); + await this.queryInterface.addConstraint('users', { type: 'PRIMARY KEY', fields: ['username'], }); + await this.queryInterface.addConstraint('posts', { fields: ['username'], references: { diff --git a/test/integration/query-interface/changeColumn.test.js b/test/integration/query-interface/changeColumn.test.js index 6a857eea295d..c3e2d1774874 100644 --- a/test/integration/query-interface/changeColumn.test.js +++ b/test/integration/query-interface/changeColumn.test.js @@ -9,12 +9,9 @@ const { DataTypes } = require('@sequelize/core'); const dialect = Support.getTestDialect(); describe(Support.getTestDialectTeaser('QueryInterface'), () => { - beforeEach(function () { + beforeEach(async function () { this.sequelize.options.quoteIdenifiers = true; this.queryInterface = this.sequelize.getQueryInterface(); - }); - - afterEach(async function () { await Support.dropTestSchemas(this.sequelize); }); diff --git a/test/integration/sequelize/query.test.js b/test/integration/sequelize/query.test.js index 620501fd1674..c683d1ec31af 100644 --- a/test/integration/sequelize/query.test.js +++ b/test/integration/sequelize/query.test.js @@ -266,22 +266,28 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { })).to.include('john'); }); } else if (dialect === 'db2') { - it('executes stored procedures', function () { - const self = this; - - return self.sequelize.query(this.insertQuery).then(() => { - return self.sequelize.query('DROP PROCEDURE foo').then(() => { - return self.sequelize.query( - `CREATE PROCEDURE foo() DYNAMIC RESULT SETS 1 LANGUAGE SQL BEGIN DECLARE cr1 CURSOR WITH RETURN FOR SELECT * FROM ${qq(self.User.tableName)}; OPEN cr1; END`, - ).then(() => { - return self.sequelize.query('CALL foo()').then(users => { - expect(users.map(u => { - return u.username; - })).to.include('john'); - }); - }); - }); - }); + it('executes stored procedures', async function () { + const { sequelize } = this; + + await sequelize.query(this.insertQuery); + + try { + await sequelize.query('DROP PROCEDURE foo'); + } catch (error) { + // DB2 does not support DROP PROCEDURE IF EXISTS + // -204 means "FOO" does not exist + // https://www.ibm.com/docs/en/db2-for-zos/11?topic=sec-204 + if (error.cause.sqlcode !== -204) { + throw error; + } + } + + await sequelize.query( + `CREATE PROCEDURE foo() DYNAMIC RESULT SETS 1 LANGUAGE SQL BEGIN DECLARE cr1 CURSOR WITH RETURN FOR SELECT * FROM ${qq(this.User.tableName)}; OPEN cr1; END`, + ); + + const users = await sequelize.query('CALL foo()'); + expect(users.map(u => u.username)).to.include('john'); }); } else { console.log(': I want to be supported in this dialect as well :-('); diff --git a/test/integration/support.js b/test/integration/support.js index 969e7de39c5f..d36a0cdb2600 100644 --- a/test/integration/support.js +++ b/test/integration/support.js @@ -4,14 +4,39 @@ // avoiding to be affected unintentionally by `sinon.useFakeTimers()` called by the tests themselves. const { setTimeout, clearTimeout } = global; +const { QueryTypes } = require('@sequelize/core'); const pTimeout = require('p-timeout'); const Support = require('../support'); +const { getTestDialect } = require('../support'); const CLEANUP_TIMEOUT = Number.parseInt(process.env.SEQ_TEST_CLEANUP_TIMEOUT, 10) || 10_000; let runningQueries = new Set(); -before(function () { +before(async function () { + if (getTestDialect() === 'db2') { + const res = await this.sequelize.query(`SELECT TBSPACE FROM SYSCAT.TABLESPACES WHERE TBSPACE = 'SYSTOOLSPACE'`, { + type: QueryTypes.SELECT, + }); + + const tableExists = res[0]?.TBSPACE === 'SYSTOOLSPACE'; + + if (!tableExists) { + // needed by dropSchema function + await this.sequelize.query(` + CREATE TABLESPACE SYSTOOLSPACE IN IBMCATGROUP + MANAGED BY AUTOMATIC STORAGE USING STOGROUP IBMSTOGROUP + EXTENTSIZE 4; + `); + + await this.sequelize.query(` + CREATE USER TEMPORARY TABLESPACE SYSTOOLSTMPSPACE IN IBMCATGROUP + MANAGED BY AUTOMATIC STORAGE USING STOGROUP IBMSTOGROUP + EXTENTSIZE 4 + `); + } + } + this.sequelize.addHook('beforeQuery', (options, query) => { runningQueries.add(query); }); diff --git a/test/support.ts b/test/support.ts index 7ef7548483ee..ff0fd665d784 100644 --- a/test/support.ts +++ b/test/support.ts @@ -224,11 +224,20 @@ export async function dropTestSchemas(sequelize: Sequelize) { // @ts-expect-error const schemaName = schema.name ? schema.name : schema; if (schemaName !== sequelize.config.database) { - schemasPromise.push(sequelize.dropSchema(schemaName)); + const promise = sequelize.dropSchema(schemaName); + + if (getTestDialect() === 'db2') { + // https://github.com/sequelize/sequelize/pull/14453#issuecomment-1155581572 + // DB2 can sometimes deadlock / timeout when deleting more than one schema at the same time. + // eslint-disable-next-line no-await-in-loop + await promise; + } else { + schemasPromise.push(promise); + } } } - await Promise.all(schemasPromise.map(async p => p.catch((error: unknown) => error))); + await Promise.all(schemasPromise); } export function getSupportedDialects() { diff --git a/test/unit/dialects/db2/query-generator.test.js b/test/unit/dialects/db2/query-generator.test.js index 75a706055499..a07a87ac85df 100644 --- a/test/unit/dialects/db2/query-generator.test.js +++ b/test/unit/dialects/db2/query-generator.test.js @@ -119,54 +119,54 @@ if (dialect === 'db2') { createTableQuery: [ { arguments: ['myTable', { title: 'VARCHAR(255)', name: 'VARCHAR(255)' }], - expectation: 'CREATE TABLE "myTable" ("title" VARCHAR(255), "name" VARCHAR(255));', + expectation: 'CREATE TABLE IF NOT EXISTS "myTable" ("title" VARCHAR(255), "name" VARCHAR(255));', }, { arguments: ['myTable', { data: 'BLOB' }], - expectation: 'CREATE TABLE "myTable" ("data" BLOB);', + expectation: 'CREATE TABLE IF NOT EXISTS "myTable" ("data" BLOB);', }, { arguments: ['myTable', { data: 'BLOB(16M)' }], - expectation: 'CREATE TABLE "myTable" ("data" BLOB(16M));', + expectation: 'CREATE TABLE IF NOT EXISTS "myTable" ("data" BLOB(16M));', }, { arguments: ['myTable', { title: 'VARCHAR(255)', name: 'VARCHAR(255)' }, { engine: 'MyISAM' }], - expectation: 'CREATE TABLE "myTable" ("title" VARCHAR(255), "name" VARCHAR(255));', + expectation: 'CREATE TABLE IF NOT EXISTS "myTable" ("title" VARCHAR(255), "name" VARCHAR(255));', }, { arguments: ['myTable', { title: 'VARCHAR(255)', name: 'VARCHAR(255)' }, { charset: 'utf8', collate: 'utf8_unicode_ci' }], - expectation: 'CREATE TABLE "myTable" ("title" VARCHAR(255), "name" VARCHAR(255));', + expectation: 'CREATE TABLE IF NOT EXISTS "myTable" ("title" VARCHAR(255), "name" VARCHAR(255));', }, { arguments: ['myTable', { title: 'VARCHAR(255)', name: 'VARCHAR(255)' }, { charset: 'latin1' }], - expectation: 'CREATE TABLE "myTable" ("title" VARCHAR(255), "name" VARCHAR(255));', + expectation: 'CREATE TABLE IF NOT EXISTS "myTable" ("title" VARCHAR(255), "name" VARCHAR(255));', }, { arguments: ['myTable', { title: 'ENUM("A", "B", "C")', name: 'VARCHAR(255)' }, { charset: 'latin1' }], - expectation: 'CREATE TABLE "myTable" ("title" ENUM("A", "B", "C"), "name" VARCHAR(255));', + expectation: 'CREATE TABLE IF NOT EXISTS "myTable" ("title" ENUM("A", "B", "C"), "name" VARCHAR(255));', }, { arguments: ['myTable', { title: 'VARCHAR(255)', name: 'VARCHAR(255)' }, { rowFormat: 'default' }], - expectation: 'CREATE TABLE "myTable" ("title" VARCHAR(255), "name" VARCHAR(255));', + expectation: 'CREATE TABLE IF NOT EXISTS "myTable" ("title" VARCHAR(255), "name" VARCHAR(255));', }, { arguments: ['myTable', { title: 'VARCHAR(255)', name: 'VARCHAR(255)', id: 'INTEGER PRIMARY KEY' }], - expectation: 'CREATE TABLE "myTable" ("title" VARCHAR(255), "name" VARCHAR(255), "id" INTEGER , PRIMARY KEY ("id"));', + expectation: 'CREATE TABLE IF NOT EXISTS "myTable" ("title" VARCHAR(255), "name" VARCHAR(255), "id" INTEGER , PRIMARY KEY ("id"));', }, { arguments: ['myTable', { title: 'VARCHAR(255)', name: 'VARCHAR(255)', otherId: 'INTEGER REFERENCES otherTable (id) ON DELETE CASCADE ON UPDATE NO ACTION' }], - expectation: 'CREATE TABLE "myTable" ("title" VARCHAR(255), "name" VARCHAR(255), "otherId" INTEGER, FOREIGN KEY ("otherId") REFERENCES otherTable (id) ON DELETE CASCADE ON UPDATE NO ACTION);', + expectation: 'CREATE TABLE IF NOT EXISTS "myTable" ("title" VARCHAR(255), "name" VARCHAR(255), "otherId" INTEGER, FOREIGN KEY ("otherId") REFERENCES otherTable (id) ON DELETE CASCADE ON UPDATE NO ACTION);', }, { arguments: ['myTable', { title: 'VARCHAR(255)', name: 'VARCHAR(255)' }, { uniqueKeys: [{ fields: ['title', 'name'], customIndex: true }] }], - expectation: 'CREATE TABLE "myTable" ("title" VARCHAR(255) NOT NULL, "name" VARCHAR(255) NOT NULL, CONSTRAINT "uniq_myTable_title_name" UNIQUE ("title", "name"));', + expectation: 'CREATE TABLE IF NOT EXISTS "myTable" ("title" VARCHAR(255) NOT NULL, "name" VARCHAR(255) NOT NULL, CONSTRAINT "uniq_myTable_title_name" UNIQUE ("title", "name"));', }, ], dropTableQuery: [ { arguments: ['myTable'], - expectation: 'DROP TABLE "myTable";', + expectation: 'DROP TABLE IF EXISTS "myTable";', }, ], diff --git a/test/unit/sql/create-table.test.js b/test/unit/sql/create-table.test.js index 2a7e7235bb46..5bf641580f25 100644 --- a/test/unit/sql/create-table.test.js +++ b/test/unit/sql/create-table.test.js @@ -25,7 +25,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { it('references enum in the right schema #3171', () => { expectsql(sql.createTableQuery(FooUser.getTableName(), sql.attributesToSQL(FooUser.rawAttributes), {}), { sqlite: 'CREATE TABLE IF NOT EXISTS `foo.users` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `mood` TEXT);', - db2: 'CREATE TABLE "foo"."users" ("id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY(START WITH 1, INCREMENT BY 1) , "mood" VARCHAR(255) CHECK ("mood" IN(\'happy\', \'sad\')), PRIMARY KEY ("id"));', + db2: 'CREATE TABLE IF NOT EXISTS "foo"."users" ("id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY(START WITH 1, INCREMENT BY 1) , "mood" VARCHAR(255) CHECK ("mood" IN(\'happy\', \'sad\')), PRIMARY KEY ("id"));', postgres: 'CREATE TABLE IF NOT EXISTS "foo"."users" ("id" SERIAL , "mood" "foo"."enum_users_mood", PRIMARY KEY ("id"));', mariadb: 'CREATE TABLE IF NOT EXISTS `foo`.`users` (`id` INTEGER NOT NULL auto_increment , `mood` ENUM(\'happy\', \'sad\'), PRIMARY KEY (`id`)) ENGINE=InnoDB;', mysql: 'CREATE TABLE IF NOT EXISTS `foo.users` (`id` INTEGER NOT NULL auto_increment , `mood` ENUM(\'happy\', \'sad\'), PRIMARY KEY (`id`)) ENGINE=InnoDB;', @@ -60,7 +60,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { it('references right schema when adding foreign key #9029', () => { expectsql(sql.createTableQuery(BarProject.getTableName(), sql.attributesToSQL(BarProject.rawAttributes), {}), { sqlite: 'CREATE TABLE IF NOT EXISTS `bar.projects` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `user_id` INTEGER REFERENCES `bar.users` (`id`) ON DELETE NO ACTION ON UPDATE CASCADE);', - db2: 'CREATE TABLE "bar"."projects" ("id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY(START WITH 1, INCREMENT BY 1) , "user_id" INTEGER, PRIMARY KEY ("id"), FOREIGN KEY ("user_id") REFERENCES "bar"."users" ("id") ON DELETE NO ACTION);', + db2: 'CREATE TABLE IF NOT EXISTS "bar"."projects" ("id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY(START WITH 1, INCREMENT BY 1) , "user_id" INTEGER, PRIMARY KEY ("id"), FOREIGN KEY ("user_id") REFERENCES "bar"."users" ("id") ON DELETE NO ACTION);', postgres: 'CREATE TABLE IF NOT EXISTS "bar"."projects" ("id" SERIAL , "user_id" INTEGER REFERENCES "bar"."users" ("id") ON DELETE NO ACTION ON UPDATE CASCADE, PRIMARY KEY ("id"));', mariadb: 'CREATE TABLE IF NOT EXISTS `bar`.`projects` (`id` INTEGER NOT NULL auto_increment , `user_id` INTEGER, PRIMARY KEY (`id`), FOREIGN KEY (`user_id`) REFERENCES `bar`.`users` (`id`) ON DELETE NO ACTION ON UPDATE CASCADE) ENGINE=InnoDB;', mysql: 'CREATE TABLE IF NOT EXISTS `bar.projects` (`id` INTEGER NOT NULL auto_increment , `user_id` INTEGER, PRIMARY KEY (`id`), FOREIGN KEY (`user_id`) REFERENCES `bar.users` (`id`) ON DELETE NO ACTION ON UPDATE CASCADE) ENGINE=InnoDB;', @@ -94,7 +94,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { expectsql(sql.createTableQuery(Image.getTableName(), sql.attributesToSQL(Image.rawAttributes), {}), { sqlite: 'CREATE TABLE IF NOT EXISTS `images` (`id` INTEGER PRIMARY KEY AUTOINCREMENT REFERENCES `files` (`id`));', postgres: 'CREATE TABLE IF NOT EXISTS "images" ("id" SERIAL REFERENCES "files" ("id"), PRIMARY KEY ("id"));', - db2: 'CREATE TABLE "images" ("id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY(START WITH 1, INCREMENT BY 1) , PRIMARY KEY ("id"), FOREIGN KEY ("id") REFERENCES "files" ("id"));', + db2: 'CREATE TABLE IF NOT EXISTS "images" ("id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY(START WITH 1, INCREMENT BY 1) , PRIMARY KEY ("id"), FOREIGN KEY ("id") REFERENCES "files" ("id"));', mariadb: 'CREATE TABLE IF NOT EXISTS `images` (`id` INTEGER auto_increment , PRIMARY KEY (`id`), FOREIGN KEY (`id`) REFERENCES `files` (`id`)) ENGINE=InnoDB;', mysql: 'CREATE TABLE IF NOT EXISTS `images` (`id` INTEGER auto_increment , PRIMARY KEY (`id`), FOREIGN KEY (`id`) REFERENCES `files` (`id`)) ENGINE=InnoDB;', mssql: 'IF OBJECT_ID(\'[images]\', \'U\') IS NULL CREATE TABLE [images] ([id] INTEGER IDENTITY(1,1) , PRIMARY KEY ([id]), FOREIGN KEY ([id]) REFERENCES [files] ([id]));', diff --git a/test/unit/sql/index.test.js b/test/unit/sql/index.test.js index 754ec1c7b760..131bd28cf091 100644 --- a/test/unit/sql/index.test.js +++ b/test/unit/sql/index.test.js @@ -33,6 +33,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { tableName: 'table', }, ['column1', 'column2'], {}, 'schema_table'), { default: 'CREATE INDEX [schema_table_column1_column2] ON [schema].[table] ([column1], [column2])', + db2: 'CREATE INDEX "schema"."schema_table_column1_column2" ON "schema"."table" ("column1", "column2")', mariadb: 'ALTER TABLE `schema`.`table` ADD INDEX `schema_table_column1_column2` (`column1`, `column2`)', });