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

Added Deferrable option for Unqiue constraints (Postgres) #8356

Merged
merged 1 commit into from
Dec 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions src/decorator/Unique.ts
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -1826,7 +1826,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 @@ -1941,7 +1942,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 @@ -2168,7 +2172,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
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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