Skip to content

Commit

Permalink
feat: add support for Postgres 10+ GENERATED ALWAYS AS IDENTITY
Browse files Browse the repository at this point in the history
allow developers to create a Column of identity
and choose between `ALWAYS` and `BY DEFAULT`.

Closes: typeorm#8370
  • Loading branch information
leoromanovsky committed Nov 22, 2021
1 parent 0334d10 commit fb4af30
Show file tree
Hide file tree
Showing 15 changed files with 196 additions and 9 deletions.
2 changes: 1 addition & 1 deletion docs/decorator-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ export class User {
There are four generation strategies:
* `increment` - uses AUTO_INCREMENT / SERIAL / SEQUENCE (depend on database type) to generate incremental number.
* `identity` - only for [PostgreSQL 10+](https://www.postgresql.org/docs/13/sql-createtable.html). Postgres versions above 10 support the SQL-Compliant **IDENTITY** column. When marking the generation strategy as `identity` the column will be produced using `GENERATED BY DEFAULT AS IDENTITY`
* `identity` - only for [PostgreSQL 10+](https://www.postgresql.org/docs/13/sql-createtable.html). Postgres versions above 10 support the SQL-Compliant **IDENTITY** column. When marking the generation strategy as `identity` the column will be produced using `GENERATED [ALWAYS|BY DEFAULT] AS IDENTITY`
* `uuid` - generates unique `uuid` string.
* `rowid` - only for [CockroachDB](https://www.cockroachlabs.com/docs/stable/serial.html). Value is automatically generated using the `unique_rowid()`
function. This produces a 64-bit integer from the current timestamp and ID of the node executing the `INSERT` or `UPSERT` operation.
Expand Down
11 changes: 7 additions & 4 deletions src/decorator/columns/PrimaryGeneratedColumn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {PrimaryGeneratedColumnNumericOptions} from "../options/PrimaryGeneratedC
import {PrimaryGeneratedColumnUUIDOptions} from "../options/PrimaryGeneratedColumnUUIDOptions";
import {GeneratedMetadataArgs} from "../../metadata-args/GeneratedMetadataArgs";
import { ColumnOptions } from "../options/ColumnOptions";
import { PrimaryGeneratedColumnIdentityOptions } from "../options/PrimaryGeneratedColumnIdentityOptions";

/**
* Column decorator is used to mark a specific class property as a table column.
Expand All @@ -29,15 +30,15 @@ export function PrimaryGeneratedColumn(strategy: "uuid", options?: PrimaryGenera
*/
export function PrimaryGeneratedColumn(strategy: "rowid", options?: PrimaryGeneratedColumnUUIDOptions): PropertyDecorator;

export function PrimaryGeneratedColumn(strategy: "identity", options?: PrimaryGeneratedColumnUUIDOptions): PropertyDecorator;
export function PrimaryGeneratedColumn(strategy: "identity", options?: PrimaryGeneratedColumnIdentityOptions): PropertyDecorator;

/**
* Column decorator is used to mark a specific class property as a table column.
* Only properties decorated with this decorator will be persisted to the database when entity be saved.
* This column creates an integer PRIMARY COLUMN with generated set to true.
*/
export function PrimaryGeneratedColumn(strategyOrOptions?: "increment"|"uuid"|"rowid"|"identity"|PrimaryGeneratedColumnNumericOptions|PrimaryGeneratedColumnUUIDOptions,
maybeOptions?: PrimaryGeneratedColumnNumericOptions|PrimaryGeneratedColumnUUIDOptions): PropertyDecorator {
export function PrimaryGeneratedColumn(strategyOrOptions?: "increment"|"uuid"|"rowid"|"identity"|PrimaryGeneratedColumnNumericOptions|PrimaryGeneratedColumnUUIDOptions|PrimaryGeneratedColumnIdentityOptions,
maybeOptions?: PrimaryGeneratedColumnNumericOptions|PrimaryGeneratedColumnUUIDOptions|PrimaryGeneratedColumnIdentityOptions): PropertyDecorator {

// normalize parameters
const options: ColumnOptions = {};
Expand All @@ -60,7 +61,9 @@ export function PrimaryGeneratedColumn(strategyOrOptions?: "increment"|"uuid"|"r

// if column type is not explicitly set then determine it based on generation strategy
if (!options.type) {
if (strategy === "increment" || strategy === "identity") {
if (strategy === "increment") {
options.type = Number;
} else if (strategy === "identity") {
options.type = Number;
} else if (strategy === "uuid") {
options.type = "uuid";
Expand Down
5 changes: 5 additions & 0 deletions src/decorator/options/ColumnOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,11 @@ export interface ColumnOptions extends ColumnCommonOptions {
*/
generatedType?: "VIRTUAL"|"STORED";

/**
* Identity column type. Supports only in Postgres 10+.
*/
generatedIdentity?: "ALWAYS"|"BY DEFAULT";

/**
* Return type of HSTORE column.
* Returns value as string or as object.
Expand Down
27 changes: 27 additions & 0 deletions src/decorator/options/PrimaryGeneratedColumnIdentityOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {PrimaryGeneratedColumnType} from "../../driver/types/ColumnTypes";

/**
* Describes all options for PrimaryGeneratedColumn decorator with identity generation strategy.
*/
export interface PrimaryGeneratedColumnIdentityOptions {

/**
* Column type. Must be one of the value from the ColumnTypes class.
*/
type?: PrimaryGeneratedColumnType;

/**
* Column name in the database.
*/
name?: string;

/**
* Column comment. Not supported by all database types.
*/
comment?: string;

/**
* Identity column type. Supports only in Postgres 10+.
*/
generatedIdentity?: "ALWAYS"|"BY DEFAULT";
}
2 changes: 1 addition & 1 deletion src/driver/cockroachdb/CockroachDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,7 @@ export class CockroachDriver implements Driver {
/**
* Creates a database type from a given column metadata.
*/
normalizeType(column: { type?: ColumnType, length?: number | string, precision?: number|null, scale?: number, isArray?: boolean, isGenerated?: boolean, generationStrategy?: "increment"|"uuid"|"rowid" }): string {
normalizeType(column: { type?: ColumnType, length?: number | string, precision?: number|null, scale?: number, isArray?: boolean, isGenerated?: boolean, generationStrategy?: "increment"|"uuid"|"rowid"|"identity" }): string {
if (column.type === Number || column.type === "integer" || column.type === "int" || column.type === "bigint" || column.type === "int64") {
return "int8";

Expand Down
3 changes: 2 additions & 1 deletion src/driver/postgres/PostgresQueryRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1785,6 +1785,7 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner
if (dbColumn.is_identity === "YES") { // Postgres 10+ Identity column
tableColumn.isGenerated = true;
tableColumn.generationStrategy = "identity";
tableColumn.generatedIdentity = dbColumn.identity_generation;
} else if (dbColumn["column_default"] !== null && dbColumn["column_default"] !== undefined) {
const serialDefaultName = `nextval('${this.buildSequenceName(table, dbColumn["column_name"])}'::regclass)`;
const serialDefaultPath = `nextval('${this.buildSequencePath(table, dbColumn["column_name"])}'::regclass)`;
Expand Down Expand Up @@ -2334,7 +2335,7 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner
let c = "\"" + column.name + "\"";
if (column.isGenerated === true && column.generationStrategy !== "uuid") {
if (column.generationStrategy === "identity") { // Postgres 10+ Identity generated column
c += ` ${column.type} GENERATED BY DEFAULT AS IDENTITY`;
c += ` ${column.type} GENERATED ${column.generatedIdentity || "BY DEFAULT"} AS IDENTITY`;
} else { // classic SERIAL primary column
if (column.type === "integer" || column.type === "int" || column.type === "int4")
c += " SERIAL";
Expand Down
2 changes: 1 addition & 1 deletion src/metadata-args/GeneratedMetadataArgs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ export interface GeneratedMetadataArgs {
/**
* Generation strategy.
*/
readonly strategy: "uuid"|"increment"|"rowid";
readonly strategy: "uuid"|"increment"|"rowid"|"identity";

}
9 changes: 8 additions & 1 deletion src/metadata/ColumnMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,12 @@ export class ColumnMetadata {
/**
* Specifies generation strategy if this column will use auto increment.
*/
generationStrategy?: "uuid"|"increment"|"rowid";
generationStrategy?: "uuid"|"increment"|"rowid"|"identity";

/**
* Identity column type. Supports only in Postgres 10+.
*/
generatedIdentity?: "ALWAYS"|"BY DEFAULT";

/**
* Column comment.
Expand Down Expand Up @@ -352,6 +357,8 @@ export class ColumnMetadata {
this.isInsert = options.args.options.insert;
if (options.args.options.update !== undefined)
this.isUpdate = options.args.options.update;
if (options.args.options.generatedIdentity !== undefined)
this.generatedIdentity = options.args.options.generatedIdentity;
if (options.args.options.readonly !== undefined)
this.isUpdate = !options.args.options.readonly;
if (options.args.options.comment)
Expand Down
5 changes: 5 additions & 0 deletions src/schema-builder/options/TableColumnOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ export interface TableColumnOptions {
*/
generatedType?: "VIRTUAL"|"STORED";

/**
* Identity column type. Supports only in Postgres 10+.
*/
generatedIdentity?: "ALWAYS"|"BY DEFAULT";

/**
* Spatial Feature Type (Geometry, Point, Polygon, etc.)
*/
Expand Down
7 changes: 7 additions & 0 deletions src/schema-builder/table/TableColumn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@ export class TableColumn {
*/
generatedType?: "VIRTUAL"|"STORED";

/**
* Identity column type. Supports only in Postgres 10+.
*/
generatedIdentity?: "ALWAYS"|"BY DEFAULT";

/**
* Spatial Feature Type (Geometry, Point, Polygon, etc.)
*/
Expand Down Expand Up @@ -161,6 +166,7 @@ export class TableColumn {
this.isNullable = options.isNullable || false;
this.isGenerated = options.isGenerated || false;
this.generationStrategy = options.generationStrategy;
this.generatedIdentity = options.generatedIdentity;
this.isPrimary = options.isPrimary || false;
this.isUnique = options.isUnique || false;
this.isArray = options.isArray || false;
Expand Down Expand Up @@ -202,6 +208,7 @@ export class TableColumn {
isNullable: this.isNullable,
isGenerated: this.isGenerated,
generationStrategy: this.generationStrategy,
generatedIdentity: this.generatedIdentity,
isPrimary: this.isPrimary,
isUnique: this.isUnique,
isArray: this.isArray,
Expand Down
1 change: 1 addition & 0 deletions src/schema-builder/util/TableUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export class TableUtils {
comment: columnMetadata.comment,
isGenerated: columnMetadata.isGenerated,
generationStrategy: columnMetadata.generationStrategy,
generatedIdentity: columnMetadata.generatedIdentity,
isNullable: columnMetadata.isNullable,
type: driver.normalizeType(columnMetadata),
isPrimary: columnMetadata.isPrimary,
Expand Down
7 changes: 7 additions & 0 deletions test/github-issues/8370/entity/UserAlways.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Entity, PrimaryGeneratedColumn } from "../../../../src";

@Entity()
export class UserAlways {
@PrimaryGeneratedColumn("identity", { generatedIdentity: "ALWAYS" })
id!: number;
}
7 changes: 7 additions & 0 deletions test/github-issues/8370/entity/UserDefault.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Entity, PrimaryGeneratedColumn } from "../../../../src";

@Entity()
export class UserDefault {
@PrimaryGeneratedColumn("identity", { generatedIdentity: "BY DEFAULT" })
id!: number;
}
7 changes: 7 additions & 0 deletions test/github-issues/8370/entity/UserEntity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Entity, PrimaryGeneratedColumn } from "../../../../src";

@Entity()
export class User {
@PrimaryGeneratedColumn("identity")
id!: number;
}
110 changes: 110 additions & 0 deletions test/github-issues/8370/issue-8370.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import "reflect-metadata";
import {
createTestingConnections,
closeTestingConnections,
} from "../../utils/test-utils";
import { Connection } from "../../../src";
import { User } from "./entity/UserEntity";

import { expect } from "chai";
import { UserAlways } from "./entity/UserAlways";
import { UserDefault } from "./entity/UserDefault";

describe("github issues > #8370 Add support for Postgres GENERATED ALWAYS AS IDENTITY", () => {

describe("User entity", () => {
let connections: Connection[];
before(
async () =>
(connections = await createTestingConnections({
entities: [User],
schemaCreate: false,
dropSchema: true,
enabledDrivers: ["postgres"],
}))
);
after(() => closeTestingConnections(connections));

it("should produce proper SQL for creating a table with `BY DEFAULT` identity column for default options", () =>
Promise.all(
connections.map(async (connection) => {
const sqlInMemory = await connection.driver
.createSchemaBuilder()
.log();
expect(sqlInMemory)
.to.have.property("upQueries")
.that.is.an("array")
.and.has.length(1);
expect(sqlInMemory.upQueries[0])
.to.have.property("query")
.that.eql(
`CREATE TABLE "user" ("id" integer GENERATED BY DEFAULT AS IDENTITY NOT NULL, CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`
);
})
));
});

describe("UserAlways entity", () => {
let connections: Connection[];
before(
async () =>
(connections = await createTestingConnections({
entities: [UserAlways],
schemaCreate: false,
dropSchema: true,
enabledDrivers: ["postgres"],
}))
);
after(() => closeTestingConnections(connections));

it("should produce proper SQL for creating a table with `ALWAYS` identity column", () =>
Promise.all(
connections.map(async (connection) => {
const sqlInMemory = await connection.driver
.createSchemaBuilder()
.log();
expect(sqlInMemory)
.to.have.property("upQueries")
.that.is.an("array")
.and.has.length(1);
expect(sqlInMemory.upQueries[0])
.to.have.property("query")
.that.eql(
`CREATE TABLE "user_always" ("id" integer GENERATED ALWAYS AS IDENTITY NOT NULL, CONSTRAINT "PK_08a31cb18e870610e560b6c0230" PRIMARY KEY ("id"))`
);
})
));
});

describe("UserDefault entity", () => {
let connections: Connection[];
before(
async () =>
(connections = await createTestingConnections({
entities: [UserDefault],
schemaCreate: false,
dropSchema: true,
enabledDrivers: ["postgres"],
}))
);
after(() => closeTestingConnections(connections));

it("should produce proper SQL for creating a table with `BY DEFAULT` identity column", () =>
Promise.all(
connections.map(async (connection) => {
const sqlInMemory = await connection.driver
.createSchemaBuilder()
.log();
expect(sqlInMemory)
.to.have.property("upQueries")
.that.is.an("array")
.and.has.length(1);
expect(sqlInMemory.upQueries[0])
.to.have.property("query")
.that.eql(
`CREATE TABLE "user_default" ("id" integer GENERATED BY DEFAULT AS IDENTITY NOT NULL, CONSTRAINT "PK_25101c5a870759239a1a9ef429c" PRIMARY KEY ("id"))`
);
})
));
});
});

0 comments on commit fb4af30

Please sign in to comment.