Files
SwiftDBAI/Sources/SwiftDBAI/Config/MutationPolicy.swift
Krishna Kumar b1724fe7ca Initial implementation of SwiftDBAI
Chat with any SQLite database using natural language. Built on
AnyLanguageModel (HuggingFace) for LLM-agnostic provider support
and GRDB for SQLite access.

Core features:
- Auto schema introspection from sqlite_master (zero config)
- NL → SQL generation via any AnyLanguageModel provider
- Three rendering modes: text summary, data table, Swift Charts
- Drop-in DataChatView (SwiftUI) and headless ChatEngine
- Operation allowlist with read-only default
- Mutation policy with per-table control
- ToolExecutionDelegate for destructive operation confirmation
- Multi-turn conversation context
- 352 tests across 24 suites, all passing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 09:30:56 -05:00

149 lines
5.7 KiB
Swift

// MutationPolicy.swift
// SwiftDBAI
//
// Defines which mutation operations are permitted and optionally restricts
// them to specific tables. Wraps OperationAllowlist with table-level granularity.
import Foundation
/// Controls which SQL mutation operations the LLM may generate and,
/// optionally, which tables those mutations may target.
///
/// `MutationPolicy` builds on ``OperationAllowlist`` by adding per-table
/// restrictions. The default policy is **read-only** no mutations are
/// allowed on any table. Write operations require explicit opt-in.
///
/// ```swift
/// // Read-only (default) only SELECT is allowed
/// let readOnly = MutationPolicy.readOnly
///
/// // Allow INSERT and UPDATE on specific tables only
/// let restricted = MutationPolicy(
/// allowedOperations: [.insert, .update],
/// allowedTables: ["orders", "order_items"]
/// )
///
/// // Allow INSERT and UPDATE on all tables
/// let broad = MutationPolicy(allowedOperations: [.insert, .update])
///
/// // Full access including DELETE (requires confirmation)
/// let full = MutationPolicy.unrestricted
/// ```
public struct MutationPolicy: Sendable, Equatable {
// MARK: - Properties
/// The underlying operation allowlist (always includes SELECT).
public let operationAllowlist: OperationAllowlist
/// Optional set of table names that mutations may target.
///
/// When `nil`, mutations are allowed on all tables (subject to
/// ``operationAllowlist``). When non-nil, mutation operations
/// (INSERT, UPDATE, DELETE) are only permitted on the listed tables.
/// SELECT queries are never restricted by this property.
public let allowedMutationTables: Set<String>?
/// When `true`, destructive operations (DELETE) require explicit user
/// confirmation before execution, even when the operation is allowed.
/// Defaults to `true`.
public let requiresDestructiveConfirmation: Bool
// MARK: - Initialization
/// Creates a mutation policy with the given operations and optional table restrictions.
///
/// SELECT is always implicitly included you cannot create a policy
/// that disallows reads.
///
/// - Parameters:
/// - allowedOperations: The mutation operations to permit (INSERT, UPDATE, DELETE).
/// SELECT is always allowed regardless of this parameter.
/// - allowedTables: Optional set of table names mutations may target.
/// Pass `nil` to allow mutations on all tables. Defaults to `nil`.
/// - requiresDestructiveConfirmation: Whether DELETE requires user confirmation.
/// Defaults to `true`.
public init(
allowedOperations: Set<SQLOperation> = [],
allowedTables: Set<String>? = nil,
requiresDestructiveConfirmation: Bool = true
) {
// Always include SELECT
var ops = allowedOperations
ops.insert(.select)
self.operationAllowlist = OperationAllowlist(ops)
self.allowedMutationTables = allowedTables
self.requiresDestructiveConfirmation = requiresDestructiveConfirmation
}
// MARK: - Presets
/// Read-only policy: only SELECT queries are allowed. This is the default.
public static let readOnly = MutationPolicy()
/// Standard read-write: SELECT, INSERT, and UPDATE on all tables.
public static let readWrite = MutationPolicy(
allowedOperations: [.insert, .update]
)
/// Unrestricted: all operations including DELETE on all tables.
/// DELETE still requires confirmation by default.
public static let unrestricted = MutationPolicy(
allowedOperations: [.insert, .update, .delete]
)
// MARK: - Validation
/// Returns `true` if the given operation is permitted by this policy.
public func isOperationAllowed(_ operation: SQLOperation) -> Bool {
operationAllowlist.isAllowed(operation)
}
/// Returns `true` if the given mutation operation is permitted on the
/// specified table.
///
/// SELECT operations always return `true` regardless of table restrictions.
/// For mutation operations, this checks both the operation allowlist and
/// the table restrictions (if any).
///
/// - Parameters:
/// - operation: The SQL operation type.
/// - table: The target table name (case-insensitive comparison).
/// - Returns: Whether the operation is allowed on the given table.
public func isAllowed(operation: SQLOperation, on table: String) -> Bool {
// SELECT is always allowed
guard operation != .select else { return true }
// Check operation allowlist first
guard operationAllowlist.isAllowed(operation) else { return false }
// If no table restrictions, the operation is allowed
guard let allowedTables = allowedMutationTables else { return true }
// Case-insensitive table name check
let lowerTable = table.lowercased()
return allowedTables.contains { $0.lowercased() == lowerTable }
}
/// Returns `true` if the given operation requires user confirmation.
public func requiresConfirmation(for operation: SQLOperation) -> Bool {
operation == .delete && requiresDestructiveConfirmation
}
/// Returns a human-readable description for inclusion in the LLM system prompt.
func describeForLLM() -> String {
var desc = operationAllowlist.describeForLLM()
if let tables = allowedMutationTables, !tables.isEmpty {
let sorted = tables.sorted()
desc += " Mutations (INSERT/UPDATE/DELETE) are restricted to these tables only: \(sorted.joined(separator: ", "))."
}
if requiresDestructiveConfirmation && operationAllowlist.isAllowed(.delete) {
desc += " DELETE operations require user confirmation before execution."
}
return desc
}
}