Skip to content

Commit aa77a21

Browse files
committed
comment numeric number parser
1 parent d408f87 commit aa77a21

13 files changed

+203
-30
lines changed

Sources/PostgreSQL/Connection/PostgreSQLConnection+Query.swift

+5-7
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ extension PostgreSQLConnection {
1919
public func query(
2020
_ string: String,
2121
_ parameters: [PostgreSQLDataCustomConvertible] = [],
22+
resultFormat: PostgreSQLResultFormat = .binary(),
2223
onRow: @escaping ([String: PostgreSQLData]) -> ()
2324
) throws -> Future<Void> {
2425
let parameters = try parameters.map { try $0.convertToPostgreSQLData() }
@@ -30,29 +31,26 @@ extension PostgreSQLConnection {
3031
)
3132
let describe = PostgreSQLDescribeRequest(type: .statement, name: "")
3233
var currentRow: PostgreSQLRowDescription?
33-
var currentParameters: PostgreSQLParameterDescription?
3434

3535
return send([
3636
.parse(parse), .describe(describe), .sync
3737
]) { message in
3838
switch message {
3939
case .parseComplete: break
4040
case .rowDescription(let row): currentRow = row
41-
case .parameterDescription(let parameters): currentParameters = parameters
41+
case .parameterDescription: break
4242
case .noData: break
4343
default: fatalError("Unexpected message during PostgreSQLParseRequest: \(message)")
4444
}
4545
}.flatMap(to: Void.self) {
46-
// let parameterDataTypes = currentParameters?.dataTypes ?? [] // no parameters
47-
// let resultDataTypes = currentRow?.fields.map { $0.dataType } ?? [] // nil currentRow means no resutls
48-
46+
let resultFormats = resultFormat.formatCodeFactory(currentRow?.fields.map { $0.dataType } ?? [])
4947
// cache so we don't compute twice
5048
let bind = PostgreSQLBindRequest(
5149
portalName: "",
5250
statementName: "",
5351
parameterFormatCodes: parameters.map { $0.format },
5452
parameters: parameters.map { .init(data: $0.data) },
55-
resultFormatCodes: [.text]
53+
resultFormatCodes: resultFormats
5654
)
5755
let execute = PostgreSQLExecuteRequest(
5856
portalName: "",
@@ -65,7 +63,7 @@ extension PostgreSQLConnection {
6563
case .bindComplete: break
6664
case .dataRow(let data):
6765
let row = currentRow !! "Unexpected PostgreSQLDataRow without preceding PostgreSQLRowDescription."
68-
let parsed = try row.parse(data: data)
66+
let parsed = try row.parse(data: data, formatCodes: resultFormats)
6967
onRow(parsed)
7068
case .close: break
7169
case .noData: break

Sources/PostgreSQL/Connection/PostgreSQLConnection+SimpleQuery.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ extension PostgreSQLConnection {
2323
currentRow = row
2424
case .dataRow(let data):
2525
let row = currentRow !! "Unexpected PostgreSQLDataRow without preceding PostgreSQLRowDescription."
26-
let parsed = try row.parse(data: data)
26+
let parsed = try row.parse(data: data, formatCodes: row.fields.map { $0.formatCode })
2727
onRow(parsed)
2828
case .close: break // query over, waiting for `readyForQuery`
2929
default: fatalError("Unexpected message during PostgreSQLQuery: \(message)")

Sources/PostgreSQL/Data/PostgreSQLData+BinaryFloatingPoint.swift

+8-7
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,23 @@ extension BinaryFloatingPoint {
1818
/// See `PostgreSQLDataCustomConvertible.convertFromPostgreSQLData(_:)`
1919
public static func convertFromPostgreSQLData(_ data: PostgreSQLData) throws -> Self {
2020
guard let value = data.data else {
21-
throw PostgreSQLError(identifier: "data", reason: "Could not decode String from `null` data.")
21+
throw PostgreSQLError(identifier: "binaryFloatingPoint", reason: "Could not decode \(Self.self) from `null` data.")
2222
}
2323
switch data.format {
2424
case .binary:
2525
switch data.type {
2626
case .float4: return Self.init(value.makeFloatingPoint(Float.self))
2727
case .float8: return Self.init(value.makeFloatingPoint(Double.self))
28-
case .char: return Self.init(value.makeFixedWidthInteger(Int8.self))
29-
case .int2: return Self.init(value.makeFixedWidthInteger(Int16.self))
30-
case .int4: return Self.init(value.makeFixedWidthInteger(Int32.self))
31-
case .int8: return Self.init(value.makeFixedWidthInteger(Int64.self))
28+
case .char: return try Self.init(value.makeFixedWidthInteger(Int8.self))
29+
case .int2: return try Self.init(value.makeFixedWidthInteger(Int16.self))
30+
case .int4: return try Self.init(value.makeFixedWidthInteger(Int32.self))
31+
case .int8: return try Self.init(value.makeFixedWidthInteger(Int64.self))
3232
default: throw DecodingError.typeMismatch(Self.self, .init(codingPath: [], debugDescription: ""))
3333
}
3434
case .text:
35-
guard let converted = try Double(data.decode(String.self)) else {
36-
fatalError()
35+
let string = try data.decode(String.self)
36+
guard let converted = Double(string) else {
37+
throw PostgreSQLError(identifier: "binaryFloatingPoint", reason: "Could not decode \(Self.self) from string: \(string).")
3738
}
3839
return Self(converted)
3940
}

Sources/PostgreSQL/Data/PostgreSQLData+Date.swift

+16-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,18 @@ extension Date: PostgreSQLDataCustomConvertible {
1717
case .time: return try value.makeString().parseDate(format: "HH:mm:ss")
1818
default: throw PostgreSQLError(identifier: "date", reason: "Could not parse Date from text data type: \(data.type).")
1919
}
20-
case .binary: throw PostgreSQLError(identifier: "date", reason: "Could not parse Date from binary data type: \(data.type).")
20+
case .binary:
21+
switch data.type {
22+
case .timestamp, .time:
23+
let microseconds = try value.makeFixedWidthInteger(Int64.self)
24+
let seconds = microseconds / _microsecondsPerSecond
25+
return Date(timeInterval: Double(seconds), since: _psqlDateStart)
26+
case .date:
27+
let days = try value.makeFixedWidthInteger(Int32.self)
28+
let seconds = days * _secondsInDay
29+
return Date(timeInterval: Double(seconds), since: _psqlDateStart)
30+
default: throw PostgreSQLError(identifier: "date", reason: "Could not parse Date from binary data type: \(data.type).")
31+
}
2132
}
2233
}
2334

@@ -27,6 +38,10 @@ extension Date: PostgreSQLDataCustomConvertible {
2738
}
2839
}
2940

41+
private let _microsecondsPerSecond: Int64 = 1_000_000
42+
private let _secondsInDay: Int32 = 24 * 60 * 60
43+
private let _psqlDateStart = Date(timeIntervalSince1970: 946_684_800) // values are stored as seconds before or after midnight 2000-01-01
44+
3045
extension String {
3146
/// Parses a Date from this string with the supplied date format.
3247
fileprivate func parseDate(format: String) throws -> Date {

Sources/PostgreSQL/Data/PostgreSQLData+FixedWidthInteger.swift

+4-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,10 @@ extension UInt64: PostgreSQLDataCustomConvertible {}
8282

8383
extension Data {
8484
/// Converts this data to a fixed-width integer.
85-
internal func makeFixedWidthInteger<I>(_ type: I.Type = I.self) -> I where I: FixedWidthInteger {
85+
internal func makeFixedWidthInteger<I>(_ type: I.Type = I.self) throws -> I where I: FixedWidthInteger {
86+
guard count >= (I.bitWidth / 8) else {
87+
throw PostgreSQLError(identifier: "fixedWidthData", reason: "Not enough bytes to decode \(I.self): \(count)/\(I.bitWidth / 8)")
88+
}
8689
return unsafeCast(to: I.self).bigEndian
8790
}
8891
}

Sources/PostgreSQL/Data/PostgreSQLData+Point.swift

+14
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,20 @@ public struct PostgreSQLPoint {
1515
}
1616
}
1717

18+
extension PostgreSQLPoint: CustomStringConvertible {
19+
/// See `CustomStringConvertible.description`
20+
public var description: String {
21+
return "(\(x),\(y))"
22+
}
23+
}
24+
25+
extension PostgreSQLPoint: Equatable {
26+
/// See `Equatable.==`
27+
public static func ==(lhs: PostgreSQLPoint, rhs: PostgreSQLPoint) -> Bool {
28+
return lhs.x == rhs.x && lhs.y == rhs.y
29+
}
30+
}
31+
1832
extension PostgreSQLPoint: PostgreSQLDataCustomConvertible {
1933
/// See `PostgreSQLDataCustomConvertible.preferredDataType`
2034
public static var preferredDataType: PostgreSQLDataType? { return .point }

Sources/PostgreSQL/Data/PostgreSQLData+String.swift

+76-5
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,73 @@ extension String: PostgreSQLDataCustomConvertible {
77
/// See `PostgreSQLDataCustomConvertible.convertFromPostgreSQLData(_:)`
88
public static func convertFromPostgreSQLData(_ data: PostgreSQLData) throws -> String {
99
guard let value = data.data else {
10-
throw PostgreSQLError(identifier: "data", reason: "Could not decode String from `null` data.")
10+
throw PostgreSQLError(identifier: "string", reason: "Could not decode String from `null` data.")
1111
}
1212
switch data.format {
13-
case .text: return String(data: value, encoding: .utf8) !! "Non-utf8"
13+
case .text:
14+
guard let string = String(data: value, encoding: .utf8) else {
15+
throw PostgreSQLError(identifier: "string", reason: "Non-UTF8 string: \(value.hexDebug).")
16+
}
17+
return string
1418
case .binary:
1519
switch data.type {
16-
case .text, .name: return String(data: value, encoding: .utf8) !! "Non-utf8"
17-
default: throw PostgreSQLError(identifier: "data", reason: "Could not decode String from data type: \(data.type)")
20+
case .text, .name, .varchar, .bpchar:
21+
guard let string = String(data: value, encoding: .utf8) else {
22+
throw PostgreSQLError(identifier: "string", reason: "Non-UTF8 string: \(value.hexDebug).")
23+
}
24+
return string
25+
case .point:
26+
let point = try PostgreSQLPoint.convertFromPostgreSQLData(data)
27+
return point.description
28+
case .numeric:
29+
/// create mutable value since we will be using `.extract` which advances the buffer's view
30+
var value = value
31+
32+
/// grab the numeric metadata from the beginning of the array
33+
let metadata = value.extract(PostgreSQLNumericMetadata.self)
34+
35+
var integer = ""
36+
var fractional = ""
37+
for offset in 0..<metadata.ndigits.bigEndian {
38+
/// extract current char and advance memory
39+
let char = value.extract(Int16.self).bigEndian
40+
41+
/// conver the current char to its string form
42+
let string: String
43+
if char == 0 {
44+
/// 0 means 4 zeros
45+
string = "0000"
46+
} else {
47+
string = char.description
48+
}
49+
50+
/// depending on our offset, append the string to before or after the decimal point
51+
if offset < metadata.weight.bigEndian + 1 {
52+
integer += string
53+
} else {
54+
fractional += string
55+
}
56+
}
57+
58+
/// use the dscale to remove extraneous zeroes at the end of the fractional part
59+
let lastSignificantIndex = fractional.index(fractional.startIndex, offsetBy: Int(metadata.dscale.bigEndian))
60+
fractional = String(fractional[..<lastSignificantIndex])
61+
62+
/// determine whether fraction is empty and dynamically add `.`
63+
let numeric: String
64+
if fractional != "" {
65+
numeric = integer + "." + fractional
66+
} else {
67+
numeric = integer
68+
}
69+
70+
/// use sign to determine adding a leading `-`
71+
if metadata.sign.bigEndian == 1 {
72+
return "-" + numeric
73+
} else {
74+
return numeric
75+
}
76+
default: throw PostgreSQLError(identifier: "string", reason: "Could not decode String from binary data type: \(data.type)")
1877
}
1978
}
2079
}
@@ -25,11 +84,23 @@ extension String: PostgreSQLDataCustomConvertible {
2584
}
2685
}
2786

87+
/// Represents the meta information preceeding a numeric value.
88+
struct PostgreSQLNumericMetadata {
89+
/// The number of digits after this metadata
90+
var ndigits: Int16
91+
/// How many of the digits are before the decimal point (always add 1)
92+
var weight: Int16
93+
/// If 1, this number is negative. Otherwise, positive.
94+
var sign: Int16
95+
/// The number of sig digits after the decimal place (get rid of trailing 0s)
96+
var dscale: Int16
97+
}
98+
2899
extension Data {
29100
/// Convert the row's data into a string, throwing if invalid encoding.
30101
internal func makeString(encoding: String.Encoding = .utf8) throws -> String {
31102
guard let string = String(data: self, encoding: encoding) else {
32-
throw PostgreSQLError(identifier: "utf8String", reason: "Unexpected non-UTF8 string.")
103+
throw PostgreSQLError(identifier: "utf8String", reason: "Unexpected non-UTF8 string: \(hexDebug).")
33104
}
34105

35106
return string

Sources/PostgreSQL/Data/PostgreSQLJSONType.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ extension PostgreSQLJSONType {
1818
case .jsonb:
1919
switch data.format {
2020
case .text: return try JSONDecoder().decode(Self.self, from: value)
21-
case .binary: fatalError()
21+
case .binary:
22+
assert(value[0] == 0x01)
23+
return try JSONDecoder().decode(Self.self, from: value[1...])
2224
}
2325
default: fatalError()
2426
}

Sources/PostgreSQL/DataType/PostgreSQLDataType.swift

+7
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,10 @@ extension PostgreSQLDataType {
7777
return string
7878
}
7979
}
80+
81+
extension PostgreSQLDataType: CustomStringConvertible {
82+
/// See `CustomStringConvertible.description`
83+
public var description: String {
84+
return sqlName
85+
}
86+
}

Sources/PostgreSQL/DataType/PostgreSQLFormatCode.swift

+30
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,33 @@ public enum PostgreSQLFormatCode: Int16, Codable {
66
case text = 0
77
case binary = 1
88
}
9+
10+
public struct PostgreSQLResultFormat {
11+
/// The format codes
12+
internal let formatCodeFactory: ([PostgreSQLDataType]) -> [PostgreSQLFormatCode]
13+
14+
/// Dynamically choose result format based on data type.
15+
public static func dynamic(_ callback: @escaping (PostgreSQLDataType) -> PostgreSQLFormatCode) -> PostgreSQLResultFormat {
16+
return .init { return $0.map { callback($0) } }
17+
}
18+
19+
/// Request all of the results in a specific format.
20+
public static func all(_ code: PostgreSQLFormatCode) -> PostgreSQLResultFormat {
21+
return .init { _ in return [code] }
22+
}
23+
24+
/// Request all of the results in a specific format.
25+
public static func text() -> PostgreSQLResultFormat {
26+
return .all(.text)
27+
}
28+
29+
/// Request all of the results in a specific format.
30+
public static func binary() -> PostgreSQLResultFormat {
31+
return .all(.binary)
32+
}
33+
34+
/// Let the server decide the formatting options.
35+
public static func notSpecified() -> PostgreSQLResultFormat {
36+
return .init { _ in return [] }
37+
}
38+
}

Sources/PostgreSQL/Message/PostgreSQLRowDescription.swift

+9-2
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,17 @@ struct PostgreSQLRowDescription: Decodable {
2121

2222
extension PostgreSQLRowDescription {
2323
/// Parses a `PostgreSQLDataRow` using the metadata from this row description.
24-
public func parse(data: PostgreSQLDataRow) throws -> [String: PostgreSQLData] {
24+
/// Important to pass formatCodes in since the format codes in the field are likely not correct (if from a describe request)
25+
public func parse(data: PostgreSQLDataRow, formatCodes: [PostgreSQLFormatCode]) throws -> [String: PostgreSQLData] {
2526
return try .init(uniqueKeysWithValues: fields.enumerated().map { (i, field) in
27+
let formatCode: PostgreSQLFormatCode
28+
switch formatCodes.count {
29+
case 0: formatCode = .text
30+
case 1: formatCode = formatCodes[0]
31+
default: formatCode = formatCodes[i]
32+
}
2633
let key = field.name
27-
let value = try data.columns[i].parse(dataType: field.dataType, format: field.formatCode)
34+
let value = try data.columns[i].parse(dataType: field.dataType, format: formatCode)
2835
return (key, value)
2936
})
3037
}

Sources/PostgreSQL/Utilities.swift

+26-1
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,35 @@ extension UnsafeRawBufferPointer {
2626
}
2727

2828
extension Data {
29-
public mutating func unsafePopFirst() -> Byte {
29+
internal mutating func unsafePopFirst() -> Byte {
3030
guard let byte = popFirst() else {
3131
fatalError("Unexpected end of data")
3232
}
3333
return byte
3434
}
35+
36+
internal mutating func skip(_ n: Int) {
37+
guard n < count else {
38+
self = Data()
39+
return
40+
}
41+
for _ in 0..<n {
42+
assert(popFirst() != nil)
43+
}
44+
}
45+
46+
internal mutating func skip<T>(sizeOf: T.Type) {
47+
skip(MemoryLayout<T>.size)
48+
}
49+
50+
/// Casts data to a supplied type.
51+
internal mutating func extract<T>(_ type: T.Type = T.self) -> T {
52+
assert(MemoryLayout<T>.size <= count, "Insufficient data to decode: \(T.self)")
53+
defer { skip(sizeOf: T.self) }
54+
return withUnsafeBytes { (pointer: UnsafePointer<T>) -> T in
55+
return pointer.pointee
56+
}
57+
}
3558
}
3659

3760

@@ -42,4 +65,6 @@ extension Data {
4265
return pointer.pointee
4366
}
4467
}
68+
69+
4570
}

0 commit comments

Comments
 (0)