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>
137 lines
6.1 KiB
Swift
137 lines
6.1 KiB
Swift
// DataChatViewUsageTests.swift
|
|
// SwiftDBAITests
|
|
//
|
|
// Proves DataChatView works with minimal setup — under 10 lines of code.
|
|
// A developer only needs a GRDB connection and a LanguageModel to get a
|
|
// full chat-with-database SwiftUI view.
|
|
|
|
import Testing
|
|
import Foundation
|
|
import GRDB
|
|
@testable import SwiftDBAI
|
|
|
|
// MARK: - Minimal Setup: DataChatView in Under 10 Lines
|
|
|
|
/// This test suite proves the "zero_config_reads" principle:
|
|
/// A developer with an existing SQLite database can create a fully functional
|
|
/// chat UI by providing only a GRDB connection and a language model instance.
|
|
/// No schema files, no annotations, no manual configuration required.
|
|
@Suite("DataChatView Minimal Setup")
|
|
struct DataChatViewMinimalSetupTests {
|
|
|
|
// ┌──────────────────────────────────────────────────────────┐
|
|
// │ USAGE EXAMPLE — DataChatView in 6 lines of real code │
|
|
// │ │
|
|
// │ import SwiftDBAI │
|
|
// │ import GRDB │
|
|
// │ │
|
|
// │ let db = try DatabaseQueue(path: "mydata.sqlite") │
|
|
// │ let model = OllamaLanguageModel(model: "llama3") │
|
|
// │ │
|
|
// │ var body: some View { │
|
|
// │ DataChatView(database: db, model: model) │
|
|
// │ } │
|
|
// └──────────────────────────────────────────────────────────┘
|
|
|
|
/// Creates a temporary in-memory database with sample data for tests.
|
|
private static func makeSampleDatabase() throws -> DatabaseQueue {
|
|
let db = try DatabaseQueue()
|
|
try db.write { db in
|
|
try db.execute(sql: """
|
|
CREATE TABLE products (
|
|
id INTEGER PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
price REAL NOT NULL,
|
|
category TEXT
|
|
);
|
|
INSERT INTO products (name, price, category) VALUES ('Widget', 9.99, 'Hardware');
|
|
INSERT INTO products (name, price, category) VALUES ('Gadget', 24.99, 'Electronics');
|
|
INSERT INTO products (name, price, category) VALUES ('Doohickey', 4.99, 'Hardware');
|
|
""")
|
|
}
|
|
return db
|
|
}
|
|
|
|
@Test("DataChatView initializes from database + model in 2 lines")
|
|
@MainActor
|
|
func dataChatViewMinimalInit() throws {
|
|
// LINE 1: Create (or receive) a GRDB connection
|
|
let db = try Self.makeSampleDatabase()
|
|
// LINE 2: Create the view — that's it!
|
|
let _ = DataChatView(database: db, model: MockLanguageModel())
|
|
// The view is ready. No schema files, no annotations, no extra config.
|
|
}
|
|
|
|
@Test("DataChatView path-based init works in 1 line given a path and model")
|
|
@MainActor
|
|
func dataChatViewPathInit() throws {
|
|
// Create a temp database file
|
|
let tempDir = FileManager.default.temporaryDirectory
|
|
let dbPath = tempDir.appendingPathComponent("test_\(UUID().uuidString).sqlite").path
|
|
let db = try DatabaseQueue(path: dbPath)
|
|
try db.write { db in
|
|
try db.execute(sql: "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
|
|
}
|
|
|
|
// ONE LINE to get a full chat UI:
|
|
let _ = DataChatView(databasePath: dbPath, model: MockLanguageModel())
|
|
|
|
// Cleanup
|
|
try? FileManager.default.removeItem(atPath: dbPath)
|
|
}
|
|
|
|
@Test("ChatEngine headless usage works in 3 lines")
|
|
func chatEngineMinimalUsage() async throws {
|
|
// LINE 1: Database
|
|
let db = try Self.makeSampleDatabase()
|
|
// LINE 2: Engine
|
|
let engine = ChatEngine(database: db, model: MockLanguageModel(responseText: "SELECT COUNT(*) AS total FROM products"))
|
|
// LINE 3: Schema preparation verifies auto-introspection works
|
|
let schema = try await engine.prepareSchema()
|
|
|
|
// The engine auto-discovered the schema — no manual config needed
|
|
#expect(schema.tableNames.contains("products"))
|
|
#expect(schema.tableNames.count == 1)
|
|
}
|
|
|
|
@Test("ChatViewModel works with zero configuration beyond db + model")
|
|
@MainActor
|
|
func chatViewModelMinimalUsage() async throws {
|
|
let db = try Self.makeSampleDatabase()
|
|
let engine = ChatEngine(database: db, model: MockLanguageModel())
|
|
let viewModel = ChatViewModel(engine: engine)
|
|
|
|
// Prepare triggers auto-schema-introspection
|
|
await viewModel.prepare()
|
|
|
|
#expect(viewModel.schemaReadiness.isReady)
|
|
#expect(viewModel.messages.isEmpty) // Clean slate, ready to chat
|
|
}
|
|
|
|
@Test("Default configuration is read-only (safe by default)")
|
|
@MainActor
|
|
func defaultIsReadOnly() throws {
|
|
let db = try Self.makeSampleDatabase()
|
|
// No allowlist specified — defaults to .readOnly
|
|
let _ = DataChatView(database: db, model: MockLanguageModel())
|
|
// This compiles and works. SELECT-only is the safe default.
|
|
// Developer must explicitly opt in to writes:
|
|
// DataChatView(database: db, model: model, allowlist: .standard)
|
|
}
|
|
|
|
@Test("Full DataChatView with all options still under 10 lines")
|
|
@MainActor
|
|
func dataChatViewFullConfig() throws {
|
|
let db = try Self.makeSampleDatabase() // 1
|
|
let model = MockLanguageModel() // 2
|
|
let _ = DataChatView( // 3-8
|
|
database: db,
|
|
model: model,
|
|
allowlist: .readOnly,
|
|
additionalContext: "Product catalog for an e-commerce store",
|
|
maxSummaryRows: 100
|
|
)
|
|
// Even with ALL options specified, it's under 10 lines of setup.
|
|
}
|
|
}
|