Skip to content

Commit

Permalink
feat: Postgres IDENTITY Column support (typeorm#7741)
Browse files Browse the repository at this point in the history
* feat: extend column options interfaces to also support Postgres10+ Identity generationStrategy

* feat: extend PrimaryGeneratedColumn support for Postgres10+ identity

* feat: update buildCreateColumnSql with Postgres10+ IDENTITY support

* feat: update loadTables for Postgres10+ IDENTITY column recognition

* doc: add description of identity support for PrimaryGeneratedColumn

* test: add test for SQL of table with IDENTITY COLUMN in Postgres 10+

* feat: take identity generation type into consideration when checking for changed column

* fix: better non behavior changing way of checking for identity column

* chore: rebase and resolve PR conversations
  • Loading branch information
mitsos1os authored and HeartPattern committed Nov 29, 2021
1 parent ea8fc49 commit 96b3475
Show file tree
Hide file tree
Showing 10 changed files with 66 additions and 22 deletions.
3 changes: 2 additions & 1 deletion docs/decorator-reference.md
Expand Up @@ -237,9 +237,10 @@ export class User {
}
```
There are two generation strategies:
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`
* `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
8 changes: 4 additions & 4 deletions docs/entities.md
Expand Up @@ -125,7 +125,7 @@ export class User {
}
```

* `@PrimaryGeneratedColumn()` creates a primary column which value will be automatically generated with an auto-increment value. It will create `int` column with `auto-increment`/`serial`/`sequence` (depend on the database). You don't have to manually assign its value before save - value will be automatically generated.
* `@PrimaryGeneratedColumn()` creates a primary column which value will be automatically generated with an auto-increment value. It will create `int` column with `auto-increment`/`serial`/`sequence`/`identity` (depend on the database and configuration provided). You don't have to manually assign its value before save - value will be automatically generated.

```typescript
import {Entity, PrimaryGeneratedColumn} from "typeorm";
Expand Down Expand Up @@ -196,13 +196,13 @@ There are several special column types with additional functionality available:
* `@CreateDateColumn` is a special column that is automatically set to the entity's insertion date.
You don't need to set this column - it will be automatically set.

* `@UpdateDateColumn` is a special column that is automatically set to the entity's update time
* `@UpdateDateColumn` is a special column that is automatically set to the entity's update time
each time you call `save` of entity manager or repository.
You don't need to set this column - it will be automatically set.

* `@DeleteDateColumn` is a special column that is automatically set to the entity's delete time each time you call soft-delete of entity manager or repository. You don't need to set this column - it will be automatically set. If the @DeleteDateColumn is set, the default scope will be "non-deleted".

* `@VersionColumn` is a special column that is automatically set to the version of the entity (incremental number)
* `@VersionColumn` is a special column that is automatically set to the version of the entity (incremental number)
each time you call `save` of entity manager or repository.
You don't need to set this column - it will be automatically set.

Expand Down Expand Up @@ -524,7 +524,7 @@ export class User {

`uuid` value will be automatically generated and stored into the database.

Besides "uuid" there is also "increment" and "rowid" (CockroachDB only) generated types, however there are some limitations
Besides "uuid" there is also "increment", "identity" (Postgres 10+ only) and "rowid" (CockroachDB only) generated types, however there are some limitations
on some database platforms with this type of generation (for example some databases can only have one increment column,
or some of them require increment to be a primary key).

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 6 additions & 4 deletions src/decorator/columns/PrimaryGeneratedColumn.ts
Expand Up @@ -29,20 +29,22 @@ export function PrimaryGeneratedColumn(strategy: "uuid", options?: PrimaryGenera
*/
export function PrimaryGeneratedColumn(strategy: "rowid", options?: PrimaryGeneratedColumnUUIDOptions): PropertyDecorator;

export function PrimaryGeneratedColumn(strategy: "identity", options?: PrimaryGeneratedColumnUUIDOptions): 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"|PrimaryGeneratedColumnNumericOptions|PrimaryGeneratedColumnUUIDOptions,
export function PrimaryGeneratedColumn(strategyOrOptions?: "increment"|"uuid"|"rowid"|"identity"|PrimaryGeneratedColumnNumericOptions|PrimaryGeneratedColumnUUIDOptions,
maybeOptions?: PrimaryGeneratedColumnNumericOptions|PrimaryGeneratedColumnUUIDOptions): PropertyDecorator {

// normalize parameters
const options: ColumnOptions = {};
let strategy: "increment"|"uuid"|"rowid";
let strategy: "increment"|"uuid"|"rowid"|"identity";
if (strategyOrOptions) {
if (typeof strategyOrOptions === "string")
strategy = strategyOrOptions as "increment"|"uuid"|"rowid";
strategy = strategyOrOptions as "increment"|"uuid"|"rowid"|"identity";

if (strategyOrOptions instanceof Object) {
strategy = "increment";
Expand All @@ -58,7 +60,7 @@ 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") {
if (strategy === "increment" || strategy === "identity") {
options.type = Number;
} else if (strategy === "uuid") {
options.type = "uuid";
Expand Down
2 changes: 1 addition & 1 deletion src/decorator/options/ColumnCommonOptions.ts
Expand Up @@ -26,7 +26,7 @@ export interface ColumnCommonOptions {
* Specifies if this column will use auto increment (sequence, generated identity, rowid).
* Note that in some databases only one column in entity can be marked as generated, and it must be a primary column.
*/
generated?: boolean|"increment"|"uuid"|"rowid";
generated?: boolean|"increment"|"uuid"|"rowid"|"identity";

/**
* Specifies if column's value must be unique or not.
Expand Down
26 changes: 18 additions & 8 deletions src/driver/postgres/PostgresQueryRunner.ts
Expand Up @@ -957,7 +957,10 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner
}
}

if (oldColumn.isGenerated !== newColumn.isGenerated && newColumn.generationStrategy !== "uuid") {
if (oldColumn.isGenerated !== newColumn.isGenerated &&
newColumn.generationStrategy !== "uuid" &&
newColumn.generationStrategy !== "identity"
) {
if (newColumn.isGenerated === true) {
upQueries.push(new Query(`CREATE SEQUENCE IF NOT EXISTS ${this.escapePath(this.buildSequencePath(table, newColumn))} OWNED BY ${this.escapePath(table)}."${newColumn.name}"`));
downQueries.push(new Query(`DROP SEQUENCE ${this.escapePath(this.buildSequencePath(table, newColumn))}`));
Expand Down Expand Up @@ -1779,7 +1782,10 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner
: false;
tableColumn.isUnique = !!uniqueConstraint && !isConstraintComposite;

if (dbColumn["column_default"] !== null && dbColumn["column_default"] !== undefined) {
if (dbColumn.is_identity === "YES") { // Postgres 10+ Identity column
tableColumn.isGenerated = true;
tableColumn.generationStrategy = "identity";
} 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 @@ -2327,12 +2333,16 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner
protected buildCreateColumnSql(table: Table, column: TableColumn) {
let c = "\"" + column.name + "\"";
if (column.isGenerated === true && column.generationStrategy !== "uuid") {
if (column.type === "integer" || column.type === "int" || column.type === "int4")
c += " SERIAL";
if (column.type === "smallint" || column.type === "int2")
c += " SMALLSERIAL";
if (column.type === "bigint" || column.type === "int8")
c += " BIGSERIAL";
if (column.generationStrategy === "identity") { // Postgres 10+ Identity generated column
c += ` ${column.type} GENERATED BY DEFAULT AS IDENTITY`;
} else { // classic SERIAL primary column
if (column.type === "integer" || column.type === "int" || column.type === "int4")
c += " SERIAL";
if (column.type === "smallint" || column.type === "int2")
c += " SMALLSERIAL";
if (column.type === "bigint" || column.type === "int8")
c += " BIGSERIAL";
}
}
if (column.type === "enum" || column.type === "simple-enum") {
c += " " + this.buildEnumName(table, column);
Expand Down
2 changes: 1 addition & 1 deletion src/schema-builder/options/TableColumnOptions.ts
Expand Up @@ -41,7 +41,7 @@ export interface TableColumnOptions {
/**
* Specifies generation strategy if this column will use auto increment.
*/
generationStrategy?: "uuid"|"increment"|"rowid";
generationStrategy?: "uuid"|"increment"|"rowid"|"identity";

/**
* Indicates if column is a primary key.
Expand Down
2 changes: 1 addition & 1 deletion src/schema-builder/table/TableColumn.ts
Expand Up @@ -43,7 +43,7 @@ export class TableColumn {
* Specifies generation strategy if this column will use auto increment.
* `rowid` option supported only in CockroachDB.
*/
generationStrategy?: "uuid"|"increment"|"rowid";
generationStrategy?: "uuid"|"increment"|"rowid"|"identity";

/**
* Indicates if column is a primary key.
Expand Down
7 changes: 7 additions & 0 deletions test/github-issues/5365/entity/UserEntity.ts
@@ -0,0 +1,7 @@
import { Entity, PrimaryGeneratedColumn } from "../../../../src";

@Entity()
export class User {
@PrimaryGeneratedColumn("identity")
id!: number;
}
24 changes: 24 additions & 0 deletions test/github-issues/5365/issue-5365.ts
@@ -0,0 +1,24 @@
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 > #5365 Generated Identity for Postgres 10+", () => {

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 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" ("id" integer GENERATED BY DEFAULT AS IDENTITY NOT NULL, CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`);
})));
});

0 comments on commit 96b3475

Please sign in to comment.