Files
SwiftDBAI/README.md
Krishna Kumar fcd752466a SwiftDBAI: natural language queries for any SQLite database
Drop-in SwiftUI chat view, headless ChatEngine, LLM-agnostic via
AnyLanguageModel. Read-only by default with configurable allowlists.
Robust SQL parser with 63 tests. Includes demo app with GitHub stars dataset.
2026-04-05 17:11:12 -05:00

347 lines
9.7 KiB
Markdown

# SwiftDBAI
A Swift package that adds a natural language query interface to any SQLite database in your iOS, macOS, or visionOS app. Drop in one SwiftUI view and your users can ask questions about their data in plain English.
<!-- badges -->
![Swift 6.1+](https://img.shields.io/badge/Swift-6.1+-orange.svg)
![Platforms](https://img.shields.io/badge/Platforms-iOS%2017%20|%20macOS%2014%20|%20visionOS%201-blue.svg)
![License](https://img.shields.io/badge/License-MIT-green.svg)
## Demo
| iPhone | iPad |
|---|---|
| ![iPhone](screenshots/iphone-results.png) | ![iPad](screenshots/results-chart.png) |
| Custom theme | Sheet presentation |
|---|---|
| ![Custom theme](screenshots/custom-theme.png) | ![Sheet presentation](screenshots/sheet-presentation.png) |
The demo app is at `Example/SwiftDBAIDemo/`. It points SwiftDBAI at a real database of ~2,000 top GitHub repos with live star counts. Generate the Xcode project with [xcodegen](https://github.com/yonaskolb/XcodeGen):
```
cd Example/SwiftDBAIDemo && xcodegen generate
```
For a real-world integration, see [SwiftDBAI added to NetNewsWire](https://github.com/krishkumar/NetNewsWire) -- natural language queries against an RSS reader's article database.
## Features
- Drop-in SwiftUI chat view (`DataChatView`) -- one line to add a database chat UI
- Headless `ChatEngine` for programmatic / non-UI use
- LLM-agnostic via [AnyLanguageModel](https://github.com/huggingface/AnyLanguageModel) -- works with OpenAI, Anthropic, Gemini, Ollama, llama.cpp, or any OpenAI-compatible endpoint
- Automatic schema introspection -- no manual annotations required
- Safety-first: read-only by default, operation allowlists, table-level mutation policies, destructive operation confirmation delegate
- Configurable query timeouts, context windows, and custom validators
## Installation
Add SwiftDBAI via Swift Package Manager:
```swift
dependencies: [
.package(url: "https://github.com/krishkumar/SwiftDBAI.git", from: "1.0.0"),
]
```
Then add the dependency to your target:
```swift
.target(
name: "MyApp",
dependencies: ["SwiftDBAI"]
)
```
## Quick Start
Drop a full chat UI into any SwiftUI view with `DataChatView`:
```swift
import SwiftDBAI
import AnyLanguageModel
struct ContentView: View {
var body: some View {
DataChatView(
databasePath: "/path/to/mydata.sqlite",
model: OllamaLanguageModel(model: "llama3")
)
}
}
```
That's it. `DataChatView` opens the database, introspects the schema, and renders a chat interface. The default mode is **read-only** (SELECT only).
To pass an existing GRDB connection and customize behavior:
```swift
DataChatView(
database: myDatabasePool,
model: OpenAILanguageModel(apiKey: "sk-...", model: "gpt-4o"),
allowlist: .standard,
additionalContext: "This database stores a recipe app's data.",
maxSummaryRows: 100
)
```
## Presentation
`DataChatSheet` wraps `DataChatView` in a `NavigationStack` with a title and Done button, ready for any presentation context.
**SwiftUI sheet:**
```swift
.sheet(isPresented: $showChat) {
DataChatSheet(
databasePath: "/path/to/mydata.sqlite",
model: OllamaLanguageModel(model: "llama3")
)
}
// Or use the convenience modifier:
.dataChatSheet(isPresented: $showChat, databasePath: path, model: myLLM)
```
**SwiftUI full-screen cover:**
```swift
.fullScreenCover(isPresented: $showChat) {
DataChatSheet(databasePath: path, model: myLLM)
}
// Or use the convenience modifier:
.dataChatFullScreen(isPresented: $showChat, databasePath: path, model: myLLM)
```
**UIKit modal:**
```swift
let vc = DataChatViewController(databasePath: path, model: myLLM)
present(vc, animated: true)
```
**UIKit navigation push:**
```swift
let vc = DataChatViewController(databasePath: path, model: myLLM)
navigationController?.pushViewController(vc, animated: true)
```
All presentation wrappers accept the same parameters as `DataChatView` (`allowlist`, `additionalContext`, etc.) plus a `title` for the navigation bar.
## Tool Calling
If your app already has an LLM integration, use `DatabaseTool` to register SwiftDBAI as a tool the LLM can call. No extra LLM needed -- your existing one generates SQL, SwiftDBAI validates and executes it.
```swift
import SwiftDBAI
// 1. Create the tool
let tool = try await DatabaseTool(databasePath: "/path/to/mydata.sqlite")
// 2. Add schema context to your LLM's system prompt
let systemPrompt = "You are a helpful assistant.\n\n" + tool.systemPromptSnippet
// 3. Register with your LLM (OpenAI function calling example)
let functionDef = tool.openAIFunctionDefinition
// Pass to OpenAI's tools parameter...
// 4. When the LLM calls the tool
let result = try tool.execute(sql: "SELECT * FROM users WHERE active = 1")
result.jsonString // return to LLM as tool response
result.markdownTable // display to user
result.rowCount // 42
result.executionTime // 0.003
```
SQL is validated against a read-only allowlist before execution. INSERT, UPDATE, DELETE, and DROP are rejected.
![DatabaseTool API](screenshots/tool-api.png)
## Headless / Programmatic Use
Use `ChatEngine` directly when you don't need a UI:
```swift
import SwiftDBAI
import AnyLanguageModel
import GRDB
let pool = try DatabasePool(path: "/path/to/mydata.sqlite")
let engine = ChatEngine(
database: pool,
model: OpenAILanguageModel(apiKey: "sk-...", model: "gpt-4o")
)
let response = try await engine.send("How many users signed up this week?")
print(response.summary) // "There were 42 new signups this week."
print(response.sql) // Optional("SELECT COUNT(*) FROM users WHERE ...")
```
`ChatEngine` also accepts a `ProviderConfiguration` for convenience:
```swift
let engine = ChatEngine(
database: pool,
provider: .anthropic(apiKey: "sk-ant-...", model: "claude-sonnet-4-20250514")
)
```
For fine-grained control, pass a `ChatEngineConfiguration`:
```swift
var config = ChatEngineConfiguration(
queryTimeout: 10,
contextWindowSize: 20,
maxSummaryRows: 100,
additionalContext: "The 'status' column uses: 'active', 'inactive', 'suspended'."
)
let engine = ChatEngine(
database: pool,
model: model,
allowlist: .standard,
configuration: config
)
```
## Choosing a Provider
SwiftDBAI works with any provider supported by AnyLanguageModel. Use `ProviderConfiguration` factory methods or construct model instances directly.
```swift
// OpenAI
let config = ProviderConfiguration.openAI(apiKey: "sk-...", model: "gpt-4o")
// Anthropic
let config = ProviderConfiguration.anthropic(apiKey: "sk-ant-...", model: "claude-sonnet-4-20250514")
// Gemini
let config = ProviderConfiguration.gemini(apiKey: "AIza...", model: "gemini-2.0-flash")
// Ollama (local, no API key needed)
let config = ProviderConfiguration.ollama(model: "llama3.2")
// llama.cpp (local)
let config = ProviderConfiguration.llamaCpp(model: "default")
// Any OpenAI-compatible endpoint
let config = ProviderConfiguration.openAICompatible(
apiKey: "your-key",
model: "llama-3.1-70b",
baseURL: URL(string: "https://api.together.xyz/v1/")!
)
```
Use with ChatEngine:
```swift
let engine = ChatEngine(database: pool, provider: config)
// or
let engine = ChatEngine(database: pool, model: config.makeModel())
```
API keys can also come from environment variables:
```swift
let config = ProviderConfiguration.fromEnvironment(
provider: .openAI,
environmentVariable: "OPENAI_API_KEY",
model: "gpt-4o"
)
```
## Safety and Mutation Control
### Operation Allowlist
By default, only SELECT queries are allowed. Opt in to writes explicitly:
| Preset | Allowed Operations |
|---|---|
| `.readOnly` (default) | SELECT |
| `.standard` | SELECT, INSERT, UPDATE |
| `.unrestricted` | SELECT, INSERT, UPDATE, DELETE |
```swift
// Custom allowlist
let allowlist = OperationAllowlist([.select, .insert])
```
### Mutation Policy
For table-level control, use `MutationPolicy`:
```swift
// Allow INSERT and UPDATE only on specific tables
let policy = MutationPolicy(
allowedOperations: [.insert, .update],
allowedTables: ["orders", "order_items"]
)
let engine = ChatEngine(
database: pool,
model: model,
mutationPolicy: policy
)
```
Presets: `.readOnly`, `.readWrite`, `.unrestricted`.
### Confirmation Delegate
Destructive operations (DELETE, DROP, ALTER, TRUNCATE) require confirmation through a `ToolExecutionDelegate`:
```swift
struct MyDelegate: ToolExecutionDelegate {
func confirmDestructiveOperation(
_ context: DestructiveOperationContext
) async -> Bool {
// Present confirmation UI, return true to proceed
return await showConfirmationDialog(context.description)
}
}
let engine = ChatEngine(
database: pool,
model: model,
allowlist: .unrestricted,
delegate: MyDelegate()
)
```
Without a delegate, destructive operations throw `SwiftDBAIError.confirmationRequired` so you can handle confirmation in your own flow.
Built-in delegates: `AutoApproveDelegate` (testing only), `RejectAllDelegate` (safest).
## Architecture
```
User Question
|
v
ChatEngine
|-- SchemaIntrospector (auto-discovers tables, columns, keys, indexes)
|-- PromptBuilder (builds LLM system prompt with schema context)
|-- LanguageModel (generates SQL via AnyLanguageModel)
|-- SQLQueryParser (parses and validates against allowlist/policy)
|-- QueryValidator (optional custom validators)
|-- GRDB (executes SQL against SQLite)
|-- TextSummaryRenderer (summarizes results via LLM)
v
ChatResponse { summary, sql, queryResult }
```
`DataChatView` wraps this pipeline in a SwiftUI view with `ChatViewModel` managing state.
## Requirements
- iOS 17.0+ / macOS 14.0+ / visionOS 1.0+
- Swift 6.1+
- Xcode 16+
## License
MIT. See [LICENSE](LICENSE) for details.