Skip to content

Commit e42bfe1

Browse files
committed
Non-copyable ColumnWriter
1 parent f2e1370 commit e42bfe1

File tree

3 files changed

+36
-22
lines changed

3 files changed

+36
-22
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ let package = Package(
5454
.product(name: "NIOFoundationCompat", package: "swift-nio"),
5555
.product(name: "ServiceLifecycle", package: "swift-service-lifecycle"),
5656
],
57-
swiftSettings: swiftSettings
57+
swiftSettings: swiftSettings + [.enableExperimentalFeature("Lifetimes")]
5858
),
5959
.target(
6060
name: "_ConnectionPoolModule",

Sources/PostgresNIO/Connection/PostgresConnection+CopyFrom.swift

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -98,27 +98,42 @@ public struct PostgresCopyFromWriter: Sendable {
9898
}
9999
}
100100

101-
101+
// PostgresBinaryCopyFromWriter relies on non-Escapable types, which were only introduced in Swift 6.2
102+
#if compiler(>=6.2)
102103
/// Handle to send binary data for a `COPY ... FROM STDIN` query to the backend.
103104
///
104105
/// It takes care of serializing `PostgresEncodable` column types into the binary format that Postgres expects.
105106
public struct PostgresBinaryCopyFromWriter: ~Copyable {
106107
/// Handle to serialize columns into a row that is being written by `PostgresBinaryCopyFromWriter`.
107-
public struct ColumnWriter: ~Copyable {
108-
/// The `PostgresBinaryCopyFromWriter` that is gathering the serialized data.
109-
///
110-
/// We need to model this as `UnsafeMutablePointer` because we can't express in the Swift type system that
111-
/// `ColumnWriter` never exceeds the lifetime of `PostgresBinaryCopyFromWriter`.
108+
public struct ColumnWriter: ~Escapable, ~Copyable {
109+
/// Pointer to the `PostgresBinaryCopyFromWriter` that is gathering the serialized data.
112110
@usableFromInline
113111
let underlying: UnsafeMutablePointer<PostgresBinaryCopyFromWriter>
114112

115113
/// The number of columns that have been written by this `ColumnWriter`.
116114
@usableFromInline
117115
var columns: UInt16 = 0
118116

117+
/// - Warning: Do not call directly, call `withColumnWriter` instead
119118
@usableFromInline
120-
init(underlying: UnsafeMutablePointer<PostgresBinaryCopyFromWriter>) {
121-
self.underlying = underlying
119+
init(_underlying: UnsafeMutablePointer<PostgresBinaryCopyFromWriter>) {
120+
self.underlying = _underlying
121+
}
122+
123+
@usableFromInline
124+
static func withColumnWriter<T>(
125+
writingTo underlying: inout PostgresBinaryCopyFromWriter,
126+
body: (inout ColumnWriter) throws -> T
127+
) rethrows -> T {
128+
return try withUnsafeMutablePointer(to: &underlying) { pointerToUnderlying in
129+
// We can guarantee that `ColumWriter` never outlives `underlying` because `ColumnWriter` is
130+
// `~Escapable` and thus cannot escape the context of the closure to `withUnsafeMutablePointer`.
131+
// To model this without resorting to unsafe pointers, we would need to be able to declare an `inout`
132+
// reference to `PostgresBinaryCopyFromWriter` as a member of `ColumnWriter`, which isn't possible at
133+
// the moment (https://github.com/swiftlang/swift/issues/85832).
134+
var columnWriter = ColumnWriter(_underlying: pointerToUnderlying)
135+
return try body(&columnWriter)
136+
}
122137
}
123138

124139
/// Serialize a single column to a row.
@@ -128,16 +143,19 @@ public struct PostgresBinaryCopyFromWriter: ~Copyable {
128143
/// be called with an `Int32`. Serializing an integer of a different width will cause a deserialization
129144
/// failure in the backend.
130145
@inlinable
146+
#if compiler(<6.3)
147+
@_lifetime(&self)
148+
#endif
131149
public mutating func writeColumn(_ column: (some PostgresEncodable)?) throws {
132150
columns += 1
133151
try invokeWriteColumn(on: underlying, column)
134152
}
135153

136-
// Needed to work around https://github.com/swiftlang/swift/issues/83309, copying the implementation into
154+
// Needed to work around https://github.com/swiftlang/swift/issues/83309, copying the implementation into
137155
// `writeColumn` causes an assertion failure when thread sanitizer is enabled.
138-
@inlinable
156+
@inlinable
139157
func invokeWriteColumn(
140-
on writer: UnsafeMutablePointer<PostgresBinaryCopyFromWriter>,
158+
on writer: UnsafeMutablePointer<PostgresBinaryCopyFromWriter>,
141159
_ column: (some PostgresEncodable)?
142160
) throws {
143161
try writer.pointee.writeColumn(column)
@@ -169,17 +187,8 @@ public struct PostgresBinaryCopyFromWriter: ~Copyable {
169187
let columnIndex = buffer.writerIndex
170188
buffer.writeInteger(UInt16(0))
171189

172-
let columns = try withUnsafeMutablePointer(to: &self) { pointerToSelf in
173-
// Important: We need to ensure that `pointerToSelf` (and thus `ColumnWriter`) does not exceed the lifetime
174-
// of `self` because it is holding an unsafe reference to it.
175-
//
176-
// We achieve this because `ColumnWriter` is non-Copyable and thus the client can't store a copy to it.
177-
// Furthermore, `columnWriter` is destroyed before the end of `withUnsafeMutablePointer`, which holds `self`
178-
// alive.
179-
var columnWriter = ColumnWriter(underlying: pointerToSelf)
180-
190+
let columns = try ColumnWriter.withColumnWriter(writingTo: &self) { columnWriter in
181191
try body(&columnWriter)
182-
183192
return columnWriter.columns
184193
}
185194

@@ -212,6 +221,7 @@ public struct PostgresBinaryCopyFromWriter: ~Copyable {
212221
buffer.clear()
213222
}
214223
}
224+
#endif
215225

216226
/// Specifies the format in which data is transferred to the backend in a COPY operation.
217227
///
@@ -289,6 +299,7 @@ private func buildCopyFromQuery(
289299
}
290300

291301
extension PostgresConnection {
302+
#if compiler(>=6.2)
292303
/// Copy data into a table using a `COPY <table name> FROM STDIN` query, transferring data in a binary format.
293304
///
294305
/// - Parameters:
@@ -331,6 +342,7 @@ extension PostgresConnection {
331342
try await binaryWriter.flush()
332343
}
333344
}
345+
#endif
334346

335347
/// Copy data into a table using a `COPY <table name> FROM STDIN` query.
336348
///

Tests/PostgresNIOTests/New/PostgresConnectionTests.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -936,6 +936,7 @@ import Synchronization
936936
}
937937
}
938938

939+
#if compiler(>=6.2) // copyFromBinary is only available in Swift 6.2+
939940
@Test func testCopyFromBinary() async throws {
940941
try await self.withAsyncTestingChannel { connection, channel in
941942
try await withThrowingTaskGroup(of: Void.self) { taskGroup async throws -> Void in
@@ -997,6 +998,7 @@ import Synchronization
997998
}
998999
}
9991000
}
1001+
#endif
10001002

10011003
func withAsyncTestingChannel(_ body: (PostgresConnection, NIOAsyncTestingChannel) async throws -> ()) async throws {
10021004
let eventLoop = NIOAsyncTestingEventLoop()

0 commit comments

Comments
 (0)