Skip to content

Commit 039842d

Browse files
committed
psql client + asymmetric stream
1 parent eccd3e8 commit 039842d

9 files changed

+311
-43
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import Bits
2+
3+
/// Identifies the message as a Close command.
4+
struct PostgreSQLCloseCommand: Decodable {
5+
/// 'S' to close a prepared statement; or 'P' to close a portal.
6+
var type: Byte
7+
8+
/// The name of the prepared statement or portal to close (an empty string selects the unnamed prepared statement or portal).
9+
var name: String
10+
11+
/// See Decodable.init
12+
init(from decoder: Decoder) throws {
13+
let single = try decoder.singleValueContainer()
14+
type = try single.decode(Byte.self)
15+
name = try single.decode(String.self)
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import Bits
2+
import Foundation
3+
4+
/// Identifies the message as a data row.
5+
struct PostgreSQLDataRow: Decodable {
6+
/// The data row's columns
7+
var columns: [PostgreSQLDataRowColumn]
8+
9+
init(from decoder: Decoder) throws {
10+
let single = try decoder.singleValueContainer()
11+
/// The number of column values that follow (possibly zero).
12+
let count = try single.decode(Int16.self)
13+
14+
var columns: [PostgreSQLDataRowColumn] = []
15+
for _ in 0..<count {
16+
let column = try single.decode(PostgreSQLDataRowColumn.self)
17+
columns.append(column)
18+
}
19+
self.columns = columns
20+
}
21+
}
22+
23+
struct PostgreSQLDataRowColumn: Decodable {
24+
/// The length of the column value, in bytes (this count does not include itself).
25+
/// Can be zero. As a special case, -1 indicates a NULL column value. No value bytes follow in the NULL case.
26+
27+
/// The value of the column, in the format indicated by the associated format code. n is the above length.
28+
var value: Data?
29+
30+
init(from decoder: Decoder) throws {
31+
let single = try decoder.singleValueContainer()
32+
/// The length of the column value, in bytes (this count does not include itself).
33+
/// Can be zero. As a special case, -1 indicates a NULL column value. No value bytes follow in the NULL case.
34+
let count = try single.decode(Int32.self)
35+
switch count {
36+
case -1: value = nil
37+
case 0: value = .init()
38+
case 1...:
39+
// FIXME: optimize
40+
var bytes: [Byte] = []
41+
for _ in 0..<count {
42+
let byte = try single.decode(Byte.self)
43+
bytes.append(byte)
44+
}
45+
value = Data(bytes)
46+
default: fatalError("Illegal data row column value count: \(count)")
47+
}
48+
}
49+
}

Sources/PostgreSQL/Message/PostgreSQLMessage.swift

+4
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,8 @@ enum PostgreSQLMessage {
88
case parameterStatus(PostgreSQLParameterStatus)
99
case backendKeyData(PostgreSQLBackendKeyData)
1010
case readyForQuery(PostgreSQLReadyForQuery)
11+
case query(PostgreSQLQuery)
12+
case rowDescription(PostgreSQLRowDescription)
13+
case dataRow(PostgreSQLDataRow)
14+
case close(PostgreSQLCloseCommand)
1115
}

Sources/PostgreSQL/Message/PostgreSQLMessageDecoder.swift

+19-7
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@ final class PostgreSQLMessageDecoder {
2525
case .S: message = try .parameterStatus(decoder.decode())
2626
case .K: message = try .backendKeyData(decoder.decode())
2727
case .Z: message = try .readyForQuery(decoder.decode())
28-
default: fatalError("Unrecognized message type: \(type)")
28+
case .T: message = try .rowDescription(decoder.decode())
29+
case .D: message = try .dataRow(decoder.decode())
30+
case .C: message = try .close(decoder.decode())
31+
default:
32+
let string = String(bytes: [type], encoding: .ascii) ?? "n/a"
33+
fatalError("Unrecognized message type: \(string) (\(type)")
2934
}
3035
return (message, decoder.data.count)
3136
}
@@ -78,6 +83,19 @@ internal final class _PostgreSQLMessageDecoder: Decoder, SingleValueDecodingCont
7883
return self
7984
}
8085

86+
/// See SingleValueDecodingContainer.decode
87+
func decode(_ type: UInt8.Type) throws -> UInt8 {
88+
return self.data.unsafePopFirst()
89+
}
90+
91+
/// See SingleValueDecodingContainer.decode
92+
func decode(_ type: Int16.Type) throws -> Int16 {
93+
var int: Int16 = 0
94+
int += Int16(self.data.unsafePopFirst() << 8)
95+
int += Int16(self.data.unsafePopFirst())
96+
return int
97+
}
98+
8199
/// See SingleValueDecodingContainer.decode
82100
func decode(_ type: Int32.Type) throws -> Int32 {
83101
var int: Int32 = 0
@@ -88,11 +106,6 @@ internal final class _PostgreSQLMessageDecoder: Decoder, SingleValueDecodingCont
88106
return int
89107
}
90108

91-
/// See SingleValueDecodingContainer.decode
92-
func decode(_ type: UInt8.Type) throws -> UInt8 {
93-
return self.data.unsafePopFirst()
94-
}
95-
96109
/// See SingleValueDecodingContainer.decode
97110
func decode(_ type: String.Type) throws -> String {
98111
var bytes: [UInt8] = []
@@ -124,7 +137,6 @@ internal final class _PostgreSQLMessageDecoder: Decoder, SingleValueDecodingCont
124137
func decode(_ type: Int.Type) throws -> Int { fatalError("Unsupported decode type: \(type)") }
125138
func decode(_ type: Int8.Type) throws -> Int8 { fatalError("Unsupported decode type: \(type)") }
126139
func decode(_ type: Int64.Type) throws -> Int64 { fatalError("Unsupported decode type: \(type)") }
127-
func decode(_ type: Int16.Type) throws -> Int16 { fatalError("Unsupported decode type: \(type)") }
128140
func decode(_ type: UInt.Type) throws -> UInt { fatalError("Unsupported decode type: \(type)") }
129141
func decode(_ type: UInt16.Type) throws -> UInt16 { fatalError("Unsupported decode type: \(type)") }
130142
func decode(_ type: UInt32.Type) throws -> UInt32 { fatalError("Unsupported decode type: \(type)") }

Sources/PostgreSQL/Message/PostgreSQLMessageEncoder.swift

+14-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Bits
12
import Foundation
23

34
/// Non-encoder wrapper for `_PostgreSQLMessageEncoder`.
@@ -8,12 +9,22 @@ final class PostgreSQLMessageEncoder {
89
/// Encodes a `PostgreSQLMessage` to `Data`.
910
func encode(_ message: PostgreSQLMessage) throws -> Data {
1011
let encoder = _PostgreSQLMessageEncoder()
12+
let identifier: Byte?
1113
switch message {
12-
case .startupMessage(let message): try message.encode(to: encoder)
13-
default: fatalError("Unsupported encodable type")
14+
case .startupMessage(let message):
15+
identifier = nil
16+
try message.encode(to: encoder)
17+
case .query(let query):
18+
identifier = .Q
19+
try query.encode(to: encoder)
20+
default: fatalError("Unsupported encodable type: \(type(of: message))")
1421
}
1522
encoder.updateSize()
16-
return encoder.data
23+
if let prefix = identifier {
24+
return [prefix] + encoder.data
25+
} else {
26+
return encoder.data
27+
}
1728
}
1829
}
1930

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/// Identifies the message as a simple query.
2+
struct PostgreSQLQuery: Encodable {
3+
/// The query string itself.
4+
var query: String
5+
6+
/// See Encodable.encode
7+
func encode(to encoder: Encoder) throws {
8+
var single = encoder.singleValueContainer()
9+
try single.encode(query)
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/// Identifies the message as a row description.
2+
struct PostgreSQLRowDescription: Decodable {
3+
/// The fields supplied in the row description.
4+
var fields: [PostgreSQLRowDescriptionField]
5+
6+
/// See Decodable.decode
7+
init(from decoder: Decoder) throws {
8+
let single = try decoder.singleValueContainer()
9+
10+
/// Specifies the number of fields in a row (can be zero).
11+
let fieldCount = try single.decode(Int16.self)
12+
13+
var fields: [PostgreSQLRowDescriptionField] = []
14+
for _ in 0..<fieldCount {
15+
try fields.append(single.decode(PostgreSQLRowDescriptionField.self))
16+
}
17+
self.fields = fields
18+
}
19+
}
20+
21+
/// MARK: Field
22+
23+
/// Describes a single field returns in a `RowDescription` message.
24+
struct PostgreSQLRowDescriptionField: Decodable {
25+
/// The field name.
26+
var name: String
27+
28+
/// If the field can be identified as a column of a specific table, the object ID of the table; otherwise zero.
29+
var tableObjectID: Int32
30+
31+
/// If the field can be identified as a column of a specific table, the attribute number of the column; otherwise zero.
32+
var columnAttributeNumber: Int16
33+
34+
/// The object ID of the field's data type.
35+
var dataTypeObjectID: Int32
36+
37+
/// The data type size (see pg_type.typlen). Note that negative values denote variable-width types.
38+
var dataTypeSize: Int16
39+
40+
/// The type modifier (see pg_attribute.atttypmod). The meaning of the modifier is type-specific.
41+
var dataTypeModifier: Int32
42+
43+
/// The format code being used for the field.
44+
/// Currently will be zero (text) or one (binary).
45+
/// In a RowDescription returned from the statement variant of Describe,
46+
/// the format code is not yet known and will always be zero.
47+
var formatCode: Int16
48+
49+
/// See Decodable.decode
50+
init(from decoder: Decoder) throws {
51+
let single = try decoder.singleValueContainer()
52+
name = try single.decode(String.self)
53+
tableObjectID = try single.decode(Int32.self)
54+
columnAttributeNumber = try single.decode(Int16.self)
55+
dataTypeObjectID = try single.decode(Int32.self)
56+
dataTypeSize = try single.decode(Int16.self)
57+
dataTypeModifier = try single.decode(Int32.self)
58+
formatCode = try single.decode(Int16.self)
59+
}
60+
}

Sources/PostgreSQL/PostgreSQLClient.swift

+131-5
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ import Async
33
/// A PostgreSQL frontend client.
44
final class PostgreSQLClient {
55
/// Handles enqueued redis commands and responses.
6-
private let queueStream: QueueStream<PostgreSQLMessage, PostgreSQLMessage>
6+
private let queueStream: AsymmetricQueueStream<PostgreSQLMessage, PostgreSQLMessage>
77

88
/// Creates a new Redis client on the provided data source and sink.
99
init<Stream>(stream: Stream, on worker: Worker) where Stream: ByteStream {
10-
let queueStream = QueueStream<PostgreSQLMessage, PostgreSQLMessage>()
10+
let queueStream = AsymmetricQueueStream<PostgreSQLMessage, PostgreSQLMessage>()
1111

1212
let serializerStream = PostgreSQLMessageSerializer().stream(on: worker)
1313
let parserStream = PostgreSQLMessageParser().stream(on: worker)
@@ -20,8 +20,134 @@ final class PostgreSQLClient {
2020
self.queueStream = queueStream
2121
}
2222

23-
/// Sends `RedisData` to the server.
24-
func send(_ data: PostgreSQLMessage) -> Future<PostgreSQLMessage> {
25-
return queueStream.enqueue(data)
23+
/// Sends `PostgreSQLMessage` to the server.
24+
func send(_ message: PostgreSQLMessage) -> Future<[PostgreSQLMessage]> {
25+
return queueStream.enqueue(message) { message in
26+
switch message {
27+
case .readyForQuery: return true
28+
case .errorResponse(let e): throw e
29+
default: return false
30+
}
31+
}
32+
}
33+
}
34+
35+
/// Enqueues a single input and waits for multiple output.
36+
/// This is useful for situations where one request can lead
37+
/// to multiple responses.
38+
public final class AsymmetricQueueStream<I, O>: Stream, ConnectionContext {
39+
/// See `InputStream.Input`
40+
public typealias Input = I
41+
42+
/// See `OutputStream.Output`
43+
public typealias Output = O
44+
45+
/// Current upstream output stream.
46+
private var upstream: ConnectionContext?
47+
48+
/// Current downstrema input stream.
49+
private var downstream: AnyInputStream<Output>?
50+
51+
/// Current downstream demand.
52+
private var downstreamDemand: UInt
53+
54+
/// Queued output.
55+
private var queuedOutput: [Output]
56+
57+
/// Queued input.
58+
private var queuedInput: [AsymmetricQueueStreamInput<Input>]
59+
60+
/// Current input being handled.
61+
private var currentInput: AsymmetricQueueStreamInput<Input>?
62+
63+
/// Create a new `AsymmetricQueueStream`.
64+
public init() {
65+
self.downstreamDemand = 0
66+
self.queuedOutput = []
67+
self.queuedInput = []
68+
}
69+
70+
/// Enqueue the supplied output, specifying a closure that will determine
71+
/// when the Input received is ready.
72+
public func enqueue(_ output: Output, readyCheck: @escaping (Input) throws -> Bool) -> Future<[Input]> {
73+
let input = AsymmetricQueueStreamInput(readyCheck: readyCheck)
74+
self.queuedInput.insert(input, at: 0)
75+
self.queuedOutput.insert(output, at: 0)
76+
upstream!.request()
77+
update()
78+
return input.promise.future
79+
}
80+
81+
/// Updates internal state.
82+
private func update() {
83+
while downstreamDemand > 0 {
84+
guard let output = queuedOutput.popLast() else {
85+
break
86+
}
87+
downstreamDemand -= 1
88+
downstream!.next(output)
89+
}
90+
}
91+
92+
/// See `ConnectionContext.connection`
93+
public func connection(_ event: ConnectionEvent) {
94+
switch event {
95+
case .cancel: break // handle better
96+
case .request(let count):
97+
downstreamDemand += count
98+
update()
99+
}
100+
}
101+
102+
/// See `InputStream.input`
103+
public func input(_ event: InputEvent<I>) {
104+
switch event {
105+
case .close: downstream?.close()
106+
case .connect(let upstream):
107+
self.upstream = upstream
108+
update()
109+
case .error(let error): downstream?.error(error)
110+
case .next(let input):
111+
var context: AsymmetricQueueStreamInput<Input>
112+
if let current = currentInput {
113+
context = current
114+
} else {
115+
print("extra: \(input)")
116+
let next = queuedInput.popLast()!
117+
currentInput = next
118+
context = next
119+
}
120+
121+
context.storage.append(input)
122+
do {
123+
if try context.readyCheck(input) {
124+
context.promise.complete(context.storage)
125+
currentInput = nil
126+
} else {
127+
upstream!.request(count: 1)
128+
}
129+
} catch {
130+
context.promise.fail(error)
131+
currentInput = nil
132+
}
133+
}
134+
}
135+
136+
/// See `OutputStream.output`
137+
public func output<S>(to inputStream: S) where S : InputStream, S.Input == Output {
138+
downstream = .init(inputStream)
139+
inputStream.connect(to: self)
140+
}
141+
}
142+
143+
final class AsymmetricQueueStreamInput<Input> {
144+
var storage: [Input]
145+
var promise: Promise<[Input]>
146+
var readyCheck: (Input) throws -> Bool
147+
148+
init(readyCheck: @escaping (Input) throws -> Bool) {
149+
self.storage = []
150+
self.promise = .init()
151+
self.readyCheck = readyCheck
26152
}
27153
}

0 commit comments

Comments
 (0)