diff --git a/lib/dialects/mssql/schema/mssql-tablecompiler.js b/lib/dialects/mssql/schema/mssql-tablecompiler.js index 8e3546de47..111df55cf8 100644 --- a/lib/dialects/mssql/schema/mssql-tablecompiler.js +++ b/lib/dialects/mssql/schema/mssql-tablecompiler.js @@ -4,6 +4,7 @@ // ------- const TableCompiler = require('../../../schema/tablecompiler'); const helpers = require('../../../util/helpers'); +const { isObject } = require('../../../util/is'); // Table Compiler // ------ @@ -223,7 +224,22 @@ class TableCompiler_MSSQL extends TableCompiler { ); } + /** + * Create a primary key. + * + * @param {undefined | string | string[]} columns + * @param {string | {constraintName: string, deferrable?: 'not deferrable'|'deferred'|'immediate' }} constraintName + */ primary(columns, constraintName) { + let deferrable; + if (isObject(constraintName)) { + ({ constraintName, deferrable } = constraintName); + } + if (deferrable && deferrable !== 'not deferrable') { + this.client.logger.warn( + `mssql: primary key constraint [${constraintName}] will not be deferrable ${deferrable} because mssql does not support deferred constraints.` + ); + } constraintName = constraintName ? this.formatter.wrap(constraintName) : this.formatter.wrap(`${this.tableNameRaw}_pkey`); @@ -242,7 +258,23 @@ class TableCompiler_MSSQL extends TableCompiler { } } + /** + * Create a unique index. + * + * @param {string | string[]} columns + * @param {string | {indexName: undefined | string, deferrable?: 'not deferrable'|'deferred'|'immediate' }} indexName + */ unique(columns, indexName) { + /** @type {string | undefined} */ + let deferrable; + if (isObject(indexName)) { + ({ indexName, deferrable } = indexName); + } + if (deferrable && deferrable !== 'not deferrable') { + this.client.logger.warn( + `mssql: unique index [${indexName}] will not be deferrable ${deferrable} because mssql does not support deferred constraints.` + ); + } indexName = indexName ? this.formatter.wrap(indexName) : this._indexCommand('unique', this.tableNameRaw, columns); diff --git a/lib/dialects/mysql/schema/mysql-tablecompiler.js b/lib/dialects/mysql/schema/mysql-tablecompiler.js index 15709a0602..55f7806ab5 100644 --- a/lib/dialects/mysql/schema/mysql-tablecompiler.js +++ b/lib/dialects/mysql/schema/mysql-tablecompiler.js @@ -3,6 +3,7 @@ // MySQL Table Builder & Compiler // ------- const TableCompiler = require('../../../schema/tablecompiler'); +const { isObject } = require('../../../util/is'); // Table Compiler // ------ @@ -218,6 +219,15 @@ class TableCompiler_MySQL extends TableCompiler { } primary(columns, constraintName) { + let deferrable; + if (isObject(constraintName)) { + ({ constraintName, deferrable } = constraintName); + } + if (deferrable && deferrable !== 'not deferrable') { + this.client.logger.warn( + `mysql: primary key constraint \`${constraintName}\` will not be deferrable ${deferrable} because mysql does not support deferred constraints.` + ); + } constraintName = constraintName ? this.formatter.wrap(constraintName) : this.formatter.wrap(`${this.tableNameRaw}_pkey`); @@ -229,6 +239,15 @@ class TableCompiler_MySQL extends TableCompiler { } unique(columns, indexName) { + let deferrable; + if (isObject(indexName)) { + ({ indexName, deferrable } = indexName); + } + if (deferrable && deferrable !== 'not deferrable') { + this.client.logger.warn( + `mysql: unique index \`${indexName}\` will not be deferrable ${deferrable} because mysql does not support deferred constraints.` + ); + } indexName = indexName ? this.formatter.wrap(indexName) : this._indexCommand('unique', this.tableNameRaw, columns); diff --git a/lib/dialects/sqlite3/schema/sqlite-tablecompiler.js b/lib/dialects/sqlite3/schema/sqlite-tablecompiler.js index 4627c70a9d..234c8a5888 100644 --- a/lib/dialects/sqlite3/schema/sqlite-tablecompiler.js +++ b/lib/dialects/sqlite3/schema/sqlite-tablecompiler.js @@ -1,6 +1,7 @@ const filter = require('lodash/filter'); const values = require('lodash/values'); const identity = require('lodash/identity'); +const { isObject } = require('../../../util/is'); const TableCompiler = require('../../../schema/tablecompiler'); const { formatDefault } = require('../../../formatter/formatterUtils'); @@ -122,6 +123,15 @@ class TableCompiler_SQLite3 extends TableCompiler { // Compile a unique key command. unique(columns, indexName) { + let deferrable; + if (isObject(indexName)) { + ({ indexName, deferrable } = indexName); + } + if (deferrable && deferrable !== 'not deferrable') { + this.client.logger.warn( + `sqlite3: unique index \`${indexName}\` will not be deferrable ${deferrable} because sqlite3 does not support deferred constraints.` + ); + } indexName = indexName ? this.formatter.wrap(indexName) : this._indexCommand('unique', this.tableNameRaw, columns); @@ -158,6 +168,16 @@ class TableCompiler_SQLite3 extends TableCompiler { columns = columns.map((column) => this.client.customWrapIdentifier(column, identity) ); + + let deferrable; + if (isObject(constraintName)) { + ({ constraintName, deferrable } = constraintName); + } + if (deferrable && deferrable !== 'not deferrable') { + this.client.logger.warn( + `sqlite3: primary key constraint \`${constraintName}\` will not be deferrable ${deferrable} because sqlite3 does not support deferred constraints.` + ); + } constraintName = this.client.customWrapIdentifier(constraintName, identity); if (this.method !== 'create' && this.method !== 'createIfNot') { diff --git a/test/integration2/dialects/mssql.spec.js b/test/integration2/dialects/mssql.spec.js index 710f58caf8..934d40e413 100644 --- a/test/integration2/dialects/mssql.spec.js +++ b/test/integration2/dialects/mssql.spec.js @@ -170,6 +170,35 @@ describe('MSSQL dialect', () => { }); }); + describe('unique table constraint with options object', () => { + const tableName = 'test_unique_index_options'; + before(async () => { + await knex.schema.createTable(tableName, function () { + this.integer('x').notNull(); + this.integer('y').notNull(); + }); + }); + + after(async () => { + await knex.schema.dropTable(tableName); + }); + + it('accepts indexName in options object', async () => { + const indexName = `AK_${tableName}_x_y`; + await knex.schema.alterTable(tableName, function () { + this.unique(['x', 'y'], { indexName }); + }); + expect( + knex + .insert([ + { x: 1, y: 1 }, + { x: 1, y: 1 }, + ]) + .into(tableName) + ).to.eventually.be.rejectedWith(new RegExp(indexName)); + }); + }); + describe('comment support', () => { const schemaName = 'dbo'; const tableName = 'test_attaches_comments'; diff --git a/test/integration2/schema/primary-keys.spec.js b/test/integration2/schema/primary-keys.spec.js index a3530f6376..1597a83aaf 100644 --- a/test/integration2/schema/primary-keys.spec.js +++ b/test/integration2/schema/primary-keys.spec.js @@ -15,9 +15,6 @@ describe('Schema', () => { before(function () { knex = getKnexForDb(db); - if (isMssql(knex)) { - return this.skip(); - } }); after(() => { @@ -100,8 +97,8 @@ describe('Schema', () => { it('creates a compound primary key', async () => { await knex.schema.alterTable('primary_table', (table) => { - // CockroachDB does not support nullable primary keys - if (isCockroachDB(knex)) { + // CockroachDB and mssql do not support nullable primary keys + if (isCockroachDB(knex) || isMssql(knex)) { table.dropNullable('id_two'); table.dropNullable('id_three'); } @@ -128,49 +125,54 @@ describe('Schema', () => { } }); - it('creates a compound primary key with a custom constraint name', async function () { - // CockroachDB currently does not support dropping primary key without creating new one in the same transaction - if (isCockroachDB(knex)) { - return this.skip(); - } - - await knex.schema.alterTable('primary_table', (table) => { - // CockroachDB does not support nullable primary keys - if (isCockroachDB()) { - table.dropNullable('id_two'); - table.dropNullable('id_three'); + for (const [flavor, customConstraintName] of [ + ['provided directly as a string', 'my_custom_constraint_name'], + [ + 'provided in the options object', + { constraintName: 'my_custom_constraint_name' }, + ], + ]) { + it(`creates a compound primary key with a custom constraint name ${flavor}`, async function () { + // As of 2021-10-02, CockroachDB does not support dropping a primary key without creating a new one in the same transaction. + if (isCockroachDB(knex)) { + return this.skip(); } - table.primary( - ['id_two', 'id_three'], - 'my_custom_constraint_name' - ); - }); - await knex('primary_table').insert({ id_two: 1, id_three: 1 }); - await knex('primary_table').insert({ id_two: 2, id_three: 1 }); - await knex('primary_table').insert({ id_two: 1, id_three: 2 }); + await knex.schema.alterTable('primary_table', (table) => { + // CockroachDB and mssql do not support nullable primary keys + if (isCockroachDB(knex) || isMssql(knex)) { + table.dropNullable('id_two'); + table.dropNullable('id_three'); + } + table.primary(['id_two', 'id_three'], customConstraintName); + }); - try { await knex('primary_table').insert({ id_two: 1, id_three: 1 }); - } catch (err) { - if (isSQLite(knex)) { - expect(err.message).to.equal( - 'insert into `primary_table` (`id_three`, `id_two`) values (1, 1) - SQLITE_CONSTRAINT: UNIQUE constraint failed: primary_table.id_two, primary_table.id_three' - ); - } - if (isPgBased(knex)) { - expect(err.message).to.equal( - 'insert into "primary_table" ("id_three", "id_two") values ($1, $2) - duplicate key value violates unique constraint "my_custom_constraint_name"' - ); + await knex('primary_table').insert({ id_two: 2, id_three: 1 }); + await knex('primary_table').insert({ id_two: 1, id_three: 2 }); + + try { + await knex('primary_table').insert({ id_two: 1, id_three: 1 }); + } catch (err) { + if (isSQLite(knex)) { + expect(err.message).to.equal( + 'insert into `primary_table` (`id_three`, `id_two`) values (1, 1) - SQLITE_CONSTRAINT: UNIQUE constraint failed: primary_table.id_two, primary_table.id_three' + ); + } + if (isPgBased(knex)) { + expect(err.message).to.equal( + 'insert into "primary_table" ("id_three", "id_two") values ($1, $2) - duplicate key value violates unique constraint "my_custom_constraint_name"' + ); + } } - } - await knex.schema.alterTable('primary_table', (table) => { - table.dropPrimary('my_custom_constraint_name'); - }); + await knex.schema.alterTable('primary_table', (table) => { + table.dropPrimary('my_custom_constraint_name'); + }); - await knex('primary_table').insert({ id_two: 1, id_three: 1 }); - }); + await knex('primary_table').insert({ id_two: 1, id_three: 1 }); + }); + } }); }); });