From 1e20c6b04a757db2bf125eb52ce0e0f6692593e3 Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Wed, 27 Jun 2018 21:54:24 -0700 Subject: [PATCH 01/17] Spatial support for PostgreSQL using PostGIS This includes support for both geometry and geography columns as well as GiST indices (used when passing the `spatial: true`). Client representations use GeoJSON (existing MySQL and MS SQL drivers use WKT (well-known text)) for compatibility with geospatial libraries such as Turf, JSTS, etc. --- docker-compose.yml | 2 +- src/decorator/options/IndexOptions.ts | 2 +- src/driver/postgres/PostgresDriver.ts | 22 +++- src/driver/postgres/PostgresQueryRunner.ts | 12 +- src/entity-schema/EntitySchemaIndexOptions.ts | 2 +- src/query-builder/InsertQueryBuilder.ts | 2 + src/query-builder/SelectQueryBuilder.ts | 4 + .../spatial/postgres/entity/Post.ts | 24 ++++ .../spatial/postgres/spatial-postgres.ts | 119 ++++++++++++++++++ 9 files changed, 178 insertions(+), 11 deletions(-) create mode 100644 test/functional/spatial/postgres/entity/Post.ts create mode 100644 test/functional/spatial/postgres/spatial-postgres.ts diff --git a/docker-compose.yml b/docker-compose.yml index 4e95596f48..45e981947d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,7 +39,7 @@ services: # postgres postgres: - image: "postgres:9.6.1" + image: "mdillon/postgis:9.6" container_name: "typeorm-postgres" ports: - "5432:5432" diff --git a/src/decorator/options/IndexOptions.ts b/src/decorator/options/IndexOptions.ts index dd418e3645..b73c67ac46 100644 --- a/src/decorator/options/IndexOptions.ts +++ b/src/decorator/options/IndexOptions.ts @@ -10,7 +10,7 @@ export interface IndexOptions { /** * The SPATIAL modifier indexes the entire column and does not allow indexed columns to contain NULL values. - * Works only in MySQL. + * Works only in MySQL and PostgreSQL. */ spatial?: boolean; diff --git a/src/driver/postgres/PostgresDriver.ts b/src/driver/postgres/PostgresDriver.ts index 6920248d90..4184f480ae 100644 --- a/src/driver/postgres/PostgresDriver.ts +++ b/src/driver/postgres/PostgresDriver.ts @@ -145,13 +145,18 @@ export class PostgresDriver implements Driver { "numrange", "tsrange", "tstzrange", - "daterange" + "daterange", + "geometry", + "geography" ]; /** * Gets list of spatial column data types. */ - spatialTypes: ColumnType[] = []; + spatialTypes: ColumnType[] = [ + "geometry", + "geography" + ]; /** * Gets list of column data types that support length by a driver. @@ -283,7 +288,10 @@ export class PostgresDriver implements Driver { const hasHstoreColumns = this.connection.entityMetadatas.some(metadata => { return metadata.columns.filter(column => column.type === "hstore").length > 0; }); - if (hasUuidColumns || hasCitextColumns || hasHstoreColumns) { + const hasGeometryColumns = this.connection.entityMetadatas.some(metadata => { + return metadata.columns.filter(column => this.spatialTypes.indexOf(column.type) >= 0).length > 0; + }); + if (hasUuidColumns || hasCitextColumns || hasHstoreColumns || hasGeometryColumns) { await Promise.all([this.master, ...this.slaves].map(pool => { return new Promise((ok, fail) => { pool.connect(async (err: any, connection: any, release: Function) => { @@ -307,6 +315,12 @@ export class PostgresDriver implements Driver { } catch (_) { logger.log("warn", "At least one of the entities has hstore column, but the 'hstore' extension cannot be installed automatically. Please install it manually using superuser rights"); } + if (hasGeometryColumns) + try { + await this.executeQuery(connection, `CREATE EXTENSION IF NOT EXISTS "postgis"`); + } 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"); + } release(); ok(); }); @@ -370,7 +384,7 @@ export class PostgresDriver implements Driver { || columnMetadata.type === "timestamp without time zone") { return DateUtils.mixedDateToDate(value); - } else if (columnMetadata.type === "json" || columnMetadata.type === "jsonb") { + } else if (this.spatialTypes.concat(["json", "jsonb"]).indexOf(columnMetadata.type) >= 0) { return JSON.stringify(value); } else if (columnMetadata.type === "hstore") { diff --git a/src/driver/postgres/PostgresQueryRunner.ts b/src/driver/postgres/PostgresQueryRunner.ts index b883ea50ce..b5bddb0879 100644 --- a/src/driver/postgres/PostgresQueryRunner.ts +++ b/src/driver/postgres/PostgresQueryRunner.ts @@ -1182,7 +1182,9 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner await this.startTransaction(); try { - const selectDropsQuery = `SELECT 'DROP TABLE IF EXISTS "' || schemaname || '"."' || tablename || '" CASCADE;' as "query" FROM "pg_tables" WHERE "schemaname" IN (${schemaNamesString})`; + // ignore spatial_ref_sys; it's a special table supporting PostGIS + // TODO generalize this as this.driver.ignoreTables + const selectDropsQuery = `SELECT 'DROP TABLE IF EXISTS "' || schemaname || '"."' || tablename || '" CASCADE;' as "query" FROM "pg_tables" WHERE "schemaname" IN (${schemaNamesString}) AND tablename NOT IN ('spatial_ref_sys')`; const dropQueries: ObjectLiteral[] = await this.query(selectDropsQuery); await Promise.all(dropQueries.map(q => this.query(q["query"]))); await this.dropEnumTypes(schemaNamesString); @@ -1242,12 +1244,14 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner `WHERE "t"."relkind" = 'r' AND (${constraintsCondition})`; const indicesSql = `SELECT "ns"."nspname" AS "table_schema", "t"."relname" AS "table_name", "i"."relname" AS "constraint_name", "a"."attname" AS "column_name", ` + - `CASE "ix"."indisunique" WHEN 't' THEN 'TRUE' ELSE'FALSE' END AS "is_unique", pg_get_expr("ix"."indpred", "ix"."indrelid") AS "condition" ` + + `CASE "ix"."indisunique" WHEN 't' THEN 'TRUE' ELSE'FALSE' END AS "is_unique", pg_get_expr("ix"."indpred", "ix"."indrelid") AS "condition", ` + + `"types"."typname" AS "type_name" ` + `FROM "pg_class" "t" ` + `INNER JOIN "pg_index" "ix" ON "ix"."indrelid" = "t"."oid" ` + `INNER JOIN "pg_attribute" "a" ON "a"."attrelid" = "t"."oid" AND "a"."attnum" = ANY ("ix"."indkey") ` + `INNER JOIN "pg_namespace" "ns" ON "ns"."oid" = "t"."relnamespace" ` + `INNER JOIN "pg_class" "i" ON "i"."oid" = "ix"."indexrelid" ` + + `INNER JOIN "pg_type" "types" ON "types"."oid" = "a"."atttypid" ` + `LEFT JOIN "pg_constraint" "cnst" ON "cnst"."conname" = "i"."relname" ` + `WHERE "t"."relkind" = 'r' AND "cnst"."contype" IS NULL AND (${constraintsCondition})`; @@ -1450,7 +1454,7 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner columnNames: indices.map(i => i["column_name"]), isUnique: constraint["is_unique"] === "TRUE", where: constraint["condition"], - isSpatial: false, + isSpatial: indices.every(i => this.driver.spatialTypes.indexOf(i["type_name"]) >= 0), isFulltext: false }); }); @@ -1591,7 +1595,7 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner */ protected createIndexSql(table: Table, index: TableIndex): string { const columns = index.columnNames.map(columnName => `"${columnName}"`).join(", "); - return `CREATE ${index.isUnique ? "UNIQUE " : ""}INDEX "${index.name}" ON ${this.escapeTableName(table)}(${columns}) ${index.where ? "WHERE " + index.where : ""}`; + return `CREATE ${index.isUnique ? "UNIQUE " : ""}INDEX "${index.name}" ON ${this.escapeTableName(table)} ${index.isSpatial ? "USING GiST " : ""} (${columns}) ${index.where ? "WHERE " + index.where : ""}`; } /** diff --git a/src/entity-schema/EntitySchemaIndexOptions.ts b/src/entity-schema/EntitySchemaIndexOptions.ts index 5616a0078e..7079eb6187 100644 --- a/src/entity-schema/EntitySchemaIndexOptions.ts +++ b/src/entity-schema/EntitySchemaIndexOptions.ts @@ -29,7 +29,7 @@ export interface EntitySchemaIndexOptions { /** * The SPATIAL modifier indexes the entire column and does not allow indexed columns to contain NULL values. - * Works only in MySQL. + * Works only in MySQL and PostgreSQL. */ spatial?: boolean; diff --git a/src/query-builder/InsertQueryBuilder.ts b/src/query-builder/InsertQueryBuilder.ts index 1b4990a7ff..88ae576a9c 100644 --- a/src/query-builder/InsertQueryBuilder.ts +++ b/src/query-builder/InsertQueryBuilder.ts @@ -418,6 +418,8 @@ export class InsertQueryBuilder extends QueryBuilder { this.expressionMap.nativeParameters[paramName] = value; if (this.connection.driver instanceof MysqlDriver && this.connection.driver.spatialTypes.indexOf(column.type) !== -1) { expression += `GeomFromText(${this.connection.driver.createParameter(paramName, parametersCount)})`; + } else if (this.connection.driver instanceof PostgresDriver && this.connection.driver.spatialTypes.indexOf(column.type) !== -1) { + expression += `ST_GeomFromGeoJSON(${this.connection.driver.createParameter(paramName, parametersCount)})::${column.type}`; } else { expression += this.connection.driver.createParameter(paramName, parametersCount); } diff --git a/src/query-builder/SelectQueryBuilder.ts b/src/query-builder/SelectQueryBuilder.ts index 41d4f1c46c..112127ef6f 100644 --- a/src/query-builder/SelectQueryBuilder.ts +++ b/src/query-builder/SelectQueryBuilder.ts @@ -1661,6 +1661,10 @@ export class SelectQueryBuilder extends QueryBuilder implements if (this.connection.driver instanceof MysqlDriver) selectionPath = `AsText(${selectionPath})`; + if (this.connection.driver instanceof PostgresDriver) + // cast to JSON to trigger parsing in the driver + selectionPath = `ST_AsGeoJSON(${selectionPath})::json`; + if (this.connection.driver instanceof SqlServerDriver) selectionPath = `${selectionPath}.ToString()`; } diff --git a/test/functional/spatial/postgres/entity/Post.ts b/test/functional/spatial/postgres/entity/Post.ts new file mode 100644 index 0000000000..b7313da262 --- /dev/null +++ b/test/functional/spatial/postgres/entity/Post.ts @@ -0,0 +1,24 @@ +import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn"; +import {Entity} from "../../../../../src/decorator/entity/Entity"; +import {Column} from "../../../../../src/decorator/columns/Column"; +import {Index} from "../../../../../src/decorator/Index"; + +@Entity() +export class Post { + + @PrimaryGeneratedColumn() + id: number; + + @Column("geometry", { + nullable: true + }) + @Index({ + spatial: true + }) + geom: object; + + @Column("geography", { + nullable: true + }) + geog: object; +} diff --git a/test/functional/spatial/postgres/spatial-postgres.ts b/test/functional/spatial/postgres/spatial-postgres.ts new file mode 100644 index 0000000000..dc093e3ef3 --- /dev/null +++ b/test/functional/spatial/postgres/spatial-postgres.ts @@ -0,0 +1,119 @@ +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("spatial-postgres", () => { + let connections: Connection[]; + before(async () => { + connections = await createTestingConnections({ + entities: [__dirname + "/entity/*{.js,.ts}"], + enabledDrivers: ["postgres"] + }); + }); + beforeEach(async () => { + try { + await reloadTestingDatabases(connections); + } catch (err) { + console.warn(err.stack); + throw err; + } + }); + after(async () => { + try { + await closeTestingConnections(connections); + } catch (err) { + console.warn(err.stack); + throw err; + } + }); + + it("should create correct schema with Postgres' geometry 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.empty; + expect( + schema!.columns.find( + tableColumn => + tableColumn.name === "geom" && tableColumn.type === "geometry" + ) + ).to.not.be.empty; + }) + )); + + it("should create correct schema with Postgres' geography 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.empty; + expect( + schema!.columns.find( + tableColumn => + tableColumn.name === "geog" && tableColumn.type === "geography" + ) + ).to.not.be.empty; + }) + )); + + it("should create correct schema with Postgres' geometry indices", () => + Promise.all( + connections.map(async connection => { + const queryRunner = connection.createQueryRunner(); + const schema = await queryRunner.getTable("post"); + await queryRunner.release(); + expect(schema).not.to.be.empty; + expect( + schema!.indices.find( + tableIndex => + tableIndex.isSpatial === true && + tableIndex.columnNames.length === 1 && + tableIndex.columnNames[0] === "geom" + ) + ).to.not.be.empty; + }) + )); + + it("should persist geometry correctly", () => + Promise.all( + connections.map(async connection => { + const geom = { + type: "Point", + coordinates: [0, 0] + }; + const recordRepo = connection.getRepository(Post); + const post = new Post(); + post.geom = geom; + const persistedPost = await recordRepo.save(post); + const foundPost = await recordRepo.findOne(persistedPost.id); + expect(foundPost).to.exist; + expect(foundPost!.geom).to.deep.equal(geom); + }) + )); + + it("should persist geography correctly", () => + Promise.all( + connections.map(async connection => { + const geom = { + type: "Point", + coordinates: [0, 0] + }; + const recordRepo = connection.getRepository(Post); + const post = new Post(); + post.geog = geom; + const persistedPost = await recordRepo.save(post); + const foundPost = await recordRepo.findOne(persistedPost.id); + expect(foundPost).to.exist; + expect(foundPost!.geog).to.deep.equal(geom); + }) + )); +}); From b371c067dccdb0e262b779d0ae95e97537733e39 Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Fri, 29 Jun 2018 09:30:06 -0700 Subject: [PATCH 02/17] Feature type validation + SRID support Specifying a feature type will pass it as a parameter to the underlying spatial type, allowing PostGIS to validate that stored geometries match expectations. Specifying an SRID will do similarly, with the additional benefit of allowing the coordinate system used for a geometry to be stored in order to transform it into other coordinate systems. --- src/decorator/columns/Column.ts | 9 ++++++++- src/decorator/options/ColumnOptions.ts | 11 ++++++++++- src/driver/postgres/PostgresDriver.ts | 8 ++++++++ src/driver/types/ColumnTypes.ts | 7 +++++++ src/metadata/ColumnMetadata.ts | 7 +++++++ src/query-builder/InsertQueryBuilder.ts | 6 +++++- src/schema-builder/options/TableColumnOptions.ts | 12 +++++++++++- src/schema-builder/table/TableColumn.ts | 16 +++++++++++++++- test/functional/spatial/postgres/entity/Post.ts | 13 +++++++++++++ 9 files changed, 84 insertions(+), 5 deletions(-) diff --git a/src/decorator/columns/Column.ts b/src/decorator/columns/Column.ts index 49ea39539a..bfa32ef881 100644 --- a/src/decorator/columns/Column.ts +++ b/src/decorator/columns/Column.ts @@ -1,10 +1,11 @@ import {ColumnOptions, getMetadataArgsStorage} from "../../"; import { - ColumnType, SimpleColumnType, WithLengthColumnType, + ColumnType, SimpleColumnType, SpatialColumnType, WithLengthColumnType, WithPrecisionColumnType, WithWidthColumnType } from "../../driver/types/ColumnTypes"; import {ColumnMetadataArgs} from "../../metadata-args/ColumnMetadataArgs"; import {ColumnCommonOptions} from "../options/ColumnCommonOptions"; +import {SpatialColumnOptions} from "../options/SpatialColumnOptions"; import {ColumnWithLengthOptions} from "../options/ColumnWithLengthOptions"; import {ColumnNumericOptions} from "../options/ColumnNumericOptions"; import {ColumnEnumOptions} from "../options/ColumnEnumOptions"; @@ -32,6 +33,12 @@ export function Column(options: ColumnOptions): Function; */ export function Column(type: SimpleColumnType, options?: ColumnCommonOptions): Function; +/** + * Column decorator is used to mark a specific class property as a table column. + * Only properties decorated with this decorator will be persisted to the database when entity be saved. + */ +export function Column(type: SpatialColumnType, options?: ColumnCommonOptions & SpatialColumnOptions): Function; + /** * Column decorator is used to mark a specific class property as a table column. * Only properties decorated with this decorator will be persisted to the database when entity be saved. diff --git a/src/decorator/options/ColumnOptions.ts b/src/decorator/options/ColumnOptions.ts index 0876d0e63d..e3f1ba624b 100644 --- a/src/decorator/options/ColumnOptions.ts +++ b/src/decorator/options/ColumnOptions.ts @@ -139,5 +139,14 @@ export interface ColumnOptions { * this column when reading or writing to the database. */ transformer?: ValueTransformer; - + + /** + * Spatial Feature Type (Geometry, Point, Polygon, etc.) + */ + spatialFeatureType?: string; + + /** + * SRID (Spatial Reference ID (EPSG code)) + */ + srid?: number; } diff --git a/src/driver/postgres/PostgresDriver.ts b/src/driver/postgres/PostgresDriver.ts index 4184f480ae..fb952c218a 100644 --- a/src/driver/postgres/PostgresDriver.ts +++ b/src/driver/postgres/PostgresDriver.ts @@ -636,6 +636,14 @@ export class PostgresDriver implements Driver { } else if (column.type === "timestamp with time zone") { type = "TIMESTAMP" + (column.precision !== null && column.precision !== undefined ? "(" + column.precision + ")" : "") + " WITH TIME ZONE"; + } else if (this.spatialTypes.indexOf(column.type as ColumnType) >= 0) { + if (column.spatialFeatureType != null && column.srid != null) { + type = `${column.type}(${column.spatialFeatureType},${column.srid})`; + } else if (column.spatialFeatureType != null) { + type = `${column.type}(${column.spatialFeatureType})`; + } else { + type = column.type; + } } if (column.isArray) diff --git a/src/driver/types/ColumnTypes.ts b/src/driver/types/ColumnTypes.ts index fbc0cea419..a8e65b9b60 100644 --- a/src/driver/types/ColumnTypes.ts +++ b/src/driver/types/ColumnTypes.ts @@ -16,6 +16,12 @@ export type PrimaryGeneratedColumnType = "int" // mysql, mssql, oracle, sqlite |"numeric" // postgres, mssql, sqlite |"number"; // oracle +/** + * Column types where spatial properties are used. + */ +export type SpatialColumnType = "geometry" // postgres + |"geography"; // postgres + /** * Column types where precision and scale properties are used. */ @@ -171,6 +177,7 @@ export type SimpleColumnType = export type ColumnType = WithPrecisionColumnType |WithLengthColumnType |WithWidthColumnType + |SpatialColumnType |SimpleColumnType |BooleanConstructor |DateConstructor diff --git a/src/metadata/ColumnMetadata.ts b/src/metadata/ColumnMetadata.ts index 3064d8f35a..d90fdca403 100644 --- a/src/metadata/ColumnMetadata.ts +++ b/src/metadata/ColumnMetadata.ts @@ -278,6 +278,11 @@ export class ColumnMetadata { */ isMaterializedPath: boolean = false; + /** + * SRID (Spatial Reference ID (EPSG code)) + */ + srid?: number; + // --------------------------------------------------------------------- // Constructor // --------------------------------------------------------------------- @@ -366,6 +371,8 @@ export class ColumnMetadata { } if (options.args.options.transformer) this.transformer = options.args.options.transformer; + if (options.args.options.srid) + this.srid = options.args.options.srid; if (this.isTreeLevel) this.type = options.connection.driver.mappedDataTypes.treeLevel; if (this.isCreateDate) { diff --git a/src/query-builder/InsertQueryBuilder.ts b/src/query-builder/InsertQueryBuilder.ts index 88ae576a9c..f8d10b8fbe 100644 --- a/src/query-builder/InsertQueryBuilder.ts +++ b/src/query-builder/InsertQueryBuilder.ts @@ -419,7 +419,11 @@ export class InsertQueryBuilder extends QueryBuilder { if (this.connection.driver instanceof MysqlDriver && this.connection.driver.spatialTypes.indexOf(column.type) !== -1) { expression += `GeomFromText(${this.connection.driver.createParameter(paramName, parametersCount)})`; } else if (this.connection.driver instanceof PostgresDriver && this.connection.driver.spatialTypes.indexOf(column.type) !== -1) { - expression += `ST_GeomFromGeoJSON(${this.connection.driver.createParameter(paramName, parametersCount)})::${column.type}`; + if (column.srid != null) { + expression += `ST_SetSRID(ST_GeomFromGeoJSON(${this.connection.driver.createParameter(paramName, parametersCount)}), ${column.srid})::${column.type}`; + } else { + expression += `ST_GeomFromGeoJSON(${this.connection.driver.createParameter(paramName, parametersCount)})::${column.type}`; + } } else { expression += this.connection.driver.createParameter(paramName, parametersCount); } diff --git a/src/schema-builder/options/TableColumnOptions.ts b/src/schema-builder/options/TableColumnOptions.ts index f8aff7a98c..fd3b76d2e6 100644 --- a/src/schema-builder/options/TableColumnOptions.ts +++ b/src/schema-builder/options/TableColumnOptions.ts @@ -122,4 +122,14 @@ export interface TableColumnOptions { * Generated column type. Supports only in MySQL. */ generatedType?: "VIRTUAL"|"STORED"; -} \ No newline at end of file + + /** + * Spatial Feature Type (Geometry, Point, Polygon, etc.) + */ + spatialFeatureType?: string; + + /** + * SRID (Spatial Reference ID (EPSG code)) + */ + srid?: number; +} diff --git a/src/schema-builder/table/TableColumn.ts b/src/schema-builder/table/TableColumn.ts index 985a5f0634..a67a525899 100644 --- a/src/schema-builder/table/TableColumn.ts +++ b/src/schema-builder/table/TableColumn.ts @@ -124,6 +124,16 @@ export class TableColumn { */ generatedType?: "VIRTUAL"|"STORED"; + /** + * Spatial Feature Type (Geometry, Point, Polygon, etc.) + */ + spatialFeatureType?: string; + + /** + * SRID (Spatial Reference ID (EPSG code)) + */ + srid?: number; + // ------------------------------------------------------------------------- // Constructor // ------------------------------------------------------------------------- @@ -152,6 +162,8 @@ export class TableColumn { this.enum = options.enum; this.asExpression = options.asExpression; this.generatedType = options.generatedType; + this.spatialFeatureType = options.spatialFeatureType; + this.srid = options.srid; } } @@ -185,7 +197,9 @@ export class TableColumn { isPrimary: this.isPrimary, isUnique: this.isUnique, isArray: this.isArray, - comment: this.comment + comment: this.comment, + spatialFeatureType: this.spatialFeatureType, + srid: this.srid }); } diff --git a/test/functional/spatial/postgres/entity/Post.ts b/test/functional/spatial/postgres/entity/Post.ts index b7313da262..c77c5c1eb8 100644 --- a/test/functional/spatial/postgres/entity/Post.ts +++ b/test/functional/spatial/postgres/entity/Post.ts @@ -17,6 +17,19 @@ export class Post { }) geom: object; + @Column("geometry", { + nullable: true, + spatialFeatureType: "Point" + }) + pointWithoutSRID: object; + + @Column("geometry", { + nullable: true, + spatialFeatureType: "Point", + srid: 4326 + }) + point: object; + @Column("geography", { nullable: true }) From a266a5a1ce6d80a72d120ae4823fe95e7f48ac0e Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Fri, 29 Jun 2018 11:43:02 -0700 Subject: [PATCH 03/17] Add missing interface --- src/decorator/options/SpatialColumnOptions.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/decorator/options/SpatialColumnOptions.ts diff --git a/src/decorator/options/SpatialColumnOptions.ts b/src/decorator/options/SpatialColumnOptions.ts new file mode 100644 index 0000000000..e46ecadc94 --- /dev/null +++ b/src/decorator/options/SpatialColumnOptions.ts @@ -0,0 +1,18 @@ +/** + * Options for spatial columns. + */ +export interface SpatialColumnOptions { + + /** + * Column type's feature type. + * Geometry, Point, Polygon, etc. + */ + spatialFeatureType?: string; + + /** + * Column type's SRID. + * Spatial Reference ID or EPSG code. + */ + srid?: number; + +} From c51dfcccf89f2cd972ef2e48e8db247636152e01 Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Fri, 29 Jun 2018 16:52:07 -0700 Subject: [PATCH 04/17] Use this.setParameter() internally This way, if transforms are applied to parameters, they only need to be implemented in 1 place. --- src/query-builder/QueryBuilder.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/query-builder/QueryBuilder.ts b/src/query-builder/QueryBuilder.ts index eeacde757b..2d1146c0a5 100644 --- a/src/query-builder/QueryBuilder.ts +++ b/src/query-builder/QueryBuilder.ts @@ -319,9 +319,7 @@ export abstract class QueryBuilder { if (this.expressionMap.parentQueryBuilder) this.expressionMap.parentQueryBuilder.setParameters(parameters); - Object.keys(parameters).forEach(key => { - this.expressionMap.parameters[key] = parameters[key]; - }); + Object.keys(parameters).forEach(key => this.setParameter(key, parameters[key])); return this; } From cfbea1324be738031c0e848b5810c5c41ec1a8e4 Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Fri, 29 Jun 2018 16:53:26 -0700 Subject: [PATCH 05/17] Demonstrate parameter-bound WHERE and ORDER BY --- .../query-builder/order-by/entity/Post.ts | 7 ++- .../spatial/postgres/spatial-postgres.ts | 56 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/test/functional/query-builder/order-by/entity/Post.ts b/test/functional/query-builder/order-by/entity/Post.ts index 78be00a19e..f13c611792 100644 --- a/test/functional/query-builder/order-by/entity/Post.ts +++ b/test/functional/query-builder/order-by/entity/Post.ts @@ -1,6 +1,7 @@ import {Entity} from "../../../../../src/decorator/entity/Entity"; import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn"; import {Column} from "../../../../../src/decorator/columns/Column"; +import {Index} from "../../../../../src"; @Entity({ orderBy: { @@ -21,4 +22,8 @@ export class Post { @Column() num2: number = 1; -} \ No newline at end of file + @Column({ type: "geometry" }) + @Index({ spatial: true }) + geom: object; + +} diff --git a/test/functional/spatial/postgres/spatial-postgres.ts b/test/functional/spatial/postgres/spatial-postgres.ts index dc093e3ef3..8ba77662aa 100644 --- a/test/functional/spatial/postgres/spatial-postgres.ts +++ b/test/functional/spatial/postgres/spatial-postgres.ts @@ -116,4 +116,60 @@ describe("spatial-postgres", () => { expect(foundPost!.geog).to.deep.equal(geom); }) )); + + it("should be able to order geometries by distance", () => Promise.all(connections.map(async connection => { + + const geoJson1 = { + type: "Point", + coordinates: [ + 139.9341032213472, + 36.80798008559315 + ] + }; + + const geoJson2 = { + type: "Point", + coordinates: [ + 139.933053, + 36.805711 + ] + }; + + const origin = { + type: "Point", + coordinates: [ + 139.933227, + 36.808005 + ] + }; + + const post1 = new Post(); + post1.geom = geoJson1; + + const post2 = new Post(); + post2.geom = geoJson2; + await connection.manager.save([post1, post2]); + + const posts1 = await connection.manager + .createQueryBuilder(Post, "post") + .where("ST_Distance(post.geom, ST_GeomFromGeoJSON(:origin)) > 0") + .orderBy({ + "ST_Distance(post.geom, ST_GeomFromGeoJSON(:origin))": { + order: "ASC", + nulls: "NULLS FIRST" + } + }) + .setParameters({ origin: JSON.stringify(origin) }) + .getMany(); + + const posts2 = await connection.manager + .createQueryBuilder(Post, "post") + .orderBy("ST_Distance(post.geom, ST_GeomFromGeoJSON(:origin))", "DESC") + .setParameters({ origin: JSON.stringify(origin) }) + .getMany(); + + expect(posts1[0].id).to.be.equal(post1.id); + expect(posts2[0].id).to.be.equal(post2.id); + }))); + }); From 84e9386fe1e2e0c7707cfa0accbe6c0fe6e857ee Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Sun, 1 Jul 2018 13:24:12 -0700 Subject: [PATCH 06/17] Use functions when updating spatial columns --- src/query-builder/UpdateQueryBuilder.ts | 14 ++++- .../spatial/postgres/spatial-postgres.ts | 53 +++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/query-builder/UpdateQueryBuilder.ts b/src/query-builder/UpdateQueryBuilder.ts index ac71e1a605..4e079a0eee 100644 --- a/src/query-builder/UpdateQueryBuilder.ts +++ b/src/query-builder/UpdateQueryBuilder.ts @@ -393,7 +393,19 @@ export class UpdateQueryBuilder extends QueryBuilder implements this.expressionMap.nativeParameters[paramName] = value; } - updateColumnAndValues.push(this.escape(column.databaseName) + " = " + this.connection.driver.createParameter(paramName, parametersCount)); + let expression = null; + if (this.connection.driver instanceof MysqlDriver && this.connection.driver.spatialTypes.indexOf(column.type) !== -1) { + expression = `GeomFromText(${this.connection.driver.createParameter(paramName, parametersCount)})`; + } else if (this.connection.driver instanceof PostgresDriver && this.connection.driver.spatialTypes.indexOf(column.type) !== -1) { + if (column.srid != null) { + expression = `ST_SetSRID(ST_GeomFromGeoJSON(${this.connection.driver.createParameter(paramName, parametersCount)}), ${column.srid})::${column.type}`; + } else { + expression = `ST_GeomFromGeoJSON(${this.connection.driver.createParameter(paramName, parametersCount)})::${column.type}`; + } + } else { + expression = this.connection.driver.createParameter(paramName, parametersCount); + } + updateColumnAndValues.push(this.escape(column.databaseName) + " = " + expression); parametersCount++; } }); diff --git a/test/functional/spatial/postgres/spatial-postgres.ts b/test/functional/spatial/postgres/spatial-postgres.ts index 8ba77662aa..ed0858a8be 100644 --- a/test/functional/spatial/postgres/spatial-postgres.ts +++ b/test/functional/spatial/postgres/spatial-postgres.ts @@ -117,6 +117,59 @@ describe("spatial-postgres", () => { }) )); + it("should update geometry correctly", () => + Promise.all( + connections.map(async connection => { + const geom = { + type: "Point", + coordinates: [0, 0] + }; + const geom2 = { + type: "Point", + coordinates: [45, 45] + }; + const recordRepo = connection.getRepository(Post); + const post = new Post(); + post.geom = geom; + const persistedPost = await recordRepo.save(post); + + await recordRepo.update({ + id: persistedPost.id + }, { + geom: geom2 + }); + + const foundPost = await recordRepo.findOne(persistedPost.id); + expect(foundPost).to.exist; + expect(foundPost!.geom).to.deep.equal(geom2); + }) + )); + + it("should re-save geometry correctly", () => + Promise.all( + connections.map(async connection => { + const geom = { + type: "Point", + coordinates: [0, 0] + }; + const geom2 = { + type: "Point", + coordinates: [45, 45] + }; + const recordRepo = connection.getRepository(Post); + const post = new Post(); + post.geom = geom; + const persistedPost = await recordRepo.save(post); + + persistedPost.geom = geom2; + await recordRepo.save(persistedPost); + + const foundPost = await recordRepo.findOne(persistedPost.id); + expect(foundPost).to.exist; + expect(foundPost!.geom).to.deep.equal(geom2); + }) + )); + it("should be able to order geometries by distance", () => Promise.all(connections.map(async connection => { const geoJson1 = { From 0d8b343513759859c233e3bf122d0245435d6f9c Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Sun, 1 Jul 2018 13:44:00 -0700 Subject: [PATCH 07/17] Correctly populate feature type + SRID constraints --- src/driver/postgres/PostgresQueryRunner.ts | 6 ++++++ src/metadata/ColumnMetadata.ts | 7 +++++++ src/schema-builder/util/TableUtils.ts | 4 +++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/driver/postgres/PostgresQueryRunner.ts b/src/driver/postgres/PostgresQueryRunner.ts index b5bddb0879..a320fa48b9 100644 --- a/src/driver/postgres/PostgresQueryRunner.ts +++ b/src/driver/postgres/PostgresQueryRunner.ts @@ -812,6 +812,12 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner downQueries.push(`ALTER TABLE ${this.escapeTableName(table)} ALTER COLUMN "${newColumn.name}" SET DEFAULT ${oldColumn.default}`); } } + + if (newColumn.spatialFeatureType !== oldColumn.spatialFeatureType || newColumn.srid !== oldColumn.srid) { + upQueries.push(`ALTER TABLE ${this.escapeTableName(table)} ALTER COLUMN "${newColumn.name}" TYPE ${this.driver.createFullType(newColumn)}`); + downQueries.push(`ALTER TABLE ${this.escapeTableName(table)} ALTER COLUMN "${newColumn.name}" TYPE ${this.driver.createFullType(oldColumn)}`); + } + } await this.executeQueries(upQueries, downQueries); diff --git a/src/metadata/ColumnMetadata.ts b/src/metadata/ColumnMetadata.ts index d90fdca403..a5f2191bb8 100644 --- a/src/metadata/ColumnMetadata.ts +++ b/src/metadata/ColumnMetadata.ts @@ -278,6 +278,11 @@ export class ColumnMetadata { */ isMaterializedPath: boolean = false; + /** + * Spatial Feature Type (Geometry, Point, Polygon, etc.) + */ + spatialFeatureType?: string; + /** * SRID (Spatial Reference ID (EPSG code)) */ @@ -371,6 +376,8 @@ export class ColumnMetadata { } if (options.args.options.transformer) this.transformer = options.args.options.transformer; + if (options.args.options.spatialFeatureType) + this.spatialFeatureType = options.args.options.spatialFeatureType; if (options.args.options.srid) this.srid = options.args.options.srid; if (this.isTreeLevel) diff --git a/src/schema-builder/util/TableUtils.ts b/src/schema-builder/util/TableUtils.ts index 7a7d9c7a3d..f419d07e67 100644 --- a/src/schema-builder/util/TableUtils.ts +++ b/src/schema-builder/util/TableUtils.ts @@ -27,7 +27,9 @@ export class TableUtils { isPrimary: columnMetadata.isPrimary, isUnique: driver.normalizeIsUnique(columnMetadata), isArray: columnMetadata.isArray || false, - enum: columnMetadata.enum + enum: columnMetadata.enum, + spatialFeatureType: columnMetadata.spatialFeatureType, + srid: columnMetadata.srid }; } From ce9429521fe96be8e35992dd770f2d926fb1045a Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Sun, 1 Jul 2018 21:39:54 -0700 Subject: [PATCH 08/17] Load SRID + feature type from the db This allows synchronization to modify PostGIS constraints while also surfacing metadata in expected places. --- src/driver/postgres/PostgresQueryRunner.ts | 36 ++++++++++++++++++- .../spatial/postgres/spatial-postgres.ts | 9 ++--- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/driver/postgres/PostgresQueryRunner.ts b/src/driver/postgres/PostgresQueryRunner.ts index a320fa48b9..0868236c11 100644 --- a/src/driver/postgres/PostgresQueryRunner.ts +++ b/src/driver/postgres/PostgresQueryRunner.ts @@ -813,7 +813,7 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner } } - if (newColumn.spatialFeatureType !== oldColumn.spatialFeatureType || newColumn.srid !== oldColumn.srid) { + if ((newColumn.spatialFeatureType || "").toLowerCase() !== (oldColumn.spatialFeatureType || "").toLowerCase() || newColumn.srid !== oldColumn.srid) { upQueries.push(`ALTER TABLE ${this.escapeTableName(table)} ALTER COLUMN "${newColumn.name}" TYPE ${this.driver.createFullType(newColumn)}`); downQueries.push(`ALTER TABLE ${this.escapeTableName(table)} ALTER COLUMN "${newColumn.name}" TYPE ${this.driver.createFullType(oldColumn)}`); } @@ -1358,6 +1358,40 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner tableColumn.enum = results.map(result => result["value"]); } + if (tableColumn.type === "geometry") { + const geometryColumnSql = `SELECT * FROM ( + SELECT + f_table_schema table_schema, + f_table_name table_name, + f_geometry_column column_name, + srid, + type + FROM geometry_columns + ) AS _ + WHERE ${tablesCondition} AND column_name = '${tableColumn.name}'`; + + const results: ObjectLiteral[] = await this.query(geometryColumnSql); + tableColumn.spatialFeatureType = results[0].type; + tableColumn.srid = results[0].srid; + } + + if (tableColumn.type === "geography") { + const geographyColumnSql = `SELECT * FROM ( + SELECT + f_table_schema table_schema, + f_table_name table_name, + f_geography_column column_name, + srid, + type + FROM geography_columns + ) AS _ + WHERE ${tablesCondition} AND column_name = '${tableColumn.name}'`; + + const results: ObjectLiteral[] = await this.query(geographyColumnSql); + tableColumn.spatialFeatureType = results[0].type; + tableColumn.srid = results[0].srid; + } + // check only columns that have length property if (this.driver.withLengthColumnTypes.indexOf(tableColumn.type as ColumnType) !== -1 && dbColumn["character_maximum_length"]) { const length = dbColumn["character_maximum_length"].toString(); diff --git a/test/functional/spatial/postgres/spatial-postgres.ts b/test/functional/spatial/postgres/spatial-postgres.ts index ed0858a8be..5d35323575 100644 --- a/test/functional/spatial/postgres/spatial-postgres.ts +++ b/test/functional/spatial/postgres/spatial-postgres.ts @@ -40,12 +40,13 @@ describe("spatial-postgres", () => { const schema = await queryRunner.getTable("post"); await queryRunner.release(); expect(schema).not.to.be.empty; - expect( - schema!.columns.find( + const pointColumn = schema!.columns.find( tableColumn => - tableColumn.name === "geom" && tableColumn.type === "geometry" + tableColumn.name === "point" && tableColumn.type === "geometry" ) - ).to.not.be.empty; + expect(pointColumn).to.not.be.empty; + expect(pointColumn!.spatialFeatureType!.toLowerCase()).to.equal("point"); + expect(pointColumn!.srid).to.equal(4326); }) )); From 430b8cd2d954a5e4a9ecf262cfe2c9642bc7e24a Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Sun, 1 Jul 2018 22:09:18 -0700 Subject: [PATCH 09/17] Detect spatial feature type + SRID changes --- src/driver/postgres/PostgresDriver.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/driver/postgres/PostgresDriver.ts b/src/driver/postgres/PostgresDriver.ts index fb952c218a..8096379d3d 100644 --- a/src/driver/postgres/PostgresDriver.ts +++ b/src/driver/postgres/PostgresDriver.ts @@ -721,7 +721,9 @@ export class PostgresDriver implements Driver { || tableColumn.isNullable !== columnMetadata.isNullable || tableColumn.isUnique !== this.normalizeIsUnique(columnMetadata) || (tableColumn.enum && columnMetadata.enum && !OrmUtils.isArraysEqual(tableColumn.enum, columnMetadata.enum)) - || tableColumn.isGenerated !== columnMetadata.isGenerated; + || tableColumn.isGenerated !== columnMetadata.isGenerated + || (tableColumn.spatialFeatureType || "").toLowerCase() !== (columnMetadata.spatialFeatureType || "").toLowerCase() + || tableColumn.srid !== columnMetadata.srid; }); } From f935457c0238183c01611b9d1a8069257e4a6e98 Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Sun, 1 Jul 2018 22:27:44 -0700 Subject: [PATCH 10/17] Remove unused geometry column Since the order-by tests run for multiple database engines, including Postgres-targeted spatial types will break other engines (or cause them to hang on startup). --- test/functional/query-builder/order-by/entity/Post.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/functional/query-builder/order-by/entity/Post.ts b/test/functional/query-builder/order-by/entity/Post.ts index f13c611792..fd238ccc64 100644 --- a/test/functional/query-builder/order-by/entity/Post.ts +++ b/test/functional/query-builder/order-by/entity/Post.ts @@ -1,7 +1,6 @@ import {Entity} from "../../../../../src/decorator/entity/Entity"; import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn"; import {Column} from "../../../../../src/decorator/columns/Column"; -import {Index} from "../../../../../src"; @Entity({ orderBy: { @@ -22,8 +21,4 @@ export class Post { @Column() num2: number = 1; - @Column({ type: "geometry" }) - @Index({ spatial: true }) - geom: object; - } From 61f64d2f5d63e95b389a4d4e5c7f690f348258a7 Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Mon, 2 Jul 2018 12:06:39 -0700 Subject: [PATCH 11/17] Improve readability --- src/driver/postgres/PostgresDriver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/driver/postgres/PostgresDriver.ts b/src/driver/postgres/PostgresDriver.ts index 8096379d3d..cc329ab227 100644 --- a/src/driver/postgres/PostgresDriver.ts +++ b/src/driver/postgres/PostgresDriver.ts @@ -384,7 +384,7 @@ export class PostgresDriver implements Driver { || columnMetadata.type === "timestamp without time zone") { return DateUtils.mixedDateToDate(value); - } else if (this.spatialTypes.concat(["json", "jsonb"]).indexOf(columnMetadata.type) >= 0) { + } else if (["json", "jsonb", ...this.spatialTypes].indexOf(columnMetadata.type) >= 0) { return JSON.stringify(value); } else if (columnMetadata.type === "hstore") { From 7893c66a366549d02a71f2052af39b70033fa8f6 Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Mon, 2 Jul 2018 12:08:29 -0700 Subject: [PATCH 12/17] Explanation of / apology for mdillon/postgis --- docker-compose.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 45e981947d..83c34301b3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,6 +39,9 @@ services: # postgres postgres: + # mdillon/postgis is postgres + PostGIS (only). if you need additional + # extensions, it's probably time to create a purpose-built image with all + # necessary extensions. sorry, and thanks for adding support for them! image: "mdillon/postgis:9.6" container_name: "typeorm-postgres" ports: From 4a5c7ecf6c09f7174eb55ae1b35d4e30a34021fa Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Tue, 3 Jul 2018 09:59:56 -0700 Subject: [PATCH 13/17] Document spatial indices --- docs/indices.md | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/docs/indices.md b/docs/indices.md index 1dfdad3194..0eec0fe6b9 100644 --- a/docs/indices.md +++ b/docs/indices.md @@ -98,7 +98,39 @@ export class User { @Column() lastName: string; - + +} +``` + +## Spatial Indices + +MySQL and PostgreSQL (when PostGIS is available) both support spatial indices. + +To create a spatial index on a column in MySQL, add an `Index` with `spatial: +true` on a column that uses a spatial type (`geometry`, `point`, `linestring`, +`polygon`, `multipoint`, `multilinestring`, `multipolygon`, +`geometrycollection`): + +```typescript +@Entity() +export class Thing { + @Column("point") + @Index({ spatial: true }) + point: string; +} +``` + +To create a spatial index on a column in PostgreSQL, add an `Index` with `spatial: true` on a column that uses a spatial type (`geometry`, `geography`): + +```typescript +@Entity() +export class Thing { + @Column("geometry", { + spatialFeatureType: "Point", + srid: 4326 + }) + @Index({ spatial: true }) + point: Geometry; } ``` From c8b87c84511ee12d12786dffd1b9b96d90fbc28c Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Tue, 3 Jul 2018 10:30:44 -0700 Subject: [PATCH 14/17] Add missing semicolon --- test/functional/spatial/postgres/spatial-postgres.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/spatial/postgres/spatial-postgres.ts b/test/functional/spatial/postgres/spatial-postgres.ts index 5d35323575..7322ee33dc 100644 --- a/test/functional/spatial/postgres/spatial-postgres.ts +++ b/test/functional/spatial/postgres/spatial-postgres.ts @@ -43,7 +43,7 @@ describe("spatial-postgres", () => { const pointColumn = schema!.columns.find( tableColumn => tableColumn.name === "point" && tableColumn.type === "geometry" - ) + ); expect(pointColumn).to.not.be.empty; expect(pointColumn!.spatialFeatureType!.toLowerCase()).to.equal("point"); expect(pointColumn!.srid).to.equal(4326); From e1f6aa4795e63c04b14a920d4c5c24455e213076 Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Tue, 3 Jul 2018 10:32:52 -0700 Subject: [PATCH 15/17] Doc spatial columns --- docs/decorator-reference.md | 2 ++ docs/entities.md | 55 ++++++++++++++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/docs/decorator-reference.md b/docs/decorator-reference.md index a4f7042a91..b2e71f1333 100644 --- a/docs/decorator-reference.md +++ b/docs/decorator-reference.md @@ -139,6 +139,8 @@ You can specify array of values or specify a enum class. * `hstoreType: "object"|"string"` - Return type of `HSTORE` column. Returns value as string or as object. Used only in [Postgres](https://www.postgresql.org/docs/9.6/static/hstore.html). * `array: boolean` - Used for postgres column types which can be array (for example int[]). * `transformer: ValueTransformer` - Specifies a value transformer that is to be used to (un)marshal this column when reading or writing to the database. +* `spatialFeatureType: string` - Optional feature type (`Point`, `Polygon`, `LineString`, `Geometry`) used as a constraint on a spatial column. If not specified, it will behave as though `Geometry` was provided. Used only in PostgreSQL. +* `srid: number` - Optional [Spatial Reference ID](https://postgis.net/docs/using_postgis_dbmanagement.html#spatial_ref_sys) used as a constraint on a spatial column. If not specified, it will default to `0`. Standard geographic coordinates (latitude/longitude in the WGS84 datum) correspond to [EPSG 4326](http://spatialreference.org/ref/epsg/wgs-84/). Used only in PostgreSQL. Learn more about [entity columns](entities.md#entity-columns). diff --git a/docs/entities.md b/docs/entities.md index 4efc4a2220..3f1a3862a4 100644 --- a/docs/entities.md +++ b/docs/entities.md @@ -200,6 +200,59 @@ You don't need to set this column - it will be automatically set. each time you call `save` of entity manager or repository. You don't need to set this column - it will be automatically set. +### Spatial columns + +MS SQL, MySQL / MariaDB, and PostgreSQL all support spatial columns. TypeORM's +support for each varies slightly between databases, particularly as the column +names vary between databases. + +MS SQL and MySQL / MariaDB's TypeORM support exposes (and expects) geometries to +be provided as [well-known text +(WKT)](https://en.wikipedia.org/wiki/Well-known_text), so geometry columns +should be tagged with the `string` type. + +TypeORM's PostgreSQL support uses [GeoJSON](http://geojson.org/) as an +interchange format, so geometry columns should be tagged either as `object` or +`Geometry` (or subclasses, e.g. `Point`) after importing [`geojson` +types](https://www.npmjs.com/package/@types/geojson). + +TypeORM tries to do the right thing, but it's not always possible to determine +when a value being inserted or the result of a PostGIS function should be +treated as a geometry. As a result, you may find yourself writing code similar +to the following, where values are converted into PostGIS `geometry`s from +GeoJSON and into GeoJSON as `json`: + +```typescript +const origin = { + type: "Point", + coordinates: [0, 0] +}; + +await getManager() + .createQueryBuilder(Thing, "thing") + // convert stringified GeoJSON into a geometry with an SRID that matches the + // table specification + .where("ST_Distance(geom, ST_SetSRID(ST_GeomFromGeoJSON(:origin), ST_SRID(geom))) > 0") + .orderBy({ + "ST_Distance(geom, ST_SetSRID(ST_GeomFromGeoJSON(:origin), ST_SRID(geom)))": { + order: "ASC" + } + }) + .setParameters({ + // stringify GeoJSON + origin: JSON.stringify(origin) + }) + .getMany(); + +await getManager() + .createQueryBuilder(Thing, "thing") + // convert geometry result into GeoJSON, treated as JSON (so that TypeORM + // will know to deserialize it) + .select("ST_AsGeoJSON(ST_Buffer(geom, 0.1))::json geom") + .from("thing") + .getMany(); +``` + ## Column types @@ -249,7 +302,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` +`tsrange`, `tstzrange`, `daterange`, `geometry`, `geography` ### Column types for `sqlite` / `cordova` / `react-native` From 53e2b36f1fe9157ffebf62a5ffab64eeeb15e6a5 Mon Sep 17 00:00:00 2001 From: Umed Khudoiberdiev Date: Tue, 3 Jul 2018 23:02:37 +0500 Subject: [PATCH 16/17] adding missing links --- docs/indices.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/indices.md b/docs/indices.md index 0eec0fe6b9..597455a3ed 100644 --- a/docs/indices.md +++ b/docs/indices.md @@ -3,6 +3,8 @@ * [Column indices](#column-indices) * [Unique indices](#unique-indices) * [Indices with multiple columns](#indices-with-multiple-columns) +* [Spatial Indices](#spatial-indices) +* [Disabling synchronization](#disabling-synchronization) ## Column indices From 2e069ce6a8867f4f02c07b89b7d2d447755f1883 Mon Sep 17 00:00:00 2001 From: Umed Khudoiberdiev Date: Tue, 3 Jul 2018 23:03:16 +0500 Subject: [PATCH 17/17] adding missing links --- docs/entities.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/entities.md b/docs/entities.md index 3f1a3862a4..ac19f40dfd 100644 --- a/docs/entities.md +++ b/docs/entities.md @@ -4,6 +4,7 @@ * [Entity columns](#entity-columns) * [Primary columns](#primary-columns) * [Special columns](#special-columns) + * [Spatial columns](#spatial-columns) * [Column types](#column-types) * [Column types for `mysql` / `mariadb`](#column-types-for-mysql--mariadb) * [Column types for `postgres`](#column-types-for-postgres)