Skip to content

Commit

Permalink
feat: deferrable option for Unique constraints (Postgres) (#8356)
Browse files Browse the repository at this point in the history
  • Loading branch information
kvnkusch committed Dec 11, 2021
1 parent 531013b commit e52b26c
Show file tree
Hide file tree
Showing 12 changed files with 180 additions and 14 deletions.
22 changes: 14 additions & 8 deletions src/decorator/Unique.ts
@@ -1,33 +1,38 @@
import { getMetadataArgsStorage } from "../globals";
import { UniqueMetadataArgs } from "../metadata-args/UniqueMetadataArgs";
import { UniqueOptions } from "./options/UniqueOptions";

/**
* Composite unique constraint must be set on entity classes and must specify entity's fields to be unique.
*/
export function Unique(name: string, fields: string[]): ClassDecorator & PropertyDecorator;
export function Unique(name: string, fields: string[], options?: UniqueOptions): ClassDecorator & PropertyDecorator;

/**
* Composite unique constraint must be set on entity classes and must specify entity's fields to be unique.
*/
export function Unique(fields: string[]): ClassDecorator & PropertyDecorator;
export function Unique(fields: string[], options?: UniqueOptions): ClassDecorator & PropertyDecorator;

/**
* Composite unique constraint must be set on entity classes and must specify entity's fields to be unique.
*/
export function Unique(fields: (object?: any) => (any[] | { [key: string]: number })): ClassDecorator & PropertyDecorator;
export function Unique(fields: (object?: any) => (any[] | { [key: string]: number }), options?: UniqueOptions): ClassDecorator & PropertyDecorator;

/**
* Composite unique constraint must be set on entity classes and must specify entity's fields to be unique.
*/
export function Unique(name: string, fields: (object?: any) => (any[] | { [key: string]: number })): ClassDecorator & PropertyDecorator;
export function Unique(name: string, fields: (object?: any) => (any[] | { [key: string]: number }), options?: UniqueOptions): ClassDecorator & PropertyDecorator;

/**
* Composite unique constraint must be set on entity classes and must specify entity's fields to be unique.
*/
export function Unique(nameOrFields?: string | string[] | ((object: any) => (any[] | { [key: string]: number })),
maybeFields?: ((object?: any) => (any[] | { [key: string]: number })) | string[]): ClassDecorator & PropertyDecorator {
const name = typeof nameOrFields === "string" ? nameOrFields : undefined;
const fields = typeof nameOrFields === "string" ? <((object?: any) => (any[] | { [key: string]: number })) | string[]>maybeFields : nameOrFields as string[];
export function Unique(nameOrFieldsOrOptions?: string | string[] | ((object: any) => (any[] | { [key: string]: number })) | UniqueOptions,
maybeFieldsOrOptions?: ((object?: any) => (any[] | { [key: string]: number })) | string[] | UniqueOptions,
maybeOptions?: UniqueOptions): ClassDecorator & PropertyDecorator {
const name = typeof nameOrFieldsOrOptions === "string" ? nameOrFieldsOrOptions : undefined;
const fields = typeof nameOrFieldsOrOptions === "string" ? <((object?: any) => (any[] | { [key: string]: number })) | string[]>maybeFieldsOrOptions : nameOrFieldsOrOptions as string[];
let options = (typeof nameOrFieldsOrOptions === "object" && !Array.isArray(nameOrFieldsOrOptions)) ? nameOrFieldsOrOptions as UniqueOptions : maybeOptions;
if (!options)
options = (typeof maybeFieldsOrOptions === "object" && !Array.isArray(maybeFieldsOrOptions)) ? maybeFieldsOrOptions as UniqueOptions : maybeOptions;

return function (clsOrObject: Function | Object, propertyName?: string | symbol) {

Expand All @@ -49,6 +54,7 @@ export function Unique(nameOrFields?: string | string[] | ((object: any) => (any
target: propertyName ? clsOrObject.constructor : clsOrObject as Function,
name: name,
columns,
deferrable: options ? options.deferrable : undefined,
};
getMetadataArgsStorage().uniques.push(args);
};
Expand Down
13 changes: 13 additions & 0 deletions src/decorator/options/UniqueOptions.ts
@@ -0,0 +1,13 @@
import { DeferrableType } from "../../metadata/types/DeferrableType";

/**
* Describes all unique options.
*/
export interface UniqueOptions {

/**
* Indicate if unique constraints can be deferred.
*/
deferrable?: DeferrableType;

}
13 changes: 10 additions & 3 deletions src/driver/postgres/PostgresQueryRunner.ts
Expand Up @@ -1973,7 +1973,8 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner
const uniques = dbConstraints.filter(dbC => dbC["constraint_name"] === constraint["constraint_name"]);
return new TableUnique({
name: constraint["constraint_name"],
columnNames: uniques.map(u => u["column_name"])
columnNames: uniques.map(u => u["column_name"]),
deferrable: constraint["deferrable"] ? constraint["deferred"] : undefined,
});
});

Expand Down Expand Up @@ -2088,7 +2089,10 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner
const uniquesSql = table.uniques.map(unique => {
const uniqueName = unique.name ? unique.name : this.connection.namingStrategy.uniqueConstraintName(table, unique.columnNames);
const columnNames = unique.columnNames.map(columnName => `"${columnName}"`).join(", ");
return `CONSTRAINT "${uniqueName}" UNIQUE (${columnNames})`;
let constraint = `CONSTRAINT "${uniqueName}" UNIQUE (${columnNames})`;
if (unique.deferrable)
constraint += ` DEFERRABLE ${unique.deferrable}`;
return constraint;
}).join(", ");

sql += `, ${uniquesSql}`;
Expand Down Expand Up @@ -2301,7 +2305,10 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner
*/
protected createUniqueConstraintSql(table: Table, uniqueConstraint: TableUnique): Query {
const columnNames = uniqueConstraint.columnNames.map(column => `"` + column + `"`).join(", ");
return new Query(`ALTER TABLE ${this.escapePath(table)} ADD CONSTRAINT "${uniqueConstraint.name}" UNIQUE (${columnNames})`);
let sql = `ALTER TABLE ${this.escapePath(table)} ADD CONSTRAINT "${uniqueConstraint.name}" UNIQUE (${columnNames})`;
if (uniqueConstraint.deferrable)
sql += ` DEFERRABLE ${uniqueConstraint.deferrable}`;
return new Query(sql);
}

/**
Expand Down
3 changes: 2 additions & 1 deletion src/entity-schema/EntitySchemaTransformer.ts
Expand Up @@ -216,7 +216,8 @@ export class EntitySchemaTransformer {
const uniqueAgrs: UniqueMetadataArgs = {
target: options.target || options.name,
name: unique.name,
columns: unique.columns
columns: unique.columns,
deferrable: unique.deferrable,
};
metadataArgsStorage.uniques.push(uniqueAgrs);
});
Expand Down
6 changes: 6 additions & 0 deletions src/entity-schema/EntitySchemaUniqueOptions.ts
@@ -1,3 +1,5 @@
import { DeferrableType } from "../metadata/types/DeferrableType";

export interface EntitySchemaUniqueOptions {

/**
Expand All @@ -10,4 +12,8 @@ export interface EntitySchemaUniqueOptions {
*/
columns?: ((object?: any) => (any[]|{ [key: string]: number }))|string[];

/**
* Indicate if unique constraints can be deferred.
*/
deferrable?: DeferrableType;
}
7 changes: 7 additions & 0 deletions src/metadata-args/UniqueMetadataArgs.ts
@@ -1,3 +1,5 @@
import { DeferrableType } from "../metadata/types/DeferrableType";

/**
* Arguments for UniqueMetadata class.
*/
Expand All @@ -17,4 +19,9 @@ export interface UniqueMetadataArgs {
* Columns combination to be unique.
*/
columns?: ((object?: any) => (any[]|{ [key: string]: number }))|string[];

/**
* Indicate if unique constraints can be deferred.
*/
deferrable?: DeferrableType;
}
7 changes: 7 additions & 0 deletions src/metadata/UniqueMetadata.ts
Expand Up @@ -4,6 +4,7 @@ import {NamingStrategyInterface} from "../naming-strategy/NamingStrategyInterfac
import {ColumnMetadata} from "./ColumnMetadata";
import {UniqueMetadataArgs} from "../metadata-args/UniqueMetadataArgs";
import { TypeORMError } from "../error";
import { DeferrableType } from "./types/DeferrableType";

/**
* Unique metadata contains all information about table's unique constraints.
Expand Down Expand Up @@ -34,6 +35,11 @@ export class UniqueMetadata {
*/
columns: ColumnMetadata[] = [];

/**
* Indicate if unique constraints can be deferred.
*/
deferrable?: DeferrableType;

/**
* User specified unique constraint name.
*/
Expand Down Expand Up @@ -76,6 +82,7 @@ export class UniqueMetadata {
this.target = options.args.target;
this.givenName = options.args.name;
this.givenColumnNames = options.args.columns;
this.deferrable = options.args.deferrable;
}
}

Expand Down
6 changes: 6 additions & 0 deletions src/schema-builder/options/TableUniqueOptions.ts
Expand Up @@ -17,4 +17,10 @@ export interface TableUniqueOptions {
*/
columnNames: string[];

/**
* Set this foreign key constraint as "DEFERRABLE" e.g. check constraints at start
* or at the end of a transaction
*/
deferrable?: string;

}
13 changes: 11 additions & 2 deletions src/schema-builder/table/TableUnique.ts
Expand Up @@ -20,13 +20,20 @@ export class TableUnique {
*/
columnNames: string[] = [];

/**
* Set this foreign key constraint as "DEFERRABLE" e.g. check constraints at start
* or at the end of a transaction
*/
deferrable?: string;

// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------

constructor(options: TableUniqueOptions) {
this.name = options.name;
this.columnNames = options.columnNames;
this.deferrable = options.deferrable;
}

// -------------------------------------------------------------------------
Expand All @@ -39,7 +46,8 @@ export class TableUnique {
clone(): TableUnique {
return new TableUnique(<TableUniqueOptions>{
name: this.name,
columnNames: [...this.columnNames]
columnNames: [...this.columnNames],
deferrable: this.deferrable,
});
}

Expand All @@ -53,7 +61,8 @@ export class TableUnique {
static create(uniqueMetadata: UniqueMetadata): TableUnique {
return new TableUnique(<TableUniqueOptions>{
name: uniqueMetadata.name,
columnNames: uniqueMetadata.columns.map(column => column.databaseName)
columnNames: uniqueMetadata.columns.map(column => column.databaseName),
deferrable: uniqueMetadata.deferrable,
});
}

Expand Down
100 changes: 100 additions & 0 deletions test/functional/deferrable/deferrable-unique.ts
@@ -0,0 +1,100 @@
import "reflect-metadata";
import {closeTestingConnections, createTestingConnections, reloadTestingDatabases} from "../../utils/test-utils";
import {Connection} from "../../../src/connection/Connection";
import {Company} from "./entity/Company";
import {Office} from "./entity/Office";
import {expect} from "chai";

describe("deferrable uq constraints should be check at the end of transaction", () => {

let connections: Connection[];
before(async () => connections = await createTestingConnections({
entities: [__dirname + "/entity/*{.js,.ts}"],
enabledDrivers: ["postgres"]
}));
beforeEach(() => reloadTestingDatabases(connections));
after(() => closeTestingConnections(connections));

it("use initially deferred deferrable uq constraints", () => Promise.all(connections.map(async connection => {

await connection.manager.transaction(async entityManager => {
// first save company
const company1 = new Company();
company1.id = 100;
company1.name = "Acme";

await entityManager.save(company1);

// then save company with uq violation
const company2 = new Company();
company2.id = 101;
company2.name = "Acme";

await entityManager.save(company2);

// then update company 1 to fix uq violation
company1.name = "Foobar";

await entityManager.save(company1);
});

// now check
const companies = await connection.manager.find(Company, {
order: { id: "ASC" },
});

expect(companies).to.have.length(2);

companies[0].should.be.eql({
id: 100,
name: "Foobar",
});
companies[1].should.be.eql({
id: 101,
name: "Acme",
});
})));

it("use initially immediated deferrable uq constraints", () => Promise.all(connections.map(async connection => {

await connection.manager.transaction(async entityManager => {
// first set constraints deferred manually
await entityManager.query("SET CONSTRAINTS ALL DEFERRED");

// first save office
const office1 = new Office();
office1.id = 200;
office1.name = "Boston";

await entityManager.save(office1);

// then save office with uq violation
const office2 = new Office();
office2.id = 201;
office2.name = "Boston";

await entityManager.save(office2);

// then update office 1 to fix uq violation
office1.name = "Cambridge";

await entityManager.save(office1);
});

// now check
const offices = await connection.manager.find(Office, {
order: { id: "ASC" },
});

expect(offices).to.have.length(2);

offices[0].should.be.eql({
id: 200,
name: "Cambridge",
});
offices[1].should.be.eql({
id: 201,
name: "Boston",
});
})));
});
2 changes: 2 additions & 0 deletions test/functional/deferrable/entity/Company.ts
@@ -1,8 +1,10 @@
import {Entity} from "../../../../src/decorator/entity/Entity";
import {Column} from "../../../../src/decorator/columns/Column";
import {PrimaryColumn} from "../../../../src/decorator/columns/PrimaryColumn";
import {Unique} from "../../../../src/decorator/Unique";

@Entity()
@Unique(["name"], {deferrable: "INITIALLY DEFERRED"})
export class Company {

@PrimaryColumn()
Expand Down
2 changes: 2 additions & 0 deletions test/functional/deferrable/entity/Office.ts
Expand Up @@ -2,9 +2,11 @@ import {Entity} from "../../../../src/decorator/entity/Entity";
import {Column} from "../../../../src/decorator/columns/Column";
import {ManyToOne} from "../../../../src/decorator/relations/ManyToOne";
import {PrimaryColumn} from "../../../../src/decorator/columns/PrimaryColumn";
import {Unique} from "../../../../src/decorator/Unique";
import {Company} from "./Company";

@Entity()
@Unique(["name"], {deferrable: "INITIALLY IMMEDIATE"})
export class Office {

@PrimaryColumn()
Expand Down

0 comments on commit e52b26c

Please sign in to comment.