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.
This commit is contained in:
136
Tests/SwiftDBAITests/DataChatViewUsageTests.swift
Normal file
136
Tests/SwiftDBAITests/DataChatViewUsageTests.swift
Normal file
@@ -0,0 +1,136 @@
|
||||
// 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.
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user