Skip to content
Closed
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
149 changes: 103 additions & 46 deletions src/driver/postgres/PostgresQueryRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -727,57 +727,84 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner
downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${newColumn.name}" TYPE ${this.driver.createFullType(oldColumn)}`));
}

if (oldColumn.isNullable !== newColumn.isNullable) {
if (newColumn.isNullable) {
upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${oldColumn.name}" DROP NOT NULL`));
downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${oldColumn.name}" SET NOT NULL`));
} else {
upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${oldColumn.name}" SET NOT NULL`));
downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${oldColumn.name}" DROP NOT NULL`));
}
}

if (
(newColumn.type === "enum" || newColumn.type === "simple-enum")
&& (oldColumn.type === "enum" || oldColumn.type === "simple-enum")
&& !OrmUtils.isArraysEqual(newColumn.enum!, oldColumn.enum!)
) {
const enumName = this.buildEnumName(table, newColumn);
const arraySuffix = newColumn.isArray ? "[]" : "";
const oldEnumName = this.buildEnumName(table, newColumn, true, false, true);
const oldEnumNameWithoutSchema = this.buildEnumName(table, newColumn, false, false, true);
const enumTypeBeforeColumnChange = await this.getEnumTypeName(table, oldColumn);

// rename old ENUM
upQueries.push(new Query(`ALTER TYPE "${enumTypeBeforeColumnChange.enumTypeSchema}"."${enumTypeBeforeColumnChange.enumTypeName}" RENAME TO ${oldEnumNameWithoutSchema}`));
downQueries.push(new Query(`ALTER TYPE ${oldEnumName} RENAME TO "${enumTypeBeforeColumnChange.enumTypeName}"`));

// create new ENUM
upQueries.push(this.createEnumTypeSql(table, newColumn));
downQueries.push(this.dropEnumTypeSql(table, oldColumn));

// if column have default value, we must drop it to avoid issues with type casting
if (newColumn.default !== null && newColumn.default !== undefined) {
upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${newColumn.name}" DROP DEFAULT`));
downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${newColumn.name}" SET DEFAULT ${newColumn.default}`));
}

// build column types
const upType = `${enumName}${arraySuffix} USING "${newColumn.name}"::"text"::${enumName}${arraySuffix}`;
const downType = `${oldEnumName}${arraySuffix} USING "${newColumn.name}"::"text"::${oldEnumName}${arraySuffix}`;
if (oldColumn.isArray && !newColumn.isArray) {
// enum[] -> enum
if (newColumn.isNullable) {
upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${oldColumn.name}" TYPE ${this.buildEnumName(table, newColumn, true, false, false)} USING "${oldColumn.name}"[1]::${this.buildEnumName(table, newColumn, true, false, false)}`));
if (oldColumn.isNullable) {
downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${oldColumn.name}" TYPE ${this.buildEnumName(table, oldColumn, true, false, false)}[] USING ARRAY["${oldColumn.name}"]`))
} else {
const { downQueries } = await this.getDropColumnQueries(tableOrName, newColumn);
downQueries.push(...downQueries);
}
} else {
// No migration strategic. There could be values with empty arrays. Therefore the new column needs to be nullable.
await this.dropColumn(tableOrName, oldColumn);
await this.addColumn(tableOrName, newColumn);
}
} else if (!oldColumn.isArray && newColumn.isArray) {
// enum -> enum[]
upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${oldColumn.name}" TYPE ${this.buildEnumName(table, newColumn, true, false, false)}[] USING ARRAY["${oldColumn.name}"]`));
if (oldColumn.isNullable || newColumn.isNullable) {
const { downQueries } = await this.getDropColumnQueries(tableOrName, oldColumn);
downQueries.push(...downQueries);
} else {
downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${oldColumn.name}" TYPE ${this.buildEnumName(table, newColumn, true, false, false)} USING "${oldColumn.name}"[1]`));
}
} else {
const enumName = this.buildEnumName(table, newColumn);
const arraySuffix = newColumn.isArray ? "[]" : "";
const oldEnumName = this.buildEnumName(table, newColumn, true, false, true);
const oldEnumNameWithoutSchema = this.buildEnumName(table, newColumn, false, false, true);
const enumTypeBeforeColumnChange = await this.getEnumTypeName(table, oldColumn);

// rename old ENUM
upQueries.push(new Query(`ALTER TYPE "${enumTypeBeforeColumnChange.enumTypeSchema}"."${enumTypeBeforeColumnChange.enumTypeName}" RENAME TO ${oldEnumNameWithoutSchema}`));
downQueries.push(new Query(`ALTER TYPE ${oldEnumName} RENAME TO "${enumTypeBeforeColumnChange.enumTypeName}"`));

// create new ENUM
upQueries.push(this.createEnumTypeSql(table, newColumn));
downQueries.push(this.dropEnumTypeSql(table, oldColumn));

// if column have default value, we must drop it to avoid issues with type casting
if (newColumn.default !== null && newColumn.default !== undefined) {
upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${newColumn.name}" DROP DEFAULT`));
downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${newColumn.name}" SET DEFAULT ${newColumn.default}`));
}

// update column to use new type
upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${newColumn.name}" TYPE ${upType}`));
downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${newColumn.name}" TYPE ${downType}`));
// build column types
const upType = `${enumName}${arraySuffix} USING "${newColumn.name}"::"text"::${enumName}${arraySuffix}`;
const downType = `${oldEnumName}${arraySuffix} USING "${newColumn.name}"::"text"::${oldEnumName}${arraySuffix}`;

// if column have default value and we dropped it before, we must bring it back
if (newColumn.default !== null && newColumn.default !== undefined) {
upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${newColumn.name}" SET DEFAULT ${newColumn.default}`));
downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${newColumn.name}" DROP DEFAULT`));
}
// update column to use new type
upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${newColumn.name}" TYPE ${upType}`));
downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${newColumn.name}" TYPE ${downType}`));

// remove old ENUM
upQueries.push(this.dropEnumTypeSql(table, newColumn, oldEnumName));
downQueries.push(this.createEnumTypeSql(table, oldColumn, oldEnumName));
}
// if column have default value and we dropped it before, we must bring it back
if (newColumn.default !== null && newColumn.default !== undefined) {
upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${newColumn.name}" SET DEFAULT ${newColumn.default}`));
downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${newColumn.name}" DROP DEFAULT`));
}

if (oldColumn.isNullable !== newColumn.isNullable) {
if (newColumn.isNullable) {
upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${oldColumn.name}" DROP NOT NULL`));
downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${oldColumn.name}" SET NOT NULL`));
} else {
upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${oldColumn.name}" SET NOT NULL`));
downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${oldColumn.name}" DROP NOT NULL`));
// remove old ENUM
upQueries.push(this.dropEnumTypeSql(table, newColumn, oldEnumName));
downQueries.push(this.createEnumTypeSql(table, oldColumn, oldEnumName));
}
}

Expand Down Expand Up @@ -898,10 +925,7 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner
}
}

/**
* Drops column in the table.
*/
async dropColumn(tableOrName: Table|string, columnOrName: TableColumn|string): Promise<void> {
async getDropColumnQueries(tableOrName: Table|string, columnOrName: TableColumn|string): Promise<{ upQueries: Query[], downQueries: Query[], clonedTable: Table, table: Table, column: TableColumn }> {
const table = tableOrName instanceof Table ? tableOrName : await this.getCachedTable(tableOrName);
const column = columnOrName instanceof TableColumn ? columnOrName : table.findColumnByName(columnOrName);
if (!column)
Expand Down Expand Up @@ -969,6 +993,21 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner
}
}

return {
upQueries,
downQueries,
clonedTable,
table,
column
}
}

/**
* Drops column in the table.
*/
async dropColumn(tableOrName: Table|string, columnOrName: TableColumn|string): Promise<void> {
const { upQueries, downQueries, clonedTable, table, column } = await this.getDropColumnQueries(tableOrName, columnOrName);

await this.executeQueries(upQueries, downQueries);

clonedTable.removeColumn(column);
Expand Down Expand Up @@ -2059,9 +2098,27 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner
}
const result = await this.query(`SELECT "udt_schema", "udt_name" ` +
`FROM "information_schema"."columns" WHERE "table_schema" = '${schema}' AND "table_name" = '${name}' AND "column_name"='${column.name}'`);

let udtName = result[0]["udt_name"];

// You can not modify the array type. You need to edit the enum itself.
if (column.isArray) {
// Find the information about the array type
const typeResult = await this.query(`SELECT "typelem" FROM pg_type WHERE "typname" = $1`, [udtName]);

const typelem = typeResult[0]["typelem"];
// Get with the oid of the enum array type the enum type
const baseTypeNameResult = await this.query(`SELECT "typname" FROM pg_type WHERE oid = $1`, [typelem]);

const baseTypeName = baseTypeNameResult[0]["typname"];

// Replace the udtName with the enum type
udtName = baseTypeName;
}

return {
enumTypeSchema: result[0]["udt_schema"],
enumTypeName: result[0]["udt_name"]
enumTypeName: udtName
};
}

Expand Down
16 changes: 16 additions & 0 deletions test/github-issues/4350/entity/Post1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {Entity} from "../../../../src/decorator/entity/Entity";
import {PrimaryGeneratedColumn} from "../../../../src/decorator/columns/PrimaryGeneratedColumn";
import {Column} from "../../../../src/decorator/columns/Column";

export enum TestEnum1 {
VALUE1 = "VALUE1"
}

@Entity({ name: "post" })
export class Post1 {
@PrimaryGeneratedColumn()
id: number;

@Column({ enum: TestEnum1, enumName: "TestEnum", type: "enum", array: true })
testEnum: TestEnum1[];
}
17 changes: 17 additions & 0 deletions test/github-issues/4350/entity/Post2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {Entity} from "../../../../src/decorator/entity/Entity";
import {PrimaryGeneratedColumn} from "../../../../src/decorator/columns/PrimaryGeneratedColumn";
import {Column} from "../../../../src/decorator/columns/Column";

export enum TestEnum2 {
VALUE1 = "VALUE1",
VALUE2 = "VALUE2"
}

@Entity({ name: "post" })
export class Post2 {
@PrimaryGeneratedColumn()
id: number;

@Column({ enum: TestEnum2, enumName: "TestEnum", type: "enum", array: true })
testEnum: TestEnum2[];
}
13 changes: 13 additions & 0 deletions test/github-issues/4350/entity/Post3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {Entity} from "../../../../src/decorator/entity/Entity";
import {PrimaryGeneratedColumn} from "../../../../src/decorator/columns/PrimaryGeneratedColumn";
import {Column} from "../../../../src/decorator/columns/Column";
import {TestEnum2} from "./Post2";

@Entity({ name: "post" })
export class Post3 {
@PrimaryGeneratedColumn()
id: number;

@Column({ enum: TestEnum2, enumName: "TestEnum", type: "enum", array: false, nullable: true })
testEnum: TestEnum2;
}
13 changes: 13 additions & 0 deletions test/github-issues/4350/entity/Post4.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {Entity} from "../../../../src/decorator/entity/Entity";
import {PrimaryGeneratedColumn} from "../../../../src/decorator/columns/PrimaryGeneratedColumn";
import {Column} from "../../../../src/decorator/columns/Column";
import {TestEnum2} from "./Post2";

@Entity({ name: "post" })
export class Post4 {
@PrimaryGeneratedColumn()
id: number;

@Column({ enum: TestEnum2, enumName: "TestEnum", type: "enum", array: true })
testEnum: TestEnum2[];
}
118 changes: 118 additions & 0 deletions test/github-issues/4350/issue-4350.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import "reflect-metadata";
import {closeTestingConnections, createTestingConnections} from "../../utils/test-utils";
import {Post1, TestEnum1} from "./entity/Post1";
import {Post2, TestEnum2} from "./entity/Post2";
import {Post3} from "./entity/Post3";
import {Post4} from "./entity/Post4";
import {Connection} from "../../../src";
import {expect} from "chai";

describe("github issues > #4350 Array of enums column doesn't work at all", () => {
let connections: Connection[];
afterEach(() => closeTestingConnections(connections));
after(() => closeTestingConnections(connections));

it("should migrate postgres enums correctly", async () => {
connections = await createTestingConnections({
entities: [__dirname + "/entity/Post1{.js,.ts}"],
schemaCreate: true,
dropSchema: true,
enabledDrivers: ["postgres"]
});

await Promise.all(connections.map(async connection => {
let repo = connection.getRepository(Post1);

let post = new Post1();
post.testEnum = [TestEnum1.VALUE1];

post = await repo.save(post);

post.id.should.exist;
post.testEnum.should.be.an("array").that.includes(TestEnum1.VALUE1);
}));

await closeTestingConnections(connections);

connections = await createTestingConnections({
entities: [__dirname + "/entity/Post2{.js,.ts}"],
schemaCreate: true,
dropSchema: false,
enabledDrivers: ["postgres"]
});

await Promise.all(connections.map(async connection => {
let repo = connection.getRepository(Post2);

let post = await repo.findOne() as Post2;

expect(post).to.exist;
post.testEnum = [...post.testEnum, TestEnum2.VALUE2];

post = await repo.save(post);

post.id.should.exist;
post.testEnum.should.be.an("array").that.includes(TestEnum2.VALUE1).and.that.includes(TestEnum2.VALUE2);
}));
});

it('should migrate enum array to non array and back', async () => {
connections = await createTestingConnections({
entities: [__dirname + "/entity/Post2{.js,.ts}"],
schemaCreate: true,
dropSchema: false,
enabledDrivers: ["postgres"]
});

await Promise.all(connections.map(async connection => {
let repo = connection.getRepository(Post2);

let post = await repo.findOne() as Post2;

expect(post).to.exist;

post.id.should.exist;
post.testEnum.should.be.an("array").that.includes(TestEnum2.VALUE1).and.that.includes(TestEnum2.VALUE2);
}));

await closeTestingConnections(connections);

connections = await createTestingConnections({
entities: [__dirname + "/entity/Post3{.js,.ts}"],
schemaCreate: true,
dropSchema: false,
enabledDrivers: ["postgres"]
});

await Promise.all(connections.map(async connection => {
let repo = connection.getRepository(Post3);

let post = await repo.findOne() as Post3;

expect(post).to.exist;

expect(post.testEnum).to.exist;

post.testEnum.should.not.be.an('array');
}));

await closeTestingConnections(connections);

connections = await createTestingConnections({
entities: [__dirname + "/entity/Post4{.js,.ts}"],
schemaCreate: true,
dropSchema: false,
enabledDrivers: ["postgres"]
});

await Promise.all(connections.map(async connection => {
let repo = connection.getRepository(Post4);

let post = await repo.findOne() as Post4;

expect(post).to.exist;

post.testEnum.should.be.an('array');
}));
})
});