From 6691353e298f73443073e34ab0252de775435046 Mon Sep 17 00:00:00 2001 From: Marat Levit Date: Mon, 13 Dec 2021 09:15:35 +1100 Subject: [PATCH 1/5] Added support for MSSQL unique constraint --- .../mssql/schema/mssql-tablecompiler.js | 29 +++++++++++----- test/integration2/dialects/mssql.spec.js | 34 ++++++++++++++++++- 2 files changed, 53 insertions(+), 10 deletions(-) diff --git a/lib/dialects/mssql/schema/mssql-tablecompiler.js b/lib/dialects/mssql/schema/mssql-tablecompiler.js index 3c5bc9db0f..9c575e5930 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', constraint?: true|false }} indexName */ unique(columns, indexName) { /** @type {string | undefined} */ let deferrable; + let constraint = false; if (isObject(indexName)) { - ({ indexName, deferrable } = indexName); + ({ indexName, deferrable, constraint } = 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 (constraint) { + // 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..06b2fcab35 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 () { @@ -199,6 +199,38 @@ describe('MSSQL dialect', () => { }); }); + 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, + constraint: true, + }); + }); + 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'; From 4be7a8dad1a91b5af1b01d7b96ecd0a8a929dbee Mon Sep 17 00:00:00 2001 From: Marat Levit Date: Mon, 13 Dec 2021 09:57:14 +1100 Subject: [PATCH 2/5] Added syntax test --- test/unit/schema-builder/mssql.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/unit/schema-builder/mssql.js b/test/unit/schema-builder/mssql.js index 709ff44114..e8c302ef6e 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', constraint: 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() From 7a9de41b16bf39e3c4c6003d6a1089012605c2d7 Mon Sep 17 00:00:00 2001 From: Marat Levit Date: Mon, 13 Dec 2021 10:32:03 +1100 Subject: [PATCH 3/5] Added missing await to unique tests --- test/integration2/dialects/mssql.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration2/dialects/mssql.spec.js b/test/integration2/dialects/mssql.spec.js index 06b2fcab35..7714367845 100644 --- a/test/integration2/dialects/mssql.spec.js +++ b/test/integration2/dialects/mssql.spec.js @@ -188,7 +188,7 @@ describe('MSSQL dialect', () => { await knex.schema.alterTable(tableName, function () { this.unique(['x', 'y'], { indexName }); }); - expect( + await expect( knex .insert([ { x: 1, y: 1 }, @@ -220,7 +220,7 @@ describe('MSSQL dialect', () => { constraint: true, }); }); - expect( + await expect( knex .insert([ { x: 1, y: 1 }, From 910bfeea8e0b8d45299656b4ec05b2f164db7c3b Mon Sep 17 00:00:00 2001 From: Marat Levit Date: Mon, 13 Dec 2021 10:34:14 +1100 Subject: [PATCH 4/5] Added missing constraint type --- types/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/index.d.ts b/types/index.d.ts index 53bd9f7c6e..359b280ca3 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, constraint?: boolean}>): TableBuilder; /** @deprecated */ unique(columnNames: readonly (string | Raw)[], indexName?: string): TableBuilder; foreign(column: string, foreignKeyName?: string): ForeignConstraintBuilder; From c7b39567250338a50883f99366da42739f0c2a26 Mon Sep 17 00:00:00 2001 From: Marat Levit Date: Mon, 13 Dec 2021 20:08:42 +1100 Subject: [PATCH 5/5] Updated new argument to useConstraint --- lib/dialects/mssql/schema/mssql-tablecompiler.js | 8 ++++---- test/integration2/dialects/mssql.spec.js | 2 +- test/unit/schema-builder/mssql.js | 2 +- types/index.d.ts | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/dialects/mssql/schema/mssql-tablecompiler.js b/lib/dialects/mssql/schema/mssql-tablecompiler.js index 9c575e5930..c4470954b5 100644 --- a/lib/dialects/mssql/schema/mssql-tablecompiler.js +++ b/lib/dialects/mssql/schema/mssql-tablecompiler.js @@ -279,14 +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', constraint?: true|false }} indexName + * @param {string | {indexName: undefined | string, deferrable?: 'not deferrable'|'deferred'|'immediate', useConstraint?: true|false }} indexName */ unique(columns, indexName) { /** @type {string | undefined} */ let deferrable; - let constraint = false; + let useConstraint = false; if (isObject(indexName)) { - ({ indexName, deferrable, constraint } = indexName); + ({ indexName, deferrable, useConstraint } = indexName); } if (deferrable && deferrable !== 'not deferrable') { this.client.logger.warn( @@ -305,7 +305,7 @@ class TableCompiler_MSSQL extends TableCompiler { .map((column) => this.formatter.columnize(column) + ' IS NOT NULL') .join(' AND '); - if (constraint) { + 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( diff --git a/test/integration2/dialects/mssql.spec.js b/test/integration2/dialects/mssql.spec.js index 7714367845..25c488d42a 100644 --- a/test/integration2/dialects/mssql.spec.js +++ b/test/integration2/dialects/mssql.spec.js @@ -217,7 +217,7 @@ describe('MSSQL dialect', () => { await knex.schema.alterTable(tableName, function () { this.unique(['x', 'y'], { indexName: indexName, - constraint: true, + useConstraint: true, }); }); await expect( diff --git a/test/unit/schema-builder/mssql.js b/test/unit/schema-builder/mssql.js index e8c302ef6e..b635bd1ccb 100644 --- a/test/unit/schema-builder/mssql.js +++ b/test/unit/schema-builder/mssql.js @@ -574,7 +574,7 @@ describe('MSSQL SchemaBuilder', function () { tableSql = client .schemaBuilder() .table('users', function () { - this.unique('foo', { indexName: 'bar', constraint: true }); + this.unique('foo', { indexName: 'bar', useConstraint: true }); }) .toSQL(); diff --git a/types/index.d.ts b/types/index.d.ts index 359b280ca3..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, constraint?: boolean}>): 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;