mirror of
https://github.com/typeorm/typeorm.git
synced 2025-12-08 21:26:23 +00:00
feat: implement postgres ltree (#6480)
This new feature implements support for the postgres extension ltree Closes: #4193
This commit is contained in:
parent
0d6191b177
commit
43a7386719
@ -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`
|
||||
|
||||
|
||||
@ -151,7 +151,8 @@ export class PostgresDriver implements Driver {
|
||||
"daterange",
|
||||
"geometry",
|
||||
"geography",
|
||||
"cube"
|
||||
"cube",
|
||||
"ltree"
|
||||
];
|
||||
|
||||
/**
|
||||
@ -328,6 +329,7 @@ export class PostgresDriver implements Driver {
|
||||
hasHstoreColumns,
|
||||
hasCubeColumns,
|
||||
hasGeometryColumns,
|
||||
hasLtreeColumns,
|
||||
hasExclusionConstraints,
|
||||
} = extensionsMetadata;
|
||||
|
||||
@ -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
|
||||
@ -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;
|
||||
});
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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.
|
||||
|
||||
14
test/functional/ltree/postgres/entity/Post.ts
Normal file
14
test/functional/ltree/postgres/entity/Post.ts
Normal file
@ -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
test/functional/ltree/postgres/ltree-postgres.ts
Normal file
136
test/functional/ltree/postgres/ltree-postgres.ts
Normal file
@ -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);
|
||||
})
|
||||
));
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user