Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

fix: resolve various db2 issues #14453

Merged
merged 45 commits into from Jun 15, 2022
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
de084eb
ci: only test db2
ephys Apr 26, 2022
f4c4e3d
ci: update db2 image
ephys Apr 26, 2022
a3a860a
only some tests
ephys Apr 26, 2022
5dd7fc7
ci: try randomizing db2 db name
ephys Apr 26, 2022
b5a0a71
ci: prepare node 14
ephys Apr 26, 2022
863f39e
ci: print db2 logs
ephys Apr 26, 2022
b818776
fix: uppercase db name
ephys Apr 26, 2022
8a3d21a
ci: use _ instead of - in db name
ephys Apr 26, 2022
43aee15
ci: apparently db2 does not accept _ either
ephys Apr 26, 2022
4c9c546
ci: generate smaller DB name
ephys Apr 26, 2022
fdd6fa1
ci: generate shorter db name
ephys Apr 26, 2022
0ea6f29
test: reenable tests
ephys Apr 26, 2022
77b3011
test: small tweaks to see if changeColumn still errors
ephys Apr 26, 2022
53fe653
test: log all queries
ephys Apr 26, 2022
050fb8e
feat: let's see what happens if we don't mute errors
ephys Apr 26, 2022
8037ebc
fix: use 'if exists' in db2 drop table
ephys Apr 26, 2022
c164353
fix: more attempts idk
ephys Apr 26, 2022
26f0dc3
ci: test commit
ephys Jun 12, 2022
134d74e
ci: revert changes
ephys Jun 12, 2022
9c267f9
Merge branch 'main' into ephys/db2-debug-please-ignore
ephys Jun 12, 2022
566aaba
test: remove console.log
ephys Jun 12, 2022
461fbe8
Merge branch 'ephys/db2-debug-please-ignore' of github.com:sequelize/…
ephys Jun 12, 2022
6474da0
test: fix db2 test
ephys Jun 12, 2022
fbbfd9d
fix(db2): use if-not-exists during create table
ephys Jun 12, 2022
9feb422
test(db2): fix db2 tests
ephys Jun 12, 2022
de8141d
test(db2): fix broken test
ephys Jun 12, 2022
7242c47
refactor: clean up
ephys Jun 12, 2022
74061bd
refactor: revert unnecessary change
ephys Jun 12, 2022
6e29070
test(db2): bring back systoolspace creation
ephys Jun 12, 2022
1848c71
Merge branch 'main' into ephys/db2-debug-please-ignore
ephys Jun 13, 2022
d162544
fix(db2): specify schema in create index
ephys Jun 13, 2022
c425668
test(db2): update tests
ephys Jun 13, 2022
1d53695
fix(sqlite): scope index name
ephys Jun 13, 2022
cf10085
fix(sqlite): rollback change
ephys Jun 13, 2022
30c3566
test(db2): dont create tablespace if it exists
ephys Jun 13, 2022
6dcc25d
test(db2): damnit
ephys Jun 13, 2022
3d72110
feat: move the deletion of error table after drop schema
ephys Jun 13, 2022
04c422a
fix(db2): reduce risk of error table collisions
ephys Jun 13, 2022
c20d44b
feat: re-add forced table reorg
ephys Jun 13, 2022
fbebfe0
fix(db2): make dropSchema "IF EXISTS"
ephys Jun 14, 2022
e2a4b99
Merge branch 'main' into ephys/db2-debug-please-ignore
ephys Jun 14, 2022
3bc66e7
test(db2): prevent drop schema deadlock
ephys Jun 14, 2022
d039753
Merge branch 'ephys/db2-debug-please-ignore' of github.com:sequelize/…
ephys Jun 14, 2022
168d231
fix: fix copy paste mistake
ephys Jun 15, 2022
10b37b2
test: add failing test for addConstraint with schema
ephys Jun 15, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 1 addition & 2 deletions 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
Expand All @@ -12,4 +11,4 @@ PERSISTENT_HOME=false
HADR_ENABLED=false
ETCD_ENDPOINT=
ETCD_USERNAME=
ETCD_PASSWORD=
ETCD_PASSWORD=
8 changes: 6 additions & 2 deletions 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
Expand All @@ -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
Expand All @@ -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
Expand Down
23 changes: 15 additions & 8 deletions src/dialects/abstract/query-generator.js
Expand Up @@ -613,32 +613,39 @@ 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',
];
} else {
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,
Expand Down
23 changes: 10 additions & 13 deletions src/dialects/db2/query-generator.js
Expand Up @@ -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++}` },
},
};
}
Expand All @@ -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 = [];
Expand Down Expand Up @@ -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) {
WikiRik marked this conversation as resolved.
Show resolved Hide resolved
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;

Expand Down
87 changes: 87 additions & 0 deletions 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');
Expand Down Expand Up @@ -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 = '';

Expand Down Expand Up @@ -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)}')`);
WikiRik marked this conversation as resolved.
Show resolved Hide resolved
}
}
62 changes: 17 additions & 45 deletions src/dialects/db2/query.js
Expand Up @@ -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) === ';') {
Expand All @@ -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);
}
Expand Down Expand Up @@ -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;
Expand Down
5 changes: 4 additions & 1 deletion src/sequelize.js
Expand Up @@ -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) {
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion test/integration/model.test.js
Expand Up @@ -2145,7 +2145,7 @@ describe(Support.getTestDialectTeaser('Model'), () => {
const expectedLengths = {
mssql: 2,
postgres: 2,
db2: 10,
db2: 2,
ephys marked this conversation as resolved.
Show resolved Hide resolved
mariadb: 3,
mysql: 1,
sqlite: 1,
Expand Down