database-driver-design について
Swift でデータベースクライアントライブラリを開発する人向けのスキルです。ワイヤープロトコル、型安全な API、接続管理、Swift Concurrency を扱い、クエリの安全性、状態機械、バックプレッシャー、接続プールを重視します。
主な機能
- 01型安全なコマンドとクエリの設計
- 02パラメーター化クエリの安全性
- 03ワイヤープロトコルのエンコードとデコード
- 04接続状態機械と接続プール
- 05バックプレッシャー対応の結果ストリーミング
- 06Swift Concurrency と NIO の連携
活用シーン
- 01データベースドライバー API の設計
- 02SQL クエリのインジェクション防止
- 03データベースワイヤープロトコルの実装
- 04接続プールとストリーミング結果の管理
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ wendylabsinc/ claude-skills --skill database-driver-designインストール後、Claude Code で/database-driver-designを入力して読み込まれたか確認します。
詳細
データベースドライバーの設計
このスキルでは、ワイヤープロトコルの実装、接続管理、型安全な API、Swift Concurrency との統合など、Swift で本番品質のデータベースクライアントライブラリを構築するための専門的なガイダンスを提供します。パターンは、優れた実装である valkey-swift と postgres-nio を基にしています。
エージェントの行動規範(以下のルールに従ってください)
- パラメーター化クエリを優先する - ユーザー入力を SQL/コマンド文字列に連結しないでください
- 文字列補間で安全性を確保する -
ExpressibleByStringInterpolationを実装し、値をバインディングに変換します - コマンドを型として設計する - 各コマンド/クエリを、関連するレスポンス型を持つ構造体にします
- プロトコルには状態機械を実装する - 複雑な接続ライフサイクルには、明示的な状態遷移が必要です
- バックプレッシャーをサポートする - 行/結果のストリーミングでは、コンシューマーの要求に従う必要があります
- アクターのエグゼキューターを整合させる -
unownedExecutorを使い、NIO イベントループに整合させます - 接続プールを適切に管理する - キープアライブ、アイドルタイムアウト、正常なシャットダウンを実装します
コアパターン
型としてのコマンドパターン
コンパイル時の安全性を確保するため、関連するレスポンス型を持つ型としてコマンドを定義します。
public protocol DatabaseCommand: Sendable, Hashable {
associatedtype Response: Decodable
static var name: String { get }
func encode(into encoder: inout CommandEncoder)
}
public struct GET: DatabaseCommand {
public typealias Response = String?
public static var name: String { "GET" }
public let key: String
public func encode(into encoder: inout CommandEncoder) {
encoder.encode(Self.name, key)
}
}
文字列補間による SQL インジェクション対策
public struct Query: ExpressibleByStringInterpolation {
public var sql: String
public var bindings: Bindings
public struct StringInterpolation: StringInterpolationProtocol {
var sql: String = ""
var bindings: Bindings = Bindings()
public mutating func appendLiteral(_ literal: String) {
sql.append(literal)
}
public mutating func appendInterpolation<T: Encodable>(_ value: T) {
bindings.append(value)
sql.append("$\(bindings.count)")
}
}
}
// Usage: let query: Query = "SELECT * FROM users WHERE id = \(userId)"
// Result: sql = "SELECT * FROM users WHERE id = $1", bindings = [userId]
NIO エグゼキューターと整合するアクター
アクターのエグゼキューターを NIO イベントループに整合させ、コンテキストスイッチをなくします。
public final actor Connection: Sendable {
nonisolated public let unownedExecutor: UnownedSerialExecutor
init(channel: any Channel) {
self.unownedExecutor = channel.eventLoop.executor.asUnownedSerialExecutor()
}
}
アクションパターンを用いた状態機械
明示的な遷移とアクションにより、複雑なプロトコル状態を管理します。
struct ConnectionStateMachine {
enum State {
case idle
case executing(QueryStateMachine)
case closing
case closed
case modifying // Prevents COW during mutations
}
enum Action {
case sendMessage(Message)
case forwardResult(Result)
case closeConnection
case none
}
private var state: State = .idle
mutating func handle(_ message: Message) -> Action {
switch (state, message) {
case (.idle, .query(let q)):
state = .executing(QueryStateMachine(q))
return .sendMessage(.parse(q))
// ... other transitions
}
}
}
長さプレフィックス付きバイナリーエンコーディング
プレースホルダーと後からの書き戻しを使って、長さプレフィックス付きデータを書き込みます。
extension Encodable {
func encodeRaw(into buffer: inout ByteBuffer) throws {
// Write placeholder for length (4 bytes for Int32)
let lengthIndex = buffer.writerIndex
buffer.writeInteger(Int32(0))
// Record position before encoding
let startIndex = buffer.writerIndex
// Encode the actual value
try self.encode(into: &buffer)
// Calculate and write actual length
let length = buffer.writerIndex - startIndex
buffer.setInteger(Int32(length), at: lengthIndex)
}
}
エンコード/デコード用のプロトコル階層
異なるエンコード保証に対応する、階層化されたプロトコルを設計します。
// Base: runtime-determined type, may throw
public protocol ThrowingDynamicTypeEncodable: Sendable {
func encode(into byteBuffer: inout ByteBuffer) throws
var dataType: DataType { get }
}
// Non-throwing variant
public protocol DynamicTypeEncodable: ThrowingDynamicTypeEncodable {
func encode(into byteBuffer: inout ByteBuffer)
}
// Static type info known at compile time
public protocol StaticTypeEncodable: ThrowingDynamicTypeEncodable {
static var dataType: DataType { get }
}
// Non-throwing + static type info (most efficient)
public protocol NonThrowingEncodable: StaticTypeEncodable, DynamicTypeEncodable {}
可変個のジェネリックを用いた行のデコード
パラメーターパックを使い、複数のカラムを型安全にデコードします。
extension Row {
func decode<each T: Decodable>(
_ types: (repeat each T).Type
) throws -> (repeat each T) {
var index = 0
return (repeat try decodeColumn((each T).self, at: &index))
}
}
// Usage: let (id, name, email) = try row.decode((Int.self, String.self, String.self))
バックプレッシャーに対応したストリーミング
結果のストリーミングに適応型バッファー戦略を実装します。
struct AdaptiveBuffer: BackPressureStrategy {
var lowWatermark: Int
var highWatermark: Int
var currentTarget: Int
mutating func didYield(bufferDepth: Int) -> Bool {
// Shrink target if buffer too deep
if bufferDepth > currentTarget * 2 {
currentTarget = max(lowWatermark, currentTarget / 2)
}
return bufferDepth < currentTarget
}
mutating func didConsume(bufferDepth: Int) -> Bool {
// Grow target if buffer drains completely
if bufferDepth == 0 {
currentTarget = min(highWatermark, currentTarget * 2)
}
return bufferDepth < currentTarget
}
}
接続プールとの統合
接続をプール用プロトコルに準拠させます。
extension Connection: PooledConnection {
public typealias ConnectionID = Int
}
struct KeepAliveBehavior: ConnectionKeepAliveBehavior {
typealias Connection = Connection
let frequency: Duration
func runKeepAlive(for connection: Connection) async throws {
_ = try await connection.ping()
}
}
final class ClientMetrics: ConnectionPoolObservabilityDelegate {
func connectionCreated(id: Int) { /* metrics */ }
func connectionLeased(id: Int) { /* metrics */ }
func connectionReleased(id: Int) { /* metrics */ }
func connectionClosed(id: Int) { /* metrics */ }
}
深さ制限付きの解析
ネストされた構造によるスタックオーバーフローを防ぎます。
mutating func parseToken(maxDepth: Int = 100) throws -> Token {
guard maxDepth > 0 else {
throw ParsingError.tooDeeplyNested
}
switch tokenType {
case .array:
var elements: [Token] = []
for _ in 0..<count {
elements.append(try parseToken(maxDepth: maxDepth - 1))
}
return .array(elements)
// ... other cases
}
}
簡易判断ツリー
-
ワイヤープロトコルのエンコード/デコードを実装していますか?
- プレースホルダーと後からの書き戻しを使う、長さプレフィックス付きメッセージを使用します
- プロトコルトークンを、関連値を持つ列挙型として定義します
- ネストされた構造に深さ制限を実装します
-
型安全なクエリ API を設計していますか?
- インジェクション対策に
ExpressibleByStringInterpolationを使用します - 型変換のための
Encodable/Decodableプロトコル階層を作成します - 複数カラムの行のデコードには可変個のジェネリックを使用します
- インジェクション対策に
-
接続を管理していますか?
- NIO エグゼキューターと整合するアクターを使用します
- 階層型状態機械を実装します
- リクエスト ID によるキャンセルをサポートします
-
接続プールを実装していますか?
PooledConnectionプロトコルに準拠させます- キープアライブ動作を実装します
- オブザーバビリティデリゲートを通じてメトリクスを追跡します
トリアージ優先プレイブック
-
「SQL インジェクションの脆弱性」
- クエリ型に
ExpressibleByStringInterpolationを実装します - 補間された値を
$1, $2...のパラメータープレースホルダーに変換します
- クエリ型に
-
「結果のデコード時に型が一致しない」
- 型付き throws を備えた
Decodableプロトコルを定義します - カラム/セルのコンテキストを付けてエラーをラップします
- 型付き throws を備えた
-
「接続状態の破損」
- 明示的な遷移を持つ階層型状態機械を使用します
- COW の問題を防ぐため、
.modifyingセンチネルを追加します
-
「バックプレッシャーが機能しない」
- 適応型バッファー戦略を実装します
- データソースプロトコルを通じて需要を通知します
ベストプラクティスのまとめ
- コマンド/クエリを型として表現する - 各操作を、関連するレスポンス型を持つ構造体にします
- 文字列補間による安全性の確保 - 設計によってインジェクションを防ぎます
- エンコーディングのためのプロトコル階層 - 異なる保証(エラーをスローする、静的型付けなど)を提供します
- 長さプレフィックス付きワイヤーフォーマット - プレースホルダーを書き込み、エンコードしてから長さを後から埋めます
- 階層型ステートマシン - 子ステートマシンを組み合わせて複雑なプロトコルを構成します
- 適応型バックプレッシャー - コンシューマーの処理速度に応じてバッファーの目標値を動的に調整します
- Actor と NIO の連携 -
unownedExecutorを使ってコンテキストスイッチをなくします - Cell/Row ラッパー - エラーメッセージを改善する豊富なメタデータを提供します
- プリペアドステートメントのキャッシュ - 同時に行われる準備処理の重複を排除します
- グレースフルシャットダウン - 接続を閉じる前に保留中の操作を完了させます
参考になる主なライブラリ
- valkey-swift (github.com/valkey-io/valkey-swift) - 優れた RESP3 プロトコル、クラスターサポート、パブリッシュ/サブスクライブ機能を提供します
- postgres-nio (github.com/vapor/postgres-nio) - 優れたクエリの安全性、型変換、ステートマシンを備えています