diff --git a/docs/entities.md b/docs/entities.md index 1f8b808b04..c47a6b2d0c 100644 --- a/docs/entities.md +++ b/docs/entities.md @@ -307,7 +307,7 @@ or `date`, `time`, `time without time zone`, `time with time zone`, `interval`, `bool`, `boolean`, `enum`, `point`, `line`, `lseg`, `box`, `path`, `polygon`, `circle`, `cidr`, `inet`, `macaddr`, `tsvector`, `tsquery`, `uuid`, `xml`, `json`, `jsonb`, `int4range`, `int8range`, `numrange`, -`tsrange`, `tstzrange`, `daterange`, `geometry`, `geography` +`tsrange`, `tstzrange`, `daterange`, `geometry`, `geography`, `cube` ### Column types for `cockroachdb` diff --git a/src/driver/postgres/PostgresDriver.ts b/src/driver/postgres/PostgresDriver.ts index 4195e4732d..74f631e3c4 100644 --- a/src/driver/postgres/PostgresDriver.ts +++ b/src/driver/postgres/PostgresDriver.ts @@ -148,7 +148,8 @@ export class PostgresDriver implements Driver { "tstzrange", "daterange", "geometry", - "geography" + "geography", + "cube" ]; /** @@ -301,13 +302,16 @@ export class PostgresDriver implements Driver { const hasHstoreColumns = this.connection.entityMetadatas.some(metadata => { return metadata.columns.filter(column => column.type === "hstore").length > 0; }); + const hasCubeColumns = this.connection.entityMetadatas.some(metadata => { + return metadata.columns.filter(column => column.type === "cube").length > 0; + }); const hasGeometryColumns = this.connection.entityMetadatas.some(metadata => { return metadata.columns.filter(column => this.spatialTypes.indexOf(column.type) >= 0).length > 0; }); const hasExclusionConstraints = this.connection.entityMetadatas.some(metadata => { return metadata.exclusions.length > 0; }); - if (hasUuidColumns || hasCitextColumns || hasHstoreColumns || hasGeometryColumns || hasExclusionConstraints) { + if (hasUuidColumns || hasCitextColumns || hasHstoreColumns || hasGeometryColumns || hasCubeColumns || hasExclusionConstraints) { await Promise.all([this.master, ...this.slaves].map(pool => { return new Promise((ok, fail) => { pool.connect(async (err: any, connection: any, release: Function) => { @@ -337,6 +341,12 @@ export class PostgresDriver implements Driver { } catch (_) { logger.log("warn", "At least one of the entities has a geometry column, but the 'postgis' extension cannot be installed automatically. Please install it manually using superuser rights"); } + if (hasCubeColumns) + try { + await this.executeQuery(connection, `CREATE EXTENSION IF NOT EXISTS "cube"`); + } catch (_) { + logger.log("warn", "At least one of the entities has a cube column, but the 'cube' extension cannot be installed automatically. Please install it manually using superuser rights"); + } if (hasExclusionConstraints) try { // The btree_gist extension provides operator support in PostgreSQL exclusion constraints @@ -425,6 +435,9 @@ export class PostgresDriver implements Driver { } else if (columnMetadata.type === "simple-json") { return DateUtils.simpleJsonToString(value); + } else if (columnMetadata.type === "cube") { + return `(${value.join(", ")})`; + } else if ( ( columnMetadata.type === "enum" @@ -482,6 +495,9 @@ export class PostgresDriver implements Driver { } else if (columnMetadata.type === "simple-json") { value = DateUtils.stringToSimpleJson(value); + } else if (columnMetadata.type === "cube") { + value = value.replace(/[\(\)\s]+/g, "").split(",").map(Number); + } else if (columnMetadata.type === "enum" || columnMetadata.type === "simple-enum" ) { if (columnMetadata.isArray) { // manually convert enum array to array of values (pg does not support, see https://github.com/brianc/node-pg-types/issues/56) diff --git a/src/driver/types/ColumnTypes.ts b/src/driver/types/ColumnTypes.ts index 9eb1aab5b1..dbe1241b95 100644 --- a/src/driver/types/ColumnTypes.ts +++ b/src/driver/types/ColumnTypes.ts @@ -180,7 +180,8 @@ export type SimpleColumnType = |"urowid" // oracle |"uniqueidentifier" // mssql |"rowversion" // mssql - |"array"; // cockroachdb + |"array" // cockroachdb + |"cube"; // postgres /** * Any column type column can be. diff --git a/test/functional/cube/postgres/cube-postgres.ts b/test/functional/cube/postgres/cube-postgres.ts new file mode 100644 index 0000000000..c36f7d74a4 --- /dev/null +++ b/test/functional/cube/postgres/cube-postgres.ts @@ -0,0 +1,116 @@ +import "reflect-metadata"; +import { expect } from "chai"; +import { Connection } from "../../../../src/connection/Connection"; +import { + closeTestingConnections, + createTestingConnections, + reloadTestingDatabases +} from "../../../utils/test-utils"; +import { Post } from "./entity/Post"; + +describe("cube-postgres", () => { + let connections: Connection[]; + before(async () => { + connections = await createTestingConnections({ + entities: [__dirname + "/entity/*{.js,.ts}"], + enabledDrivers: ["postgres"] + }); + }); + beforeEach(() => reloadTestingDatabases(connections)); + after(() => closeTestingConnections(connections)); + + it("should create correct schema with Postgres' cube type", () => + Promise.all( + connections.map(async connection => { + const queryRunner = connection.createQueryRunner(); + const schema = await queryRunner.getTable("post"); + await queryRunner.release(); + expect(schema).not.to.be.undefined; + const cubeColumn = schema!.columns.find( + tableColumn => + tableColumn.name === "color" && + tableColumn.type === "cube" + ); + expect(cubeColumn).to.not.be.undefined; + }) + )); + + it("should persist cube correctly", () => + Promise.all( + connections.map(async connection => { + const color = [255, 0, 0]; + const postRepo = connection.getRepository(Post); + const post = new Post(); + post.color = color; + const persistedPost = await postRepo.save(post); + const foundPost = await postRepo.findOne(persistedPost.id); + expect(foundPost).to.exist; + expect(foundPost!.color).to.deep.equal(color); + }) + )); + + it("should update cube correctly", () => + Promise.all( + connections.map(async connection => { + const color = [255, 0, 0]; + const color2 = [0, 255, 0]; + const postRepo = connection.getRepository(Post); + const post = new Post(); + post.color = color; + const persistedPost = await postRepo.save(post); + + await postRepo.update( + { id: persistedPost.id }, + { color: color2 } + ); + + const foundPost = await postRepo.findOne(persistedPost.id); + expect(foundPost).to.exist; + expect(foundPost!.color).to.deep.equal(color2); + }) + )); + + it("should re-save cube correctly", () => + Promise.all( + connections.map(async connection => { + const color = [255, 0, 0]; + const color2 = [0, 255, 0]; + const postRepo = connection.getRepository(Post); + const post = new Post(); + post.color = color; + const persistedPost = await postRepo.save(post); + + persistedPost.color = color2; + await postRepo.save(persistedPost); + + const foundPost = await postRepo.findOne(persistedPost.id); + expect(foundPost).to.exist; + expect(foundPost!.color).to.deep.equal(color2); + }) + )); + + it("should be able to order cube by euclidean distance", () => + Promise.all( + connections.map(async connection => { + const color1 = [255, 0, 0]; + const color2 = [255, 255, 0]; + const color3 = [255, 255, 255]; + + const post1 = new Post(); + post1.color = color1; + const post2 = new Post(); + post2.color = color2; + const post3 = new Post(); + post3.color = color3; + await connection.manager.save([post1, post2, post3]); + + const posts = await connection.manager + .createQueryBuilder(Post, "post") + .orderBy("color <-> '(0, 255, 0)'", "DESC") + .getMany(); + + const postIds = posts.map(post => post.id); + expect(postIds).to.deep.equal([post1.id, post3.id, post2.id]); + }) + )); +}); diff --git a/test/functional/cube/postgres/entity/Post.ts b/test/functional/cube/postgres/entity/Post.ts new file mode 100644 index 0000000000..d182b8b59b --- /dev/null +++ b/test/functional/cube/postgres/entity/Post.ts @@ -0,0 +1,15 @@ +import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn"; +import {Entity} from "../../../../../src/decorator/entity/Entity"; +import {Column} from "../../../../../src/decorator/columns/Column"; + +@Entity() +export class Post { + + @PrimaryGeneratedColumn() + id: number; + + @Column("cube", { + nullable: true + }) + color: number[]; +}