Skip to content

Commit

Permalink
feat: add support for Postgres 10+ GENERATED ALWAYS AS IDENTITY (#8371)
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: #8370
  • Loading branch information
leoromanovsky authored Nov 23, 2021
1 parent f3e2b0e commit a0f09de
Show file tree
Hide file tree
Showing 11 changed files with 152 additions and 5 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
7 changes: 4 additions & 3 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 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";
}
4 changes: 3 additions & 1 deletion src/driver/postgres/PostgresQueryRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1912,6 +1912,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 @@ -2481,7 +2482,8 @@ 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`;
const generatedIdentityOrDefault = column.generatedIdentity || "BY DEFAULT";
c += ` ${column.type} GENERATED ${generatedIdentityOrDefault} AS IDENTITY`;
} else { // classic SERIAL primary column
if (column.type === "integer" || column.type === "int" || column.type === "int4")
c += " SERIAL";
Expand Down
7 changes: 7 additions & 0 deletions src/metadata/ColumnMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ export class ColumnMetadata {
*/
generationStrategy?: "uuid"|"increment"|"rowid";

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

/**
* Column comment.
* This feature is not supported by all databases.
Expand Down Expand Up @@ -360,6 +365,8 @@ export class ColumnMetadata {
this.default = options.args.options.default;
if (options.args.options.onUpdate)
this.onUpdate = options.args.options.onUpdate;
if (options.args.options.generatedIdentity)
this.generatedIdentity = options.args.options.generatedIdentity;
if (options.args.options.scale !== null && options.args.options.scale !== undefined)
this.scale = options.args.options.scale;
if (options.args.options.zerofill) {
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
28 changes: 28 additions & 0 deletions test/github-issues/8370/entity/UserEntity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Column, Entity, PrimaryGeneratedColumn } from "../../../../src";

@Entity()
export class User {
// test PrimaryGeneratedColumn
@PrimaryGeneratedColumn("identity", { generatedIdentity: "ALWAYS" })
id: number;

// test explicit `ALWAYS`
@Column({
type: "bigint",
generated: "identity",
generatedIdentity: "ALWAYS",
})
secondId: number;

// test explicit `BY DEFAULT`
@Column({
type: "int",
generated: "identity",
generatedIdentity: "BY DEFAULT",
})
thirdId: number;

// test default `generatedIdentity`
@Column({ type: "int", generated: "identity" })
fourthId: number;
}
64 changes: 64 additions & 0 deletions test/github-issues/8370/issue-8370.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import "reflect-metadata";
import {
createTestingConnections,
closeTestingConnections,
} from "../../utils/test-utils";
import { Connection } from "../../../src";
import { User } from "./entity/UserEntity";

import { expect } from "chai";

describe("github issues > #8370 Add support for Postgres GENERATED ALWAYS AS IDENTITY", () => {
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 column 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);

// primary key
expect(sqlInMemory.upQueries[0])
.to.have.property("query")
.that.contains(
`"id" integer GENERATED ALWAYS AS IDENTITY NOT NULL`
);

// second id
expect(sqlInMemory.upQueries[0])
.to.have.property("query")
.that.contains(
`"secondId" bigint GENERATED ALWAYS AS IDENTITY NOT NULL`
);

// third id
expect(sqlInMemory.upQueries[0])
.to.have.property("query")
.that.contains(
`"thirdId" integer GENERATED BY DEFAULT AS IDENTITY NOT NULL`
);

// fourth id
expect(sqlInMemory.upQueries[0])
.to.have.property("query")
.that.contains(
`"fourthId" integer GENERATED BY DEFAULT AS IDENTITY NOT NULL`
);
})
));
});

0 comments on commit a0f09de

Please sign in to comment.