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

Add Postgres connection option to use the pgcrypto extension to generate UUIDs #3537

Merged
merged 12 commits into from
Feb 27, 2019
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ feel free to ask us and community.
* fixed signatures of `update`/`insert` methods, some `find*` methods in repositories, entity managers, BaseEntity and QueryBuilders
* handle embedded documents through multiple levels in mongodb ([#3551](https://github.com/typeorm/typeorm/issues/3551))
* fixed hanging connections in `mssql` driver ([#3327](https://github.com/typeorm/typeorm/pull/3327))
* fixed call to deprecated `uuid_generate_v4()` for Postgres, now using `gen_random_uuid()`


### Features

Expand Down
3 changes: 2 additions & 1 deletion src/connection/options-reader/ConnectionOptionsEnvReader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ export class ConnectionOptionsEnvReader {
migrationsDir: PlatformTools.getEnvVariable("TYPEORM_MIGRATIONS_DIR"),
subscribersDir: PlatformTools.getEnvVariable("TYPEORM_SUBSCRIBERS_DIR"),
},
cache: this.transformCaching()
cache: this.transformCaching(),
uuidExtension: PlatformTools.getEnvVariable("TYPEORM_UUID_EXTENSION")
};
}

Expand Down
4 changes: 3 additions & 1 deletion src/driver/postgres/PostgresConnectionOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,6 @@ export interface PostgresConnectionOptions extends BaseConnectionOptions, Postgr

};

}
readonly uuidExtension?: "pgcrypto" | "uuid-ossp";

}
8 changes: 6 additions & 2 deletions src/driver/postgres/PostgresDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,9 +302,9 @@ export class PostgresDriver implements Driver {
if (err) return fail(err);
if (hasUuidColumns)
try {
await this.executeQuery(connection, `CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
await this.executeQuery(connection, `CREATE EXTENSION IF NOT EXISTS "${this.options.uuidExtension || "uuid-ossp"}"`);
} catch (_) {
logger.log("warn", "At least one of the entities has uuid column, but the 'uuid-ossp' extension cannot be installed automatically. Please install it manually using superuser rights");
logger.log("warn", `At least one of the entities has uuid column, but the '${this.options.uuidExtension || "uuid-ossp"}' extension cannot be installed automatically. Please install it manually using superuser rights, or select another uuid extension.`);
}
if (hasCitextColumns)
try {
Expand Down Expand Up @@ -780,6 +780,10 @@ export class PostgresDriver implements Driver {
return true;
}

get uuidGenerator(): string {
return this.options.uuidExtension === "pgcrypto" ? "gen_random_uuid()" : "uuid_generate_v4()";
}

/**
* Creates an escaped parameter.
*/
Expand Down
6 changes: 2 additions & 4 deletions src/driver/postgres/PostgresQueryRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1469,11 +1469,9 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner
if (dbColumn["column_default"].replace(/"/gi, "") === `nextval('${this.buildSequenceName(table, dbColumn["column_name"], currentSchema, true)}'::regclass)`) {
tableColumn.isGenerated = true;
tableColumn.generationStrategy = "increment";

} else if (/^uuid_generate_v\d\(\)/.test(dbColumn["column_default"])) {
} else if (dbColumn["column_default"] === "gen_random_uuid()" || /^uuid_generate_v\d\(\)/.test(dbColumn["column_default"])) {
tableColumn.isGenerated = true;
tableColumn.generationStrategy = "uuid";

} else {
tableColumn.default = dbColumn["column_default"].replace(/::.*/, "");
}
Expand Down Expand Up @@ -1912,7 +1910,7 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner
if (column.default !== undefined && column.default !== null)
c += " DEFAULT " + column.default;
if (column.isGenerated && column.generationStrategy === "uuid" && !column.default)
c += " DEFAULT uuid_generate_v4()";
c += ` DEFAULT ${this.driver.uuidGenerator}`;

return c;
}
Expand Down
2 changes: 1 addition & 1 deletion test/functional/schema-builder/change-column.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ describe("schema builder > change column", () => {
const queryRunner = connection.createQueryRunner();

if (connection.driver instanceof PostgresDriver)
await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS "pgcrypto"`);

const postMetadata = connection.getMetadata(Post);
const idColumn = postMetadata.findColumnWithPropertyName("id")!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ import {closeTestingConnections, createTestingConnections, reloadTestingDatabase
import {Post} from "./entity/Post";
import {Question} from "./entity/Question";

describe("uuid-postgres", () => {
describe("pgcrypto", () => {
Copy link
Contributor Author

@typokign typokign Feb 24, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diff isn't exactly clear what I did here - I actually renamed the uuid-postgres tests to uuid-ossp, and configured the tests to use the uuid-ossp extension. Then I copied the uuid-ossp.ts file to pgcrypto.ts, left all the tests the same, and set the extension to pgcrypto. Looks like Git linked the old uuid-postgres file to the new pgcrypto file, but if you manually diff the two files you'll see that none of the tests have been changed.


let connections: Connection[];
before(async () => {
connections = await createTestingConnections({
entities: [__dirname + "/entity/*{.js,.ts}"],
enabledDrivers: ["postgres"],
driverSpecific: {
uuidExtension: "pgcrypto"
}
});
});
beforeEach(() => reloadTestingDatabases(connections));
Expand Down
98 changes: 98 additions & 0 deletions test/functional/uuid/postgres/uuid-ossp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import "reflect-metadata";
import {expect} from "chai";
import {Record} from "./entity/Record";
import {Connection} from "../../../../src/connection/Connection";
import {closeTestingConnections, createTestingConnections, reloadTestingDatabases} from "../../../utils/test-utils";
import {Post} from "./entity/Post";
import {Question} from "./entity/Question";

describe("uuid-ossp", () => {

let connections: Connection[];
before(async () => {
connections = await createTestingConnections({
entities: [__dirname + "/entity/*{.js,.ts}"],
enabledDrivers: ["postgres"],
driverSpecific: {
uuidExtension: "uuid-ossp"
}
});
});
beforeEach(() => reloadTestingDatabases(connections));
after(() => closeTestingConnections(connections));

it("should make correct schema with Postgres' uuid type", () => Promise.all(connections.map(async connection => {
const queryRunner = connection.createQueryRunner();
const schema = await queryRunner.getTable("record");
await queryRunner.release();
expect(schema).not.to.be.empty;
expect(schema!.columns.find(tableColumn => tableColumn.name === "id" && tableColumn.type === "uuid" && tableColumn.isGenerated)).to.be.not.empty;
})));

it("should persist uuid correctly", () => Promise.all(connections.map(async connection => {
const recordRepo = connection.getRepository(Record);
const record = new Record();
record.id = "fd357b8f-8838-42f6-b7a2-ae027444e895";
const persistedRecord = await recordRepo.save(record);
const foundRecord = await recordRepo.findOne(persistedRecord.id);
expect(foundRecord).to.be.exist;
expect(foundRecord!.id).to.eq("fd357b8f-8838-42f6-b7a2-ae027444e895");
})));

it("should persist uuid correctly when it is generated non primary column", () => Promise.all(connections.map(async connection => {

const postRepository = connection.getRepository(Post);
const questionRepository = connection.getRepository(Question);
const queryRunner = connection.createQueryRunner();
const postTable = await queryRunner.getTable("post");
const questionTable = await queryRunner.getTable("question");
await queryRunner.release();

const post = new Post();
await postRepository.save(post);
const loadedPost = await postRepository.findOne(1);
expect(loadedPost!.uuid).to.be.exist;
postTable!.findColumnByName("uuid")!.type.should.be.equal("uuid");

const post2 = new Post();
post2.uuid = "fd357b8f-8838-42f6-b7a2-ae027444e895";
await postRepository.save(post2);
const loadedPost2 = await postRepository.findOne(2);
expect(loadedPost2!.uuid).to.equal("fd357b8f-8838-42f6-b7a2-ae027444e895");

const question = new Question();
question.uuid2 = "fd357b8f-8838-42f6-b7a2-ae027444e895";

const savedQuestion = await questionRepository.save(question);
expect(savedQuestion!.id).to.be.exist;
expect(savedQuestion!.uuid).to.be.exist;
expect(savedQuestion!.uuid2).to.equal("fd357b8f-8838-42f6-b7a2-ae027444e895");
expect(savedQuestion!.uuid3).to.be.null;
expect(savedQuestion!.uuid4).to.be.exist;

const loadedQuestion = await questionRepository.findOne(savedQuestion.id);
expect(loadedQuestion!.id).to.be.exist;
expect(loadedQuestion!.uuid).to.be.exist;
expect(loadedQuestion!.uuid2).to.equal("fd357b8f-8838-42f6-b7a2-ae027444e895");
expect(loadedQuestion!.uuid3).to.be.null;
expect(loadedQuestion!.uuid4).to.be.exist;
questionTable!.findColumnByName("id")!.type.should.be.equal("uuid");
questionTable!.findColumnByName("uuid")!.type.should.be.equal("uuid");
questionTable!.findColumnByName("uuid2")!.type.should.be.equal("uuid");
questionTable!.findColumnByName("uuid3")!.type.should.be.equal("uuid");

const question2 = new Question();
question2.id = "1ecad7f6-23ee-453e-bb44-16eca26d5189";
question2.uuid = "35b44650-b2cd-44ec-aa54-137fbdf1c373";
question2.uuid2 = "fd357b8f-8838-42f6-b7a2-ae027444e895";
question2.uuid3 = null;
question2.uuid4 = null;
await questionRepository.save(question2);
const loadedQuestion2 = await questionRepository.findOne("1ecad7f6-23ee-453e-bb44-16eca26d5189");
expect(loadedQuestion2!.id).to.equal("1ecad7f6-23ee-453e-bb44-16eca26d5189");
expect(loadedQuestion2!.uuid).to.equal("35b44650-b2cd-44ec-aa54-137fbdf1c373");
expect(loadedQuestion2!.uuid2).to.equal("fd357b8f-8838-42f6-b7a2-ae027444e895");
expect(loadedQuestion2!.uuid3).to.be.null;
expect(loadedQuestion2!.uuid4).to.be.null;
})));
});