Skip to content
This repository has been archived by the owner on Mar 18, 2022. It is now read-only.

Commit

Permalink
feat: add multi-dimensional cube support for PostgreSQL (typeorm#4378)
Browse files Browse the repository at this point in the history
  • Loading branch information
liaujianjie authored and vlapo committed Jul 21, 2019
1 parent e12479e commit b6d6278
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 4 deletions.
2 changes: 1 addition & 1 deletion docs/entities.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
20 changes: 18 additions & 2 deletions src/driver/postgres/PostgresDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ export class PostgresDriver implements Driver {
"tstzrange",
"daterange",
"geometry",
"geography"
"geography",
"cube"
];

/**
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion src/driver/types/ColumnTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
116 changes: 116 additions & 0 deletions test/functional/cube/postgres/cube-postgres.ts
Original file line number Diff line number Diff line change
@@ -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]);
})
));
});
15 changes: 15 additions & 0 deletions test/functional/cube/postgres/entity/Post.ts
Original file line number Diff line number Diff line change
@@ -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[];
}

0 comments on commit b6d6278

Please sign in to comment.