Skip to content

Commit dcfc35f

Browse files
vkuttypCopilot
andcommitted
feat: upgrade to Swift 6 strict concurrency (swift-tools-version 6.0)
- Bump swift-tools-version from 5.9 to 6.0 - Replace .enableUpcomingFeature("StrictConcurrency") with .swiftLanguageMode(.v6) - Drop redundant .enableUpcomingFeature("ExistentialAny") (default in Swift 6) - Add @preconcurrency imports for NIOCore/NIOPosix/NIOSSL across all connection files - Use channel.eventLoop.submit + syncOperations for NIOSSLHandler and ByteToMessageHandler (both explicitly mark Sendable unavailable as event-loop-bound types) - Restructure ClientBootstrap usage to capture value-type host/port instead of the non-Sendable bootstrap object in the @sendable timeout closure - Add SQLNioCore/_UnsafeSendable bridge type for safely moving event-loop-bound NIO handlers across the Swift 6 concurrency boundary into syncOperations calls - Fix test: use @unchecked Sendable MutableList class for onNotice accumulator Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent fd5dc44 commit dcfc35f

6 files changed

Lines changed: 75 additions & 41 deletions

File tree

Package.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version: 5.9
1+
// swift-tools-version: 6.0
22
import PackageDescription
33

44
// SQLite is provided by the Apple SDK on Darwin; on Linux we need a system library.
@@ -148,6 +148,5 @@ let package = Package(
148148
)
149149

150150
var swiftSettings: [SwiftSetting] { [
151-
.enableUpcomingFeature("ExistentialAny"),
152-
.enableUpcomingFeature("StrictConcurrency"),
151+
.swiftLanguageMode(.v6),
153152
] }

Sources/MSSQLNio/MSSQLConnection.swift

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import NIOCore
2-
import NIOPosix
3-
import NIOSSL
1+
@preconcurrency import NIOCore
2+
@preconcurrency import NIOPosix
3+
@preconcurrency import NIOSSL
44
import Logging
55
import SQLNioCore
66
import Foundation
@@ -112,14 +112,14 @@ public final class MSSQLConnection: SQLDatabase, @unchecked Sendable {
112112
public static func connect(
113113
configuration: Configuration,
114114
eventLoopGroup: any EventLoopGroup = MultiThreadedEventLoopGroup.singleton
115-
) async throws -> MSSQLConnection {
116-
let bootstrap = ClientBootstrap(group: eventLoopGroup)
117-
.channelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_KEEPALIVE), value: 1)
118-
.channelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1)
119-
120-
let channel = try await mssqlWithTimeout(configuration.connectTimeout) {
121-
try await bootstrap.connect(host: configuration.host,
122-
port: configuration.port).get()
115+
) async throws -> MSSQLConnection { let channel = try await mssqlWithTimeout(configuration.connectTimeout) {
116+
// Swift 6: ClientBootstrap is not Sendable; capture host/port as value types instead.
117+
let host = configuration.host
118+
let port = configuration.port
119+
return try await ClientBootstrap(group: eventLoopGroup)
120+
.channelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_KEEPALIVE), value: 1)
121+
.channelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1)
122+
.connect(host: host, port: port).get()
123123
}
124124

125125
let conn = MSSQLConnection(channel: channel,
@@ -143,11 +143,12 @@ public final class MSSQLConnection: SQLDatabase, @unchecked Sendable {
143143
// 1. Add pipeline: TDSTLSFramer (pass-through initially) + framing + bridge
144144
let b = AsyncChannelBridge()
145145
bridge = b
146-
try await channel.pipeline.addHandlers([
147-
tlsFramer,
148-
ByteToMessageHandler(TDSFramingHandler()),
149-
b,
150-
]).get()
146+
// Swift 6: ByteToMessageHandler has Sendable marked unavailable (event-loop-bound).
147+
let frameBox = _UnsafeSendable(ByteToMessageHandler(TDSFramingHandler()))
148+
let framer = tlsFramer
149+
try await channel.eventLoop.submit {
150+
try self.channel.pipeline.syncOperations.addHandlers([framer, frameBox.value, b])
151+
}.get()
151152

152153
// 2. Pre-Login — negotiate encryption preference
153154
let preLoginResp = try await sendPreLogin()
@@ -225,8 +226,17 @@ public final class MSSQLConnection: SQLDatabase, @unchecked Sendable {
225226
// Insert NIOSSLClientHandler and tracker between TDSTLSFramer and TDSFramingHandler.
226227
// Because the channel is already active, NIOSSLClientHandler's handlerAdded()
227228
// triggers the TLS handshake automatically.
228-
try await channel.pipeline.addHandler(sslHandler, position: .after(tlsFramer)).get()
229-
try await channel.pipeline.addHandler(tracker, position: .after(sslHandler)).get()
229+
//
230+
// Swift 6: NIOSSLHandler explicitly marks Sendable unavailable (it is event-loop-bound).
231+
// We use syncOperations (no Sendable requirement) from within an event-loop submit block,
232+
// bridging with an @unchecked Sendable box since we immediately hand ownership to the loop.
233+
let sslBox = _UnsafeSendable(sslHandler)
234+
try await channel.eventLoop.submit {
235+
try self.channel.pipeline.syncOperations.addHandler(
236+
sslBox.value, position: .after(self.tlsFramer))
237+
try self.channel.pipeline.syncOperations.addHandler(
238+
tracker, position: .after(sslBox.value))
239+
}.get()
230240

231241
// Wait for TLS handshake to complete (tracker fulfils promise + deactivates framer)
232242
try await promise.futureResult.get()

Sources/MySQLNio/MySQLConnection.swift

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import NIOCore
2-
import NIOPosix
3-
import NIOSSL
1+
@preconcurrency import NIOCore
2+
@preconcurrency import NIOPosix
3+
@preconcurrency import NIOSSL
44
import Logging
55
import SQLNioCore
66
import Foundation
@@ -97,10 +97,11 @@ public final class MySQLConnection: SQLDatabase, @unchecked Sendable {
9797
private func handshake() async throws {
9898
let b = AsyncChannelBridge()
9999
bridge = b
100-
try await channel.pipeline.addHandlers([
101-
ByteToMessageHandler(MySQLFramingHandler()),
102-
b,
103-
]).get()
100+
// Swift 6: ByteToMessageHandler has Sendable marked unavailable (event-loop-bound).
101+
let frameBox = _UnsafeSendable(ByteToMessageHandler(MySQLFramingHandler()))
102+
try await channel.eventLoop.submit {
103+
try self.channel.pipeline.syncOperations.addHandlers([frameBox.value, b])
104+
}.get()
104105

105106
// 1. Receive server handshake
106107
var serverHSPacket = try await receivePacket()
@@ -154,7 +155,11 @@ public final class MySQLConnection: SQLDatabase, @unchecked Sendable {
154155
// SNI requires a hostname, not an IP address
155156
let sni = config.host.first?.isNumber == false ? config.host : nil
156157
let sslHandler = try NIOSSLClientHandler(context: sslContext, serverHostname: sni)
157-
try await channel.pipeline.addHandler(sslHandler, position: .first).get()
158+
// Swift 6: NIOSSLHandler has Sendable marked unavailable (event-loop-bound).
159+
let sslBox = _UnsafeSendable(sslHandler)
160+
try await channel.eventLoop.submit {
161+
try self.channel.pipeline.syncOperations.addHandler(sslBox.value, position: .first)
162+
}.get()
158163
}
159164

160165
private func sendHandshakeResponse(serverHS: MySQLHandshakeV10) async throws {

Sources/PostgresNio/PostgresConnection.swift

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import NIOCore
2-
import NIOPosix
3-
import NIOSSL
1+
@preconcurrency import NIOCore
2+
@preconcurrency import NIOPosix
3+
@preconcurrency import NIOSSL
44
import Logging
55
import SQLNioCore
66
import Foundation
@@ -117,10 +117,12 @@ public final class PostgresConnection: SQLDatabase, @unchecked Sendable {
117117
try await channel.pipeline.removeHandler(rawBridge).get()
118118
let b = AsyncChannelBridge()
119119
bridge = b
120-
try await channel.pipeline.addHandlers([
121-
ByteToMessageHandler(PGFramingHandler()),
122-
b,
123-
]).get()
120+
// Swift 6: ByteToMessageHandler has Sendable marked unavailable (event-loop-bound).
121+
// Use syncOperations from within the event loop to avoid the Sendable requirement.
122+
let frameBox = _UnsafeSendable(ByteToMessageHandler(PGFramingHandler()))
123+
try await channel.eventLoop.submit {
124+
try self.channel.pipeline.syncOperations.addHandlers([frameBox.value, b])
125+
}.get()
124126

125127
// 3. Startup + authentication
126128
let startup = PGFrontend.startup(user: config.username,
@@ -137,7 +139,11 @@ public final class PostgresConnection: SQLDatabase, @unchecked Sendable {
137139
let sslContext = try NIOSSLContext(configuration: tlsConfig)
138140
let sslHandler = try NIOSSLClientHandler(context: sslContext,
139141
serverHostname: config.host)
140-
try await channel.pipeline.addHandler(sslHandler, position: .first).get()
142+
// Swift 6: NIOSSLHandler has Sendable marked unavailable (event-loop-bound).
143+
let sslBox = _UnsafeSendable(sslHandler)
144+
try await channel.eventLoop.submit {
145+
try self.channel.pipeline.syncOperations.addHandler(sslBox.value, position: .first)
146+
}.get()
141147
}
142148

143149
private func authenticate() async throws {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/// Minimal @unchecked Sendable bridge for NIO channel handlers that are event-loop-bound.
2+
///
3+
/// NIO marks certain handlers (e.g. `NIOSSLHandler`, `ByteToMessageHandler`) as
4+
/// `@available(*, unavailable) Sendable` because they must remain on their event loop.
5+
/// When Swift 6 strict concurrency requires crossing a concurrency boundary to call
6+
/// `syncOperations` (which itself enforces event-loop execution), this wrapper provides
7+
/// the necessary type-level escape hatch. It is safe as long as the wrapped value is
8+
/// immediately handed to the event loop and never accessed from another isolation domain.
9+
public final class _UnsafeSendable<T>: @unchecked Sendable {
10+
public let value: T
11+
public init(_ value: T) { self.value = value }
12+
}

Tests/PostgresNioTests/PostgresIntegrationTests.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1454,8 +1454,10 @@ final class PGNoticeTests: XCTestCase, @unchecked Sendable {
14541454
func testNoticeCallback() {
14551455
pgRunAsync {
14561456
try await PGTestDatabase.withConnection { conn in
1457-
var notices: [String] = []
1458-
conn.onNotice = { msg in notices.append(msg) }
1457+
// Swift 6: @Sendable callback can't mutate a var capture; use a class ref instead.
1458+
final class MutableList: @unchecked Sendable { var items: [String] = [] }
1459+
let notices = MutableList()
1460+
conn.onNotice = { msg in notices.items.append(msg) }
14591461

14601462
// PostgreSQL RAISE NOTICE generates a notice message
14611463
_ = try await conn.execute("""
@@ -1464,8 +1466,8 @@ final class PGNoticeTests: XCTestCase, @unchecked Sendable {
14641466
END$$
14651467
""", [])
14661468

1467-
XCTAssertGreaterThan(notices.count, 0)
1468-
XCTAssertTrue(notices.joined().contains("Test notice"))
1469+
XCTAssertGreaterThan(notices.items.count, 0)
1470+
XCTAssertTrue(notices.items.joined().contains("Test notice"))
14691471
}
14701472
}
14711473
}

0 commit comments

Comments
 (0)