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:
Krishna Kumar
2026-04-04 09:30:56 -05:00
commit fcd752466a
80 changed files with 18265 additions and 0 deletions

View File

@@ -0,0 +1,164 @@
// ChatViewTests.swift
// SwiftDBAITests
//
// Tests for ChatView, ChatViewModel, and MessageBubbleView integration
// with ScrollableDataTableView.
import Testing
import Foundation
@testable import SwiftDBAI
@Suite("SchemaReadiness Tests")
struct SchemaReadinessTests {
@Test("SchemaReadiness isReady returns true only for ready state")
func isReadyProperty() {
#expect(SchemaReadiness.idle.isReady == false)
#expect(SchemaReadiness.loading.isReady == false)
#expect(SchemaReadiness.ready(tableCount: 3).isReady == true)
#expect(SchemaReadiness.failed("error").isReady == false)
}
}
@Suite("ChatViewModel Tests")
struct ChatViewModelTests {
@Test("Messages with query results produce DataTable-compatible data")
func messageWithQueryResultHasTableData() {
// A ChatMessage with a queryResult should have the data needed
// for ScrollableDataTableView rendering
let result = QueryResult(
columns: ["id", "name", "score"],
rows: [
["id": .integer(1), "name": .text("Alice"), "score": .real(95.5)],
["id": .integer(2), "name": .text("Bob"), "score": .real(87.3)],
],
sql: "SELECT id, name, score FROM users",
executionTime: 0.01
)
let message = ChatMessage(
role: .assistant,
content: "Found 2 users.",
queryResult: result,
sql: "SELECT id, name, score FROM users"
)
// Verify queryResult is present and can be converted to DataTable
#expect(message.queryResult != nil)
#expect(message.queryResult!.columns.count == 3)
#expect(message.queryResult!.rows.count == 2)
// Verify DataTable conversion works (this is what MessageBubbleView does)
let dataTable = DataTable(message.queryResult!)
#expect(dataTable.columnCount == 3)
#expect(dataTable.rowCount == 2)
#expect(dataTable.columns[0].name == "id")
#expect(dataTable.columns[1].name == "name")
#expect(dataTable.columns[2].name == "score")
}
@Test("Messages without query results do not trigger table rendering")
func messageWithoutQueryResult() {
let message = ChatMessage(
role: .assistant,
content: "Hello! How can I help?",
queryResult: nil,
sql: nil
)
#expect(message.queryResult == nil)
}
@Test("Empty query results do not trigger table rendering")
func emptyQueryResult() {
let result = QueryResult(
columns: [],
rows: [],
sql: "SELECT * FROM empty_table",
executionTime: 0.001
)
let message = ChatMessage(
role: .assistant,
content: "No results found.",
queryResult: result,
sql: "SELECT * FROM empty_table"
)
// Even though queryResult exists, it has no columns/rows
// MessageBubbleView checks both conditions before showing the table
#expect(message.queryResult != nil)
#expect(message.queryResult!.columns.isEmpty)
#expect(message.queryResult!.rows.isEmpty)
}
@Test("Mutation results do not trigger table rendering")
func mutationQueryResult() {
let result = QueryResult(
columns: [],
rows: [],
sql: "INSERT INTO users (name) VALUES ('Charlie')",
executionTime: 0.005,
rowsAffected: 1
)
let message = ChatMessage(
role: .assistant,
content: "Successfully inserted 1 row.",
queryResult: result,
sql: "INSERT INTO users (name) VALUES ('Charlie')"
)
// Mutation results have empty columns no table shown
#expect(message.queryResult!.columns.isEmpty)
}
@Test("Error messages never have query results")
func errorMessageHasNoQueryResult() {
let message = ChatMessage(
role: .error,
content: "SELECT operations are not allowed."
)
#expect(message.queryResult == nil)
#expect(message.role == .error)
}
@Test("DataTable preserves column order from QueryResult")
func dataTableColumnOrder() {
let result = QueryResult(
columns: ["date", "revenue", "category"],
rows: [
["date": .text("2024-01-01"), "revenue": .real(1500.0), "category": .text("Electronics")],
],
sql: "SELECT date, revenue, category FROM sales",
executionTime: 0.02
)
let dataTable = DataTable(result)
#expect(dataTable.columnNames == ["date", "revenue", "category"])
}
@Test("Large result sets are renderable as DataTable")
func largeResultSet() {
var rows: [[String: QueryResult.Value]] = []
for i in 0..<500 {
rows.append([
"id": .integer(Int64(i)),
"value": .real(Double(i) * 1.5),
])
}
let result = QueryResult(
columns: ["id", "value"],
rows: rows,
sql: "SELECT id, value FROM big_table",
executionTime: 0.15
)
let dataTable = DataTable(result)
#expect(dataTable.rowCount == 500)
#expect(dataTable.columnCount == 2)
}
}