Skip to content

Commit

Permalink
feat: implement postgres ltree (#6480)
Browse files Browse the repository at this point in the history
This new feature implements support for the postgres extension ltree

Closes: #4193
  • Loading branch information
tomSawkins committed Aug 2, 2020
1 parent 0d6191b commit 43a7386
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 4 deletions.
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);
})
));
});


0 comments on commit 43a7386

Please sign in to comment.