Skip to content

Commit 7328388

Browse files
authored
add PostgresNumeric (#23)
* add PostgresNumeric * fix tests * Use a dictionary for Postgres row column lookups. (#22) * Use a dictionary for Postgres row column lookups. * PR fixes. * Update the Linux tests. * add PostgresNumeric * fix tests * add NUMERIC serialization * add additional comments + decimal converters * add MONEY support * fix linuxmain * add date to string conversion * add int / float descs
1 parent a3e257c commit 7328388

File tree

8 files changed

+377
-84
lines changed

8 files changed

+377
-84
lines changed

Sources/NIOPostgres/Data/PostgresData+Double.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ extension PostgresData {
1616
.flatMap { Double($0) }
1717
case .float8:
1818
return value.readFloat(as: Double.self)
19+
case .numeric:
20+
return self.numeric?.double
1921
default: fatalError("Cannot decode Double from \(self)")
2022
}
2123
case .text:
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
import struct Foundation.Decimal
2+
3+
public struct PostgresNumeric: CustomStringConvertible, CustomDebugStringConvertible, ExpressibleByStringLiteral {
4+
/// The number of digits after this metadata
5+
internal var ndigits: Int16
6+
/// How many of the digits are before the decimal point (always add 1)
7+
internal var weight: Int16
8+
/// If 0x4000, this number is negative. See NUMERIC_NEG in
9+
/// https://github.com/postgres/postgres/blob/master/src/backend/utils/adt/numeric.c
10+
internal var sign: Int16
11+
/// The number of sig digits after the decimal place (get rid of trailing 0s)
12+
internal var dscale: Int16
13+
/// Array of Int16, each representing 4 chars of the number
14+
internal var value: ByteBuffer
15+
16+
public var description: String {
17+
return self.string
18+
}
19+
20+
public var debugDescription: String {
21+
return """
22+
ndigits: \(self.ndigits)
23+
weight: \(self.weight)
24+
sign: \(self.sign)
25+
dscale: \(self.dscale)
26+
value: \(self.value.debugDescription)
27+
"""
28+
}
29+
30+
public var double: Double? {
31+
return Double(self.string)
32+
}
33+
34+
public init(decimal: Decimal) {
35+
self.init(decimalString: decimal.description)
36+
}
37+
38+
public init?(string: String) {
39+
// validate string contents are decimal
40+
guard Double(string) != nil else {
41+
return nil
42+
}
43+
self.init(decimalString: string)
44+
}
45+
46+
public init(stringLiteral value: String) {
47+
self.init(decimalString: value)
48+
}
49+
50+
internal init(decimalString: String) {
51+
// split on period, get integer and fractional
52+
let parts = decimalString.split(separator: ".")
53+
var integer: Substring
54+
let fractional: Substring?
55+
switch parts.count {
56+
case 1:
57+
integer = parts[0]
58+
fractional = nil
59+
case 2:
60+
integer = parts[0]
61+
fractional = parts[1]
62+
default:
63+
fatalError("Unexpected decimal string: \(decimalString)")
64+
}
65+
66+
// check if negative
67+
let isNegative: Bool
68+
if integer.hasPrefix("-") {
69+
integer = integer.dropFirst()
70+
isNegative = true
71+
} else {
72+
isNegative = false
73+
}
74+
75+
// buffer will store 1+ Int16 values representing
76+
// 4 digit chunks of the number
77+
var buffer = ByteBufferAllocator().buffer(capacity: 0)
78+
79+
// weight always has 1 added to it, so start at -1
80+
var weight = -1
81+
82+
// iterate over each chunk in the integer part of the numeric
83+
// we use reverse chunked since the first chunk should be the
84+
// shortest if the integer length is not evenly divisible by 4
85+
for chunk in integer.reverseChunked(by: 4) {
86+
weight += 1
87+
// convert the 4 digits to an Int16
88+
buffer.writeInteger(Int16(chunk)!, endianness: .big)
89+
}
90+
91+
// dscale will measure how many sig digits are in the fraction
92+
var dscale = 0
93+
94+
if let fractional = fractional {
95+
// iterate over each chunk in the fractional part of the numeric
96+
// we use normal chunking size the end chunk should be the shortest
97+
// (potentially having extra zeroes)
98+
for chunk in fractional.chunked(by: 4) {
99+
// for each _significant_ digit, increment dscale by count
100+
dscale += chunk.count
101+
// add trailing zeroes if the number is not 4 long
102+
let string = chunk + String(repeating: "0", count: 4 - chunk.count)
103+
// convert the 4 digits to an Int16
104+
buffer.writeInteger(Int16(string)!, endianness: .big)
105+
}
106+
}
107+
// ndigits is the number of int16's in the buffer
108+
self.ndigits = numericCast(buffer.readableBytes / 2)
109+
self.weight = numericCast(weight)
110+
self.sign = isNegative ? 0x4000 : 0
111+
self.dscale = numericCast(dscale)
112+
self.value = buffer
113+
}
114+
115+
public var decimal: Decimal {
116+
// force cast should always succeed since we know
117+
// string returns a valid decimal
118+
return Decimal(string: self.string)!
119+
}
120+
121+
public var string: String {
122+
guard self.ndigits > 0 else {
123+
return "0"
124+
}
125+
126+
var integer = ""
127+
var fractional = ""
128+
129+
var value = self.value
130+
for offset in 0..<self.ndigits {
131+
/// extract current char and advance memory
132+
let char = value.readInteger(endianness: .big, as: Int16.self) ?? 0
133+
134+
/// convert the current char to its string form
135+
let string: String
136+
if char == 0 {
137+
/// 0 means 4 zeros
138+
string = "0000"
139+
} else {
140+
string = char.description
141+
}
142+
143+
/// depending on our offset, append the string to before or after the decimal point
144+
if offset < self.weight + 1 {
145+
// insert zeros (skip leading)
146+
if offset > 0 {
147+
integer += String(repeating: "0", count: 4 - string.count)
148+
}
149+
integer += string
150+
} else {
151+
// leading zeros matter with fractional
152+
fractional += String(repeating: "0", count: 4 - string.count) + string
153+
}
154+
}
155+
156+
if integer.count == 0 {
157+
integer = "0"
158+
}
159+
160+
if fractional.count > self.dscale {
161+
/// use the dscale to remove extraneous zeroes at the end of the fractional part
162+
let lastSignificantIndex = fractional.index(fractional.startIndex, offsetBy: Int(self.dscale))
163+
fractional = String(fractional[..<lastSignificantIndex])
164+
}
165+
166+
/// determine whether fraction is empty and dynamically add `.`
167+
let numeric: String
168+
if fractional != "" {
169+
numeric = integer + "." + fractional
170+
} else {
171+
numeric = integer
172+
}
173+
174+
/// use sign to determine adding a leading `-`
175+
if (self.sign & 0x4000) != 0 {
176+
return "-" + numeric
177+
} else {
178+
return numeric
179+
}
180+
}
181+
182+
init?(buffer: inout ByteBuffer) {
183+
guard let ndigits = buffer.readInteger(endianness: .big, as: Int16.self) else {
184+
return nil
185+
}
186+
self.ndigits = ndigits
187+
guard let weight = buffer.readInteger(endianness: .big, as: Int16.self) else {
188+
return nil
189+
}
190+
self.weight = weight
191+
guard let sign = buffer.readInteger(endianness: .big, as: Int16.self) else {
192+
return nil
193+
}
194+
self.sign = sign
195+
guard let dscale = buffer.readInteger(endianness: .big, as: Int16.self) else {
196+
return nil
197+
}
198+
self.dscale = dscale
199+
self.value = buffer
200+
}
201+
}
202+
203+
extension PostgresData {
204+
public init(numeric: PostgresNumeric) {
205+
var buffer = ByteBufferAllocator().buffer(capacity: 0)
206+
buffer.writeInteger(numeric.ndigits, endianness: .big)
207+
buffer.writeInteger(numeric.weight, endianness: .big)
208+
buffer.writeInteger(numeric.sign, endianness: .big)
209+
buffer.writeInteger(numeric.dscale, endianness: .big)
210+
var value = numeric.value
211+
buffer.writeBuffer(&value)
212+
self.init(type: .numeric, value: buffer)
213+
}
214+
215+
public var numeric: PostgresNumeric? {
216+
/// create mutable value since we will be using `.extract` which advances the buffer's view
217+
guard var value = self.value else {
218+
return nil
219+
}
220+
221+
/// grab the numeric metadata from the beginning of the array
222+
guard let metadata = PostgresNumeric(buffer: &value) else {
223+
return nil
224+
}
225+
226+
return metadata
227+
}
228+
}
229+
230+
private extension Collection {
231+
// splits the collection into chunks of the supplied size
232+
// if the collection is not evenly divisible, the last chunk will be smaller
233+
func chunked(by maxSize: Int) -> [SubSequence] {
234+
return stride(from: 0, to: self.count, by: maxSize).map { current in
235+
let chunkStartIndex = self.index(self.startIndex, offsetBy: current)
236+
let chunkEndOffset = Swift.min(
237+
self.distance(from: chunkStartIndex, to: self.endIndex),
238+
maxSize
239+
)
240+
let chunkEndIndex = self.index(chunkStartIndex, offsetBy: chunkEndOffset)
241+
return self[chunkStartIndex..<chunkEndIndex]
242+
}
243+
}
244+
245+
// splits the collection into chunks of the supplied size
246+
// if the collection is not evenly divisible, the first chunk will be smaller
247+
func reverseChunked(by maxSize: Int) -> [SubSequence] {
248+
var lastDistance = 0
249+
var chunkStartIndex = self.startIndex
250+
return stride(from: 0, to: self.count, by: maxSize).reversed().map { current in
251+
let distance = (self.count - current) - lastDistance
252+
lastDistance = distance
253+
let chunkEndOffset = Swift.min(
254+
self.distance(from: chunkStartIndex, to: self.endIndex),
255+
distance
256+
)
257+
let chunkEndIndex = self.index(chunkStartIndex, offsetBy: chunkEndOffset)
258+
defer { chunkStartIndex = chunkEndIndex }
259+
return self[chunkStartIndex..<chunkEndIndex]
260+
}
261+
}
262+
}
263+

Sources/NIOPostgres/Data/PostgresData+String.swift

Lines changed: 28 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ extension PostgresData {
99
guard var value = self.value else {
1010
return nil
1111
}
12-
1312
switch self.formatCode {
1413
case .binary:
1514
switch self.type {
@@ -19,89 +18,36 @@ extension PostgresData {
1918
}
2019
return string
2120
case .numeric:
22-
/// Represents the meta information preceeding a numeric value.
23-
struct PostgreSQLNumericMetadata {
24-
/// The number of digits after this metadata
25-
var ndigits: Int16
26-
/// How many of the digits are before the decimal point (always add 1)
27-
var weight: Int16
28-
/// If 0x4000, this number is negative. See NUMERIC_NEG in
29-
/// https://github.com/postgres/postgres/blob/master/src/backend/utils/adt/numeric.c
30-
var sign: Int16
31-
/// The number of sig digits after the decimal place (get rid of trailing 0s)
32-
var dscale: Int16
33-
}
34-
35-
/// grab the numeric metadata from the beginning of the array
36-
#warning("TODO: fix force unwrap")
37-
let metadata = PostgreSQLNumericMetadata(
38-
ndigits: value.readInteger()!,
39-
weight: value.readInteger()!,
40-
sign: value.readInteger()!,
41-
dscale: value.readInteger()!
42-
)
43-
44-
guard metadata.ndigits > 0 else {
45-
return "0"
46-
}
47-
48-
var integer = ""
49-
var fractional = ""
50-
for offset in 0..<metadata.ndigits {
51-
/// extract current char and advance memory
52-
#warning("TODO: fix force unwrap")
53-
let char = value.readInteger(as: Int16.self)!
54-
55-
/// convert the current char to its string form
56-
let string: String
57-
if char == 0 {
58-
/// 0 means 4 zeros
59-
string = "0000"
60-
} else {
61-
string = char.description
62-
}
63-
64-
/// depending on our offset, append the string to before or after the decimal point
65-
if offset < metadata.weight + 1 {
66-
// insert zeros (skip leading)
67-
if offset > 0 {
68-
integer += String(repeating: "0", count: 4 - string.count)
69-
}
70-
integer += string
71-
} else {
72-
// leading zeros matter with fractional
73-
fractional += String(repeating: "0", count: 4 - string.count) + string
74-
}
75-
}
76-
77-
if integer.count == 0 {
78-
integer = "0"
79-
}
80-
81-
if fractional.count > metadata.dscale {
82-
/// use the dscale to remove extraneous zeroes at the end of the fractional part
83-
let lastSignificantIndex = fractional.index(
84-
fractional.startIndex, offsetBy: Int(metadata.dscale)
85-
)
86-
fractional = String(fractional[..<lastSignificantIndex])
87-
}
88-
89-
/// determine whether fraction is empty and dynamically add `.`
90-
let numeric: String
91-
if fractional != "" {
92-
numeric = integer + "." + fractional
93-
} else {
94-
numeric = integer
21+
return self.numeric?.string
22+
case .uuid:
23+
return value.readUUID()!.uuidString
24+
case .timestamp, .timestamptz, .date:
25+
return self.date?.description
26+
case .money:
27+
assert(value.readableBytes == 8)
28+
guard let int64 = value.getInteger(at: value.readerIndex, as: Int64.self) else {
29+
fatalError()
9530
}
96-
97-
/// use sign to determine adding a leading `-`
98-
if (metadata.sign & 0x4000) != 0 {
99-
return "-" + numeric
100-
} else {
101-
return numeric
31+
let description = int64.description
32+
switch description.count {
33+
case 0:
34+
return "0.00"
35+
case 1:
36+
return "0.0" + description
37+
case 2:
38+
return "0." + description
39+
default:
40+
let decimalIndex = description.index(description.endIndex, offsetBy: -2)
41+
return description[description.startIndex..<decimalIndex]
42+
+ "."
43+
+ description[decimalIndex..<description.endIndex]
10244
}
103-
case .uuid: return value.readUUID()!.uuidString
104-
default: fatalError("Cannot decode String from \(self)")
45+
case .float4, .float8:
46+
return self.double?.description
47+
case .int2, .int4, .int8:
48+
return self.int?.description
49+
default:
50+
fatalError("Cannot decode String from \(self.type)")
10551
}
10652
case .text:
10753
guard let string = value.readString(length: value.readableBytes) else {

0 commit comments

Comments
 (0)