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

WIP to add support for Geometry (POINT) type #197

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Sources/MySQL/Connection/MySQLData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,12 @@ extension MySQLData: CustomStringConvertible {
switch binary.type {
case .MYSQL_TYPE_VARCHAR, .MYSQL_TYPE_VAR_STRING:
return String(data: data, encoding: .utf8).flatMap { "string(\"\($0)\")" } ?? "<non-utf8 string (\(data.count))>"
case .MYSQL_TYPE_GEOMETRY:
if let geometry = try? MySQLGeometry.convertFromData(data) {
return "ST_GeomFromText(\"\(geometry.description)\"))"
} else {
return "<invalid geometry>"
}
default: return "data(0x\(data.hexEncodedString()))"
}
case .null: return "null"
Expand Down
101 changes: 101 additions & 0 deletions Sources/MySQL/Connection/MySQLGeometry.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import NIO

/// This structure is used to represent GEOMETRY data (Currently only Points).
public enum MySQLGeometry: Equatable {
/// A single 2 dimensional point
case point(x: Double, y: Double)

fileprivate var wkbType: UInt32 {
switch self {
case .point: return 1
}
}

fileprivate static let headerSize = 9
}

extension MySQLGeometry: CustomStringConvertible {
/// See `CustomStringConvertible`.
public var description: String {
switch self {
case .point(x: let x, y: let y):
return "Point(\(x) \(y))"
}
}
}

extension MySQLGeometry {
func convertToData() -> Data {
switch self {
case .point(let x, let y):
let bufferSize = MySQLGeometry.headerSize + 16 // 2 `Double`s
var buffer = ByteBufferAllocator().buffer(capacity: bufferSize)
buffer.write(integer: UInt32(0)) // 4 byte filler
buffer.write(integer: UInt8(1)) // set little endian byte order
buffer.write(integer: wkbType, endianness: .little) // geometry type
buffer.write(floatingPoint: x)
buffer.write(floatingPoint: y)
guard let data = buffer.getData(at: 0, length: bufferSize) else {
fatalError("Could not create Data from Buffer.")
}
return data
}
}

static func convertFromData(_ data: Data) throws -> MySQLGeometry {
let count = data.count
guard count >= headerSize else {
throw MySQLError(identifier: "geometryType", reason: "Not enough data to read geometry type.")
}
var buffer = ByteBufferAllocator().buffer(capacity: count)
buffer.write(bytes: data)

_ = try buffer.requireInteger(endianness: .little, as: UInt32.self)
guard try buffer.requireInteger(endianness: .little, as: UInt8.self) == 1 else {
throw MySQLError(identifier: "endianness", reason: "Expected value of `1` for endianness.")
}

let wkbType: UInt32 = try buffer.requireInteger(endianness: .little)

switch wkbType {
case 1:
return try .point(
x: buffer.requireFloatingPoint(),
y: buffer.requireFloatingPoint()
)
default:
throw MySQLError(identifier: "geometryType", reason: "Only 'Point' geometry data type is supported.")
}
}
}

extension MySQLGeometry: MySQLDataTypeStaticRepresentable {
/// See `MySQLDataTypeStaticRepresentable`.
public static var mysqlDataType: MySQLDataType {
return .geometry
}
}

extension MySQLGeometry: MySQLDataConvertible {
/// See `MySQLDataConvertible`.
public func convertToMySQLData() -> MySQLData {
let binary = MySQLBinaryData(
type: .MYSQL_TYPE_GEOMETRY,
isUnsigned: false,
storage: .string(convertToData())
)
return MySQLData(storage: .binary(binary))
}

/// See `MySQLDataConvertible`.
public static func convertFromMySQLData(_ mysqlData: MySQLData) throws -> MySQLGeometry {
switch mysqlData.storage {
case .binary(let binary):
switch binary.storage {
case .string(let data): return try .convertFromData(data)
default: throw MySQLError(identifier: "pointBinary", reason: "Parsing MySQLGeometry from \(binary) is not supported.")
}
case .text: throw MySQLError(identifier: "pointText", reason: "Parsing MySQLGeometry from text is not supported.")
}
}
}
7 changes: 7 additions & 0 deletions Sources/MySQL/SQL/MySQLDataType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,10 @@ public struct MySQLDataType: SQLDataType, Equatable {
public static var json: MySQLDataType {
return .init(.json)
}

public static var geometry: MySQLDataType {
return .init(.geometry)
}

let primitive: Primitive

Expand Down Expand Up @@ -513,6 +517,8 @@ public struct MySQLDataType: SQLDataType, Equatable {
///
/// https://dev.mysql.com/doc/refman/8.0/en/json.html
case json

case geometry
}

/// See `SQLSerializable`.
Expand Down Expand Up @@ -644,6 +650,7 @@ public struct MySQLDataType: SQLDataType, Equatable {
}
return sql.joined(separator: " ")
case .json: return "JSON"
case .geometry: return "GEOMETRY"
}
}
}
50 changes: 47 additions & 3 deletions Tests/MySQLTests/MySQLTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class MySQLTests: XCTestCase {

func testKitchenSink() throws {
/// support
struct KitechSinkColumn {
struct KitchenSinkColumn {
let name: String
let columnType: String
let data: MySQLData
Expand All @@ -69,7 +69,7 @@ class MySQLTests: XCTestCase {
}
}
}
let tests: [KitechSinkColumn] = [
let tests: [KitchenSinkColumn] = [
.init("xchar", "CHAR(60)", "hello1"),
.init("xvarchar", "VARCHAR(61)", "hello2"),
.init("xtext", "TEXT(62)", "hello3"),
Expand Down Expand Up @@ -330,6 +330,49 @@ class MySQLTests: XCTestCase {
let res2 = try conn.simpleQuery("SELECT * FROM controls WHERE user = 'foo'").wait()
XCTAssertEqual(res2.count, 0)
}

func testGeometry() throws {
struct Coordinate: Codable, Equatable, MySQLDataConvertible, MySQLDataTypeStaticRepresentable, ReflectionDecodable {
let x, y: Double
static let mysqlDataType: MySQLDataType = .geometry
static func reflectDecoded() throws -> (Coordinate, Coordinate) {
return (.init(x: 0, y: 0), .init(x: 0, y: 1))
}
func convertToMySQLData() -> MySQLData {
return MySQLGeometry.point(x: x, y: y).convertToMySQLData()
}

static func convertFromMySQLData(_ mysqlData: MySQLData) throws -> Coordinate {
switch try MySQLGeometry.convertFromMySQLData(mysqlData) {
case .point(x: let x, y: let y):
return Coordinate(x: x, y: y)
}
}
}
struct Location: SQLTable, Equatable {
var coordinate: Coordinate
}

let conn = try MySQLConnection.makeTest()
try conn.create(table: Location.self)
.column(for: \Location.coordinate)
.run()
.wait()
let location1 = Location(coordinate: .init(x: 1, y: 2))
try conn.insert(into: Location.self)
.value(location1)
.run()
.wait()
let location2 = try conn.select().all()
.from(Location.self)
.all(decoding: Location.self)
.wait()
XCTAssertEqual(location1, location2.first)

defer {
try? conn.drop(table: Location.self).ifExists().run().wait()
}
}

static let allTests = [
("testBenchmark", testBenchmark),
Expand All @@ -349,11 +392,12 @@ class MySQLTests: XCTestCase {
("testColumnAfter", testColumnAfter),
("testDecimalPrecision", testDecimalPrecision),
("testZeroRowSelect", testZeroRowSelect),
("testGeometry", testGeometry)
]
}

extension MySQLConnection {
/// Creates a test event loop and psql client.
/// Creates a test event loop and mysql client.
static func makeTest() throws -> MySQLConnection {
let transport: MySQLTransportConfig
#if SSL_TESTS
Expand Down