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

feat: implement postgres ltree #6480

Merged
merged 1 commit into from Aug 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/entities.md
Expand Up @@ -311,7 +311,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`, `cube`
`tsrange`, `tstzrange`, `daterange`, `geometry`, `geography`, `cube`, `ltree`

### Column types for `cockroachdb`

Expand Down
18 changes: 16 additions & 2 deletions src/driver/postgres/PostgresDriver.ts
Expand Up @@ -151,7 +151,8 @@ export class PostgresDriver implements Driver {
"daterange",
"geometry",
"geography",
"cube"
"cube",
"ltree"
];

/**
Expand Down Expand Up @@ -328,6 +329,7 @@ export class PostgresDriver implements Driver {
hasHstoreColumns,
hasCubeColumns,
hasGeometryColumns,
hasLtreeColumns,
hasExclusionConstraints,
} = extensionsMetadata;

Expand Down Expand Up @@ -361,6 +363,12 @@ export class PostgresDriver implements Driver {
} 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 (hasLtreeColumns)
try {
await this.executeQuery(connection, `CREATE EXTENSION IF NOT EXISTS "ltree"`);
} catch (_) {
logger.log("warn", "At least one of the entities has a cube column, but the 'ltree' 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 All @@ -386,6 +394,9 @@ export class PostgresDriver implements Driver {
const hasGeometryColumns = this.connection.entityMetadatas.some(metadata => {
return metadata.columns.filter(column => this.spatialTypes.indexOf(column.type) >= 0).length > 0;
});
const hasLtreeColumns = this.connection.entityMetadatas.some(metadata => {
return metadata.columns.filter(column => column.type === 'ltree').length > 0;
});
const hasExclusionConstraints = this.connection.entityMetadatas.some(metadata => {
return metadata.exclusions.length > 0;
});
Expand All @@ -396,8 +407,9 @@ export class PostgresDriver implements Driver {
hasHstoreColumns,
hasCubeColumns,
hasGeometryColumns,
hasLtreeColumns,
hasExclusionConstraints,
hasExtensions: hasUuidColumns || hasCitextColumns || hasHstoreColumns || hasGeometryColumns || hasCubeColumns || hasExclusionConstraints,
hasExtensions: hasUuidColumns || hasCitextColumns || hasHstoreColumns || hasGeometryColumns || hasCubeColumns || hasLtreeColumns || hasExclusionConstraints,
};
}

Expand Down Expand Up @@ -487,6 +499,8 @@ export class PostgresDriver implements Driver {
}
return `(${value.join(",")})`;

} else if (columnMetadata.type === "ltree") {
return value.split(".").filter(Boolean).join('.').replace(/[\s]+/g, "_");
} else if (
(
columnMetadata.type === "enum"
Expand Down
3 changes: 2 additions & 1 deletion src/driver/types/ColumnTypes.ts
Expand Up @@ -193,7 +193,8 @@ export type SimpleColumnType =
|"uniqueidentifier" // mssql
|"rowversion" // mssql
|"array" // cockroachdb, sap
|"cube"; // postgres
|"cube" // postgres
|"ltree"; // postgres

/**
* Any column type column can be.
Expand Down
14 changes: 14 additions & 0 deletions test/functional/ltree/postgres/entity/Post.ts
@@ -0,0 +1,14 @@
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("ltree", {
nullable: false
})
path: string;
}
136 changes: 136 additions & 0 deletions test/functional/ltree/postgres/ltree-postgres.ts
@@ -0,0 +1,136 @@
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("ltree-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' ltree 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 ltreeColumn = schema!.columns.find(
tableColumn =>
tableColumn.name === "path" &&
tableColumn.type === "ltree" &&
!tableColumn.isArray
);
expect(ltreeColumn).to.not.be.undefined;
})
));

it("should persist ltree correctly", () =>
Promise.all(
connections.map(async connection => {
const path = 'News.Featured.Opinion';
const postRepo = connection.getRepository(Post);
const post = new Post();
post.path = path;
const persistedPost = await postRepo.save(post);
const foundPost = await postRepo.findOne(persistedPost.id);
expect(foundPost).to.exist;
expect(foundPost!.path).to.deep.equal(path);
})
));

it("should update ltree correctly", () =>
Promise.all(
connections.map(async connection => {
const path = 'News.Featured.Opinion';
const path2 = 'News.Featured.Gossip';
const postRepo = connection.getRepository(Post);
const post = new Post();
post.path = path;
const persistedPost = await postRepo.save(post);

await postRepo.update(
{ id: persistedPost.id },
{ path: path2 }
);

const foundPost = await postRepo.findOne(persistedPost.id);
expect(foundPost).to.exist;
expect(foundPost!.path).to.deep.equal(path2);
})
));

it("should re-save ltree correctly", () =>
Promise.all(
connections.map(async connection => {
const path = 'News.Featured.Opinion';
const path2 = 'News.Featured.Gossip';
const postRepo = connection.getRepository(Post);
const post = new Post();
post.path = path;
const persistedPost = await postRepo.save(post);

persistedPost.path = path2;
await postRepo.save(persistedPost);

const foundPost = await postRepo.findOne(persistedPost.id);
expect(foundPost).to.exist;
expect(foundPost!.path).to.deep.equal(path2);
})
));

it("should persist ltree correctly with trailing '.'", () =>
Promise.all(
connections.map(async connection => {
const path = 'News.Featured.Opinion.';
const postRepo = connection.getRepository(Post);
const post = new Post();
post.path = path;
const persistedPost = await postRepo.save(post);
const foundPost = await postRepo.findOne(persistedPost.id);
expect(foundPost).to.exist;
expect(foundPost!.path).to.deep.equal('News.Featured.Opinion');
})
));

it("should persist ltree correctly when containing spaces", () =>
Promise.all(
connections.map(async connection => {
const path = 'News.Featured Story.Opinion';
const postRepo = connection.getRepository(Post);
const post = new Post();
post.path = path;
const persistedPost = await postRepo.save(post);
const foundPost = await postRepo.findOne(persistedPost.id);
expect(foundPost).to.exist;
expect(foundPost!.path).to.deep.equal('News.Featured_Story.Opinion');
})
));

it("should be able to query ltree correctly", () =>
Promise.all(
connections.map(async connection => {
const path = 'News.Featured.Opinion';
const postRepo = connection.getRepository(Post);
const post = new Post();
post.path = path;
await postRepo.save(post);
const foundPost = await postRepo.createQueryBuilder().where(`path ~ 'news@.*'`).getOne()
expect(foundPost).to.exist;
expect(foundPost!.path).to.deep.equal(path);
})
));
});