Skip to content

Commit

Permalink
feat: support spatial types of MySQL 8+ (#4794)
Browse files Browse the repository at this point in the history
* add `legacySpatialSupport` option for replacing
  dropped functions to new ones

* use srid column option like PostgreSQL

Close #3702
  • Loading branch information
jeiea authored and pleerock committed Nov 22, 2019
1 parent edbb80e commit 231dadf
Show file tree
Hide file tree
Showing 10 changed files with 156 additions and 5 deletions.
2 changes: 2 additions & 0 deletions docs/connection-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ Slight performance penalty for most calls. (Default: `true`)
* `multipleStatements` - Allow multiple mysql statements per query. Be careful with this, it could increase the scope
of SQL injection attacks. (Default: `false`)

* `legacySpatialSupport` - Use spatial functions like GeomFromText and AsText which are removed in MySQL 8. (Default: true)

* `flags` - List of connection flags to use other than the default ones. It is also possible to blacklist default ones.
For more information, check [Connection Flags](https://github.com/mysqljs/mysql#connection-flags).

Expand Down
2 changes: 2 additions & 0 deletions docs/zh_CN/connection-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@

- `multipleStatements` - 每个查询允许多个 mysql 语句。请注意,它可能会增加 SQL 注入攻击的范围。 (默认值:`false`

- `legacySpatialSupport` - Use spatial functions like GeomFromText and AsText which are removed in MySQL 8. (Default: true)

- `flags` - 使用非默认连接标志的连接标志列表。也可以将默认值列入黑名单。有关更多信息,请查看[Connection Flags](https://github.com/mysqljs/mysql#connection-flags)

- `ssl` - 带有 ssl 参数的对象或包含 ssl 配置文件名称的字符串。请参阅[SSL 选项](https://github.com/mysqljs/mysql#ssl-options)
Expand Down
6 changes: 6 additions & 0 deletions src/driver/aurora-data-api/AuroraDataApiConnectionOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,10 @@ export interface AuroraDataApiConnectionOptions extends BaseConnectionOptions, A
readonly resourceArn: string;

readonly database: string;

/**
* Use spatial functions like GeomFromText and AsText which are removed in MySQL 8.
* (Default: true)
*/
readonly legacySpatialSupport?: boolean;
}
6 changes: 6 additions & 0 deletions src/driver/mysql/MysqlConnectionOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ export interface MysqlConnectionOptions extends BaseConnectionOptions, MysqlConn
*/
readonly multipleStatements?: boolean;

/**
* Use spatial functions like GeomFromText and AsText which are removed in MySQL 8.
* (Default: true)
*/
readonly legacySpatialSupport?: boolean;

/**
* List of connection flags to use other than the default ones. It is also possible to blacklist default ones.
* For more information, check https://github.com/mysqljs/mysql#connection-flags.
Expand Down
5 changes: 4 additions & 1 deletion src/driver/mysql/MysqlDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,10 @@ export class MysqlDriver implements Driver {

constructor(connection: Connection) {
this.connection = connection;
this.options = connection.options as MysqlConnectionOptions;
this.options = {
legacySpatialSupport: true,
...connection.options
} as MysqlConnectionOptions;
this.isReplicated = this.options.replication ? true : false;

// load mysql package
Expand Down
8 changes: 7 additions & 1 deletion src/query-builder/InsertQueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,13 @@ export class InsertQueryBuilder<Entity> extends QueryBuilder<Entity> {

this.expressionMap.nativeParameters[paramName] = value;
if ((this.connection.driver instanceof MysqlDriver || this.connection.driver instanceof AuroraDataApiDriver) && this.connection.driver.spatialTypes.indexOf(column.type) !== -1) {
expression += `GeomFromText(${this.connection.driver.createParameter(paramName, parametersCount)})`;
const useLegacy = this.connection.driver.options.legacySpatialSupport;
const geomFromText = useLegacy ? "GeomFromText" : "ST_GeomFromText";
if (column.srid != null) {
expression += `${geomFromText}(${this.connection.driver.createParameter(paramName, parametersCount)}, ${column.srid})`;
} else {
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}`;
Expand Down
7 changes: 5 additions & 2 deletions src/query-builder/SelectQueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1681,8 +1681,11 @@ export class SelectQueryBuilder<Entity> extends QueryBuilder<Entity> implements
const selection = this.expressionMap.selects.find(select => select.selection === aliasName + "." + column.propertyPath);
let selectionPath = this.escape(aliasName) + "." + this.escape(column.databaseName);
if (this.connection.driver.spatialTypes.indexOf(column.type) !== -1) {
if (this.connection.driver instanceof MysqlDriver || this.connection.driver instanceof AuroraDataApiDriver)
selectionPath = `AsText(${selectionPath})`;
if (this.connection.driver instanceof MysqlDriver || this.connection.driver instanceof AuroraDataApiDriver) {
const useLegacy = this.connection.driver.options.legacySpatialSupport;
const asText = useLegacy ? "AsText" : "ST_AsText";
selectionPath = `${asText}(${selectionPath})`;
}

if (this.connection.driver instanceof PostgresDriver)
// cast to JSON to trigger parsing in the driver
Expand Down
8 changes: 7 additions & 1 deletion src/query-builder/UpdateQueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,13 @@ export class UpdateQueryBuilder<Entity> extends QueryBuilder<Entity> implements

let expression = null;
if ((this.connection.driver instanceof MysqlDriver || this.connection.driver instanceof AuroraDataApiDriver) && this.connection.driver.spatialTypes.indexOf(column.type) !== -1) {
expression = `GeomFromText(${this.connection.driver.createParameter(paramName, parametersCount)})`;
const useLegacy = this.connection.driver.options.legacySpatialSupport;
const geomFromText = useLegacy ? "GeomFromText" : "ST_GeomFromText";
if (column.srid != null) {
expression = `${geomFromText}(${this.connection.driver.createParameter(paramName, parametersCount)}, ${column.srid})`;
} else {
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}`;
Expand Down
13 changes: 13 additions & 0 deletions test/github-issues/3702/entity/LetterBox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {Entity, PrimaryGeneratedColumn} from "../../../../src";
import {Column} from "../../../../src/decorator/columns/Column";

@Entity()
export class LetterBox {

@PrimaryGeneratedColumn()
id: number;

@Column({ type: "point", srid: 4326 })
coord: string;

}
104 changes: 104 additions & 0 deletions test/github-issues/3702/issue-3702.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import "reflect-metadata";
import {createTestingConnections, closeTestingConnections} from "../../utils/test-utils";
import {Connection} from "../../../src/connection/Connection";
import {expect} from "chai";
import {LetterBox} from "./entity/LetterBox";

// Another related path: test/functional/spatial
describe("github issues > #3702 MySQL Spatial Type Support : GeomFromText function is not supported", () => {

describe("when legacySpatialSupport: true", () => {
let connections: Connection[];

before(async () => connections = await createTestingConnections({
entities: [__dirname + "/entity/*{.js,.ts}"],
enabledDrivers: ["mysql"],
dropSchema: true,
schemaCreate: true,
driverSpecific: {
// it's default
// legacySpatialSupport: true,
},
}));
after(() => closeTestingConnections(connections));

it("should use GeomFromText", () => Promise.all(connections.map(async connection => {
let queryBuilder = connection.createQueryBuilder().insert();
queryBuilder.into(LetterBox).values({ coord: "POINT(20 30)" });
const sql = queryBuilder.getSql();

expect(sql).includes("GeomFromText");
expect(sql).not.includes("ST_GeomFromText");

await queryBuilder.execute();
})));


it("should provide SRID", () => Promise.all(connections.map(async connection => {
let queryBuilder = connection.createQueryBuilder().insert();
queryBuilder.into(LetterBox).values({ coord: "POINT(25 100)" });
const sql = queryBuilder.getSql();

expect(sql).includes("4326");

await queryBuilder.execute();
})));

it("should use AsText", () => Promise.all(connections.map(async connection => {
const repository = connection.getRepository(LetterBox);
let queryBuilder = repository.createQueryBuilder("letterBox").select(["letterBox"]);
const sql = queryBuilder.getSql();

expect(sql).includes("AsText");
expect(sql).not.includes("ST_AsText");

await queryBuilder.getMany();
})));
});


describe("when legacySpatialSupport: false", () => {
let connections: Connection[];

before(async () => connections = await createTestingConnections({
entities: [__dirname + "/entity/*{.js,.ts}"],
enabledDrivers: ["mysql"],
dropSchema: true,
schemaCreate: true,
driverSpecific: {
legacySpatialSupport: false,
}
}));
after(() => closeTestingConnections(connections));

it("should use ST_GeomFromText", () => Promise.all(connections.map(async connection => {
let queryBuilder = connection.createQueryBuilder().insert();
queryBuilder.into(LetterBox).values({ coord: "POINT(20 30)" });
const sql = queryBuilder.getSql();

expect(sql).includes("ST_GeomFromText");

await queryBuilder.execute();
})));

it("should provide SRID", () => Promise.all(connections.map(async connection => {
let queryBuilder = connection.createQueryBuilder().insert();
queryBuilder.into(LetterBox).values({ coord: "POINT(25 100)" });
const sql = queryBuilder.getSql();

expect(sql).includes("4326");

await queryBuilder.execute();
})));

it("should use ST_AsText", () => Promise.all(connections.map(async connection => {
const repository = connection.getRepository(LetterBox);
let queryBuilder = repository.createQueryBuilder("letterBox").select(["letterBox"]);
const sql = queryBuilder.getSql();

expect(sql).includes("ST_AsText");

await queryBuilder.getMany();
})));
});
});

0 comments on commit 231dadf

Please sign in to comment.