diff --git a/lib/dialects/mssql/schema/mssql-tablecompiler.js b/lib/dialects/mssql/schema/mssql-tablecompiler.js index 3c5bc9db0f..c4470954b5 100644 --- a/lib/dialects/mssql/schema/mssql-tablecompiler.js +++ b/lib/dialects/mssql/schema/mssql-tablecompiler.js @@ -279,13 +279,14 @@ class TableCompiler_MSSQL extends TableCompiler { * Create a unique index. * * @param {string | string[]} columns - * @param {string | {indexName: undefined | string, deferrable?: 'not deferrable'|'deferred'|'immediate' }} indexName + * @param {string | {indexName: undefined | string, deferrable?: 'not deferrable'|'deferred'|'immediate', useConstraint?: true|false }} indexName */ unique(columns, indexName) { /** @type {string | undefined} */ let deferrable; + let useConstraint = false; if (isObject(indexName)) { - ({ indexName, deferrable } = indexName); + ({ indexName, deferrable, useConstraint } = indexName); } if (deferrable && deferrable !== 'not deferrable') { this.client.logger.warn( @@ -304,13 +305,23 @@ class TableCompiler_MSSQL extends TableCompiler { .map((column) => this.formatter.columnize(column) + ' IS NOT NULL') .join(' AND '); - // make unique constraint that allows null https://stackoverflow.com/a/767702/360060 - // to be more or less compatible with other DBs (if any of the columns is NULL then "duplicates" are allowed) - this.pushQuery( - `CREATE UNIQUE INDEX ${indexName} ON ${this.tableName()} (${this.formatter.columnize( - columns - )}) WHERE ${whereAllTheColumnsAreNotNull}` - ); + if (useConstraint) { + // mssql supports unique indexes and unique constraints. + // unique indexes cannot be used with foreign key relationships hence unique constraints are used instead. + this.pushQuery( + `ALTER TABLE ${this.tableName()} ADD CONSTRAINT ${indexName} UNIQUE (${this.formatter.columnize( + columns + )})` + ); + } else { + // make unique constraint that allows null https://stackoverflow.com/a/767702/360060 + // to be more or less compatible with other DBs (if any of the columns is NULL then "duplicates" are allowed) + this.pushQuery( + `CREATE UNIQUE INDEX ${indexName} ON ${this.tableName()} (${this.formatter.columnize( + columns + )}) WHERE ${whereAllTheColumnsAreNotNull}` + ); + } } // Compile a drop index command. diff --git a/test/integration2/dialects/mssql.spec.js b/test/integration2/dialects/mssql.spec.js index 934d40e413..25c488d42a 100644 --- a/test/integration2/dialects/mssql.spec.js +++ b/test/integration2/dialects/mssql.spec.js @@ -170,7 +170,7 @@ describe('MSSQL dialect', () => { }); }); - describe('unique table constraint with options object', () => { + describe('unique table index with options object', () => { const tableName = 'test_unique_index_options'; before(async () => { await knex.schema.createTable(tableName, function () { @@ -188,7 +188,39 @@ describe('MSSQL dialect', () => { await knex.schema.alterTable(tableName, function () { this.unique(['x', 'y'], { indexName }); }); - expect( + await expect( + knex + .insert([ + { x: 1, y: 1 }, + { x: 1, y: 1 }, + ]) + .into(tableName) + ).to.eventually.be.rejectedWith(new RegExp(indexName)); + }); + }); + + describe('unique table constraint with options object', () => { + const tableName = 'test_unique_constraint_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 and constraint in options object', async () => { + const indexName = `UK_${tableName}_x_y`; + await knex.schema.alterTable(tableName, function () { + this.unique(['x', 'y'], { + indexName: indexName, + useConstraint: true, + }); + }); + await expect( knex .insert([ { x: 1, y: 1 }, diff --git a/test/unit/schema-builder/mssql.js b/test/unit/schema-builder/mssql.js index 709ff44114..b635bd1ccb 100644 --- a/test/unit/schema-builder/mssql.js +++ b/test/unit/schema-builder/mssql.js @@ -570,6 +570,20 @@ describe('MSSQL SchemaBuilder', function () { ); }); + it('test adding unique constraint', function () { + tableSql = client + .schemaBuilder() + .table('users', function () { + this.unique('foo', { indexName: 'bar', useConstraint: true }); + }) + .toSQL(); + + equal(1, tableSql.length); + expect(tableSql[0].sql).to.equal( + 'ALTER TABLE [users] ADD CONSTRAINT [bar] UNIQUE ([foo])' + ); + }); + it('test adding index', function () { tableSql = client .schemaBuilder() diff --git a/types/index.d.ts b/types/index.d.ts index 53bd9f7c6e..183aac1bee 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -2075,7 +2075,7 @@ export declare namespace Knex { ): TableBuilder; setNullable(column: string): TableBuilder; dropNullable(column: string): TableBuilder; - unique(columnNames: readonly (string | Raw)[], options?: Readonly<{indexName?: string, storageEngineIndexType?: string, deferrable?: deferrableType}>): TableBuilder; + unique(columnNames: readonly (string | Raw)[], options?: Readonly<{indexName?: string, storageEngineIndexType?: string, deferrable?: deferrableType, useConstraint?: boolean}>): TableBuilder; /** @deprecated */ unique(columnNames: readonly (string | Raw)[], indexName?: string): TableBuilder; foreign(column: string, foreignKeyName?: string): ForeignConstraintBuilder;