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

Commit b6d6278

Browse files
liaujianjievlapo
authored andcommitted
feat: add multi-dimensional cube support for PostgreSQL (typeorm#4378)
1 parent e12479e commit b6d6278

5 files changed

Lines changed: 152 additions & 4 deletions

File tree

docs/entities.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ or
307307
`date`, `time`, `time without time zone`, `time with time zone`, `interval`, `bool`, `boolean`,
308308
`enum`, `point`, `line`, `lseg`, `box`, `path`, `polygon`, `circle`, `cidr`, `inet`, `macaddr`,
309309
`tsvector`, `tsquery`, `uuid`, `xml`, `json`, `jsonb`, `int4range`, `int8range`, `numrange`,
310-
`tsrange`, `tstzrange`, `daterange`, `geometry`, `geography`
310+
`tsrange`, `tstzrange`, `daterange`, `geometry`, `geography`, `cube`
311311

312312
### Column types for `cockroachdb`
313313

src/driver/postgres/PostgresDriver.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,8 @@ export class PostgresDriver implements Driver {
148148
"tstzrange",
149149
"daterange",
150150
"geometry",
151-
"geography"
151+
"geography",
152+
"cube"
152153
];
153154

154155
/**
@@ -301,13 +302,16 @@ export class PostgresDriver implements Driver {
301302
const hasHstoreColumns = this.connection.entityMetadatas.some(metadata => {
302303
return metadata.columns.filter(column => column.type === "hstore").length > 0;
303304
});
305+
const hasCubeColumns = this.connection.entityMetadatas.some(metadata => {
306+
return metadata.columns.filter(column => column.type === "cube").length > 0;
307+
});
304308
const hasGeometryColumns = this.connection.entityMetadatas.some(metadata => {
305309
return metadata.columns.filter(column => this.spatialTypes.indexOf(column.type) >= 0).length > 0;
306310
});
307311
const hasExclusionConstraints = this.connection.entityMetadatas.some(metadata => {
308312
return metadata.exclusions.length > 0;
309313
});
310-
if (hasUuidColumns || hasCitextColumns || hasHstoreColumns || hasGeometryColumns || hasExclusionConstraints) {
314+
if (hasUuidColumns || hasCitextColumns || hasHstoreColumns || hasGeometryColumns || hasCubeColumns || hasExclusionConstraints) {
311315
await Promise.all([this.master, ...this.slaves].map(pool => {
312316
return new Promise((ok, fail) => {
313317
pool.connect(async (err: any, connection: any, release: Function) => {
@@ -337,6 +341,12 @@ export class PostgresDriver implements Driver {
337341
} catch (_) {
338342
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");
339343
}
344+
if (hasCubeColumns)
345+
try {
346+
await this.executeQuery(connection, `CREATE EXTENSION IF NOT EXISTS "cube"`);
347+
} catch (_) {
348+
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");
349+
}
340350
if (hasExclusionConstraints)
341351
try {
342352
// The btree_gist extension provides operator support in PostgreSQL exclusion constraints
@@ -425,6 +435,9 @@ export class PostgresDriver implements Driver {
425435
} else if (columnMetadata.type === "simple-json") {
426436
return DateUtils.simpleJsonToString(value);
427437

438+
} else if (columnMetadata.type === "cube") {
439+
return `(${value.join(", ")})`;
440+
428441
} else if (
429442
(
430443
columnMetadata.type === "enum"
@@ -482,6 +495,9 @@ export class PostgresDriver implements Driver {
482495
} else if (columnMetadata.type === "simple-json") {
483496
value = DateUtils.stringToSimpleJson(value);
484497

498+
} else if (columnMetadata.type === "cube") {
499+
value = value.replace(/[\(\)\s]+/g, "").split(",").map(Number);
500+
485501
} else if (columnMetadata.type === "enum" || columnMetadata.type === "simple-enum" ) {
486502
if (columnMetadata.isArray) {
487503
// manually convert enum array to array of values (pg does not support, see https://github.com/brianc/node-pg-types/issues/56)

src/driver/types/ColumnTypes.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,8 @@ export type SimpleColumnType =
180180
|"urowid" // oracle
181181
|"uniqueidentifier" // mssql
182182
|"rowversion" // mssql
183-
|"array"; // cockroachdb
183+
|"array" // cockroachdb
184+
|"cube"; // postgres
184185

185186
/**
186187
* Any column type column can be.
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import "reflect-metadata";
2+
import { expect } from "chai";
3+
import { Connection } from "../../../../src/connection/Connection";
4+
import {
5+
closeTestingConnections,
6+
createTestingConnections,
7+
reloadTestingDatabases
8+
} from "../../../utils/test-utils";
9+
import { Post } from "./entity/Post";
10+
11+
describe("cube-postgres", () => {
12+
let connections: Connection[];
13+
before(async () => {
14+
connections = await createTestingConnections({
15+
entities: [__dirname + "/entity/*{.js,.ts}"],
16+
enabledDrivers: ["postgres"]
17+
});
18+
});
19+
beforeEach(() => reloadTestingDatabases(connections));
20+
after(() => closeTestingConnections(connections));
21+
22+
it("should create correct schema with Postgres' cube type", () =>
23+
Promise.all(
24+
connections.map(async connection => {
25+
const queryRunner = connection.createQueryRunner();
26+
const schema = await queryRunner.getTable("post");
27+
await queryRunner.release();
28+
expect(schema).not.to.be.undefined;
29+
const cubeColumn = schema!.columns.find(
30+
tableColumn =>
31+
tableColumn.name === "color" &&
32+
tableColumn.type === "cube"
33+
);
34+
expect(cubeColumn).to.not.be.undefined;
35+
})
36+
));
37+
38+
it("should persist cube correctly", () =>
39+
Promise.all(
40+
connections.map(async connection => {
41+
const color = [255, 0, 0];
42+
const postRepo = connection.getRepository(Post);
43+
const post = new Post();
44+
post.color = color;
45+
const persistedPost = await postRepo.save(post);
46+
const foundPost = await postRepo.findOne(persistedPost.id);
47+
expect(foundPost).to.exist;
48+
expect(foundPost!.color).to.deep.equal(color);
49+
})
50+
));
51+
52+
it("should update cube correctly", () =>
53+
Promise.all(
54+
connections.map(async connection => {
55+
const color = [255, 0, 0];
56+
const color2 = [0, 255, 0];
57+
const postRepo = connection.getRepository(Post);
58+
const post = new Post();
59+
post.color = color;
60+
const persistedPost = await postRepo.save(post);
61+
62+
await postRepo.update(
63+
{ id: persistedPost.id },
64+
{ color: color2 }
65+
);
66+
67+
const foundPost = await postRepo.findOne(persistedPost.id);
68+
expect(foundPost).to.exist;
69+
expect(foundPost!.color).to.deep.equal(color2);
70+
})
71+
));
72+
73+
it("should re-save cube correctly", () =>
74+
Promise.all(
75+
connections.map(async connection => {
76+
const color = [255, 0, 0];
77+
const color2 = [0, 255, 0];
78+
const postRepo = connection.getRepository(Post);
79+
const post = new Post();
80+
post.color = color;
81+
const persistedPost = await postRepo.save(post);
82+
83+
persistedPost.color = color2;
84+
await postRepo.save(persistedPost);
85+
86+
const foundPost = await postRepo.findOne(persistedPost.id);
87+
expect(foundPost).to.exist;
88+
expect(foundPost!.color).to.deep.equal(color2);
89+
})
90+
));
91+
92+
it("should be able to order cube by euclidean distance", () =>
93+
Promise.all(
94+
connections.map(async connection => {
95+
const color1 = [255, 0, 0];
96+
const color2 = [255, 255, 0];
97+
const color3 = [255, 255, 255];
98+
99+
const post1 = new Post();
100+
post1.color = color1;
101+
const post2 = new Post();
102+
post2.color = color2;
103+
const post3 = new Post();
104+
post3.color = color3;
105+
await connection.manager.save([post1, post2, post3]);
106+
107+
const posts = await connection.manager
108+
.createQueryBuilder(Post, "post")
109+
.orderBy("color <-> '(0, 255, 0)'", "DESC")
110+
.getMany();
111+
112+
const postIds = posts.map(post => post.id);
113+
expect(postIds).to.deep.equal([post1.id, post3.id, post2.id]);
114+
})
115+
));
116+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn";
2+
import {Entity} from "../../../../../src/decorator/entity/Entity";
3+
import {Column} from "../../../../../src/decorator/columns/Column";
4+
5+
@Entity()
6+
export class Post {
7+
8+
@PrimaryGeneratedColumn()
9+
id: number;
10+
11+
@Column("cube", {
12+
nullable: true
13+
})
14+
color: number[];
15+
}

0 commit comments

Comments
 (0)