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>
This commit is contained in:
Krishna Kumar
2026-04-04 09:30:56 -05:00
commit b1724fe7ca
55 changed files with 15506 additions and 0 deletions

View File

@@ -0,0 +1,164 @@
// DatabaseSchema.swift
// SwiftDBAI
//
// Auto-introspected SQLite schema model types.
import Foundation
/// Complete schema representation of an SQLite database.
public struct DatabaseSchema: Sendable, Equatable {
/// All tables in the database, keyed by table name.
public let tables: [String: TableSchema]
/// Ordered table names (preserves discovery order).
public let tableNames: [String]
/// Returns a compact text description suitable for LLM system prompts.
public var schemaDescription: String {
var lines: [String] = []
for name in tableNames {
guard let table = tables[name] else { continue }
lines.append(table.descriptionForLLM)
}
return lines.joined(separator: "\n\n")
}
/// Returns a description suitable for LLM system prompts.
/// Alias for `schemaDescription` for API compatibility.
public func describeForLLM() -> String {
schemaDescription
}
public init(tables: [String: TableSchema], tableNames: [String]) {
self.tables = tables
self.tableNames = tableNames
}
}
/// Schema for a single SQLite table.
public struct TableSchema: Sendable, Equatable {
public let name: String
public let columns: [ColumnSchema]
public let primaryKey: [String]
public let foreignKeys: [ForeignKeySchema]
public let indexes: [IndexSchema]
/// Text description for embedding in LLM prompts.
public var descriptionForLLM: String {
var parts: [String] = []
let colDefs = columns.map { col in
var def = " \(col.name) \(col.type)"
if col.isPrimaryKey { def += " PRIMARY KEY" }
if col.isNotNull { def += " NOT NULL" }
if let defaultValue = col.defaultValue { def += " DEFAULT \(defaultValue)" }
return def
}
parts.append("TABLE \(name) (\n\(colDefs.joined(separator: ",\n"))\n)")
if !foreignKeys.isEmpty {
let fkDescs = foreignKeys.map {
" FOREIGN KEY (\($0.fromColumn)) REFERENCES \($0.toTable)(\($0.toColumn))"
}
parts.append("FOREIGN KEYS:\n\(fkDescs.joined(separator: "\n"))")
}
if !indexes.isEmpty {
let idxDescs = indexes.map {
" INDEX \($0.name) ON (\($0.columns.joined(separator: ", ")))\($0.isUnique ? " UNIQUE" : "")"
}
parts.append("INDEXES:\n\(idxDescs.joined(separator: "\n"))")
}
return parts.joined(separator: "\n")
}
public init(
name: String,
columns: [ColumnSchema],
primaryKey: [String],
foreignKeys: [ForeignKeySchema],
indexes: [IndexSchema]
) {
self.name = name
self.columns = columns
self.primaryKey = primaryKey
self.foreignKeys = foreignKeys
self.indexes = indexes
}
}
/// Schema for a single column.
public struct ColumnSchema: Sendable, Equatable {
/// Column position (0-based).
public let cid: Int
/// Column name.
public let name: String
/// Declared SQLite type (e.g. "TEXT", "INTEGER", "REAL", "BLOB").
public let type: String
/// Whether the column has a NOT NULL constraint.
public let isNotNull: Bool
/// Default value expression, if any.
public let defaultValue: String?
/// Whether this column is part of the primary key.
public let isPrimaryKey: Bool
public init(
cid: Int,
name: String,
type: String,
isNotNull: Bool,
defaultValue: String?,
isPrimaryKey: Bool
) {
self.cid = cid
self.name = name
self.type = type
self.isNotNull = isNotNull
self.defaultValue = defaultValue
self.isPrimaryKey = isPrimaryKey
}
}
/// Schema for a foreign key relationship.
public struct ForeignKeySchema: Sendable, Equatable {
/// Column in the source table.
public let fromColumn: String
/// Referenced table name.
public let toTable: String
/// Referenced column name.
public let toColumn: String
/// ON UPDATE action (e.g. "CASCADE", "NO ACTION").
public let onUpdate: String
/// ON DELETE action.
public let onDelete: String
public init(
fromColumn: String,
toTable: String,
toColumn: String,
onUpdate: String,
onDelete: String
) {
self.fromColumn = fromColumn
self.toTable = toTable
self.toColumn = toColumn
self.onUpdate = onUpdate
self.onDelete = onDelete
}
}
/// Schema for a database index.
public struct IndexSchema: Sendable, Equatable {
/// Index name.
public let name: String
/// Whether the index enforces uniqueness.
public let isUnique: Bool
/// Columns included in the index, in order.
public let columns: [String]
public init(name: String, isUnique: Bool, columns: [String]) {
self.name = name
self.isUnique = isUnique
self.columns = columns
}
}

View File

@@ -0,0 +1,153 @@
// SchemaIntrospector.swift
// SwiftDBAI
//
// Auto-introspects SQLite database schema using GRDB.
import GRDB
/// Introspects an SQLite database schema by querying sqlite_master and PRAGMA statements.
///
/// Usage:
/// ```swift
/// let dbPool = try DatabasePool(path: "path/to/db.sqlite")
/// let schema = try await SchemaIntrospector.introspect(database: dbPool)
/// print(schema.schemaDescription)
/// ```
public struct SchemaIntrospector: Sendable {
// MARK: - Public API
/// Introspects the full schema of the given database.
///
/// Discovers all user tables (excluding sqlite_ internal tables),
/// their columns, primary keys, foreign keys, and indexes.
///
/// - Parameter database: A GRDB `DatabaseReader` (DatabasePool or DatabaseQueue).
/// - Returns: A complete `DatabaseSchema` representation.
public static func introspect(database: any DatabaseReader) async throws -> DatabaseSchema {
try await database.read { db in
try introspect(db: db)
}
}
/// Synchronous introspection within an existing database access context.
///
/// - Parameter db: A GRDB `Database` instance from within a read/write block.
/// - Returns: A complete `DatabaseSchema` representation.
public static func introspect(db: Database) throws -> DatabaseSchema {
let tableNames = try fetchTableNames(db: db)
var tables: [String: TableSchema] = [:]
for tableName in tableNames {
let columns = try fetchColumns(db: db, table: tableName)
let primaryKey = try fetchPrimaryKey(db: db, table: tableName)
let foreignKeys = try fetchForeignKeys(db: db, table: tableName)
let indexes = try fetchIndexes(db: db, table: tableName)
// Mark columns that are part of the primary key
let pkSet = Set(primaryKey)
let annotatedColumns = columns.map { col in
ColumnSchema(
cid: col.cid,
name: col.name,
type: col.type,
isNotNull: col.isNotNull,
defaultValue: col.defaultValue,
isPrimaryKey: pkSet.contains(col.name)
)
}
tables[tableName] = TableSchema(
name: tableName,
columns: annotatedColumns,
primaryKey: primaryKey,
foreignKeys: foreignKeys,
indexes: indexes
)
}
return DatabaseSchema(tables: tables, tableNames: tableNames)
}
// MARK: - Private Helpers
/// Fetches all user table names from sqlite_master.
private static func fetchTableNames(db: Database) throws -> [String] {
let sql = """
SELECT name FROM sqlite_master
WHERE type = 'table'
AND name NOT LIKE 'sqlite_%'
ORDER BY name
"""
return try String.fetchAll(db, sql: sql)
}
/// Fetches column metadata for a table using PRAGMA table_info.
private static func fetchColumns(db: Database, table: String) throws -> [ColumnSchema] {
let sql = "PRAGMA table_info(\(table.quotedDatabaseIdentifier))"
let rows = try Row.fetchAll(db, sql: sql)
return rows.map { row in
ColumnSchema(
cid: row["cid"],
name: row["name"],
type: (row["type"] as String?) ?? "",
isNotNull: row["notnull"] == 1,
defaultValue: row["dflt_value"],
isPrimaryKey: row["pk"] != 0
)
}
}
/// Fetches primary key columns for a table.
private static func fetchPrimaryKey(db: Database, table: String) throws -> [String] {
let sql = "PRAGMA table_info(\(table.quotedDatabaseIdentifier))"
let rows = try Row.fetchAll(db, sql: sql)
return rows
.filter { ($0["pk"] as Int) > 0 }
.sorted { ($0["pk"] as Int) < ($1["pk"] as Int) }
.map { $0["name"] }
}
/// Fetches foreign key relationships for a table.
private static func fetchForeignKeys(db: Database, table: String) throws -> [ForeignKeySchema] {
let sql = "PRAGMA foreign_key_list(\(table.quotedDatabaseIdentifier))"
let rows = try Row.fetchAll(db, sql: sql)
return rows.map { row in
ForeignKeySchema(
fromColumn: row["from"],
toTable: row["table"],
toColumn: row["to"],
onUpdate: row["on_update"] ?? "NO ACTION",
onDelete: row["on_delete"] ?? "NO ACTION"
)
}
}
/// Fetches indexes and their columns for a table.
private static func fetchIndexes(db: Database, table: String) throws -> [IndexSchema] {
let indexListSQL = "PRAGMA index_list(\(table.quotedDatabaseIdentifier))"
let indexRows = try Row.fetchAll(db, sql: indexListSQL)
var indexes: [IndexSchema] = []
for indexRow in indexRows {
let indexName: String = indexRow["name"]
let isUnique: Bool = indexRow["unique"] == 1
// Skip auto-generated indexes for primary keys
if indexName.hasPrefix("sqlite_autoindex_") { continue }
let infoSQL = "PRAGMA index_info(\(indexName.quotedDatabaseIdentifier))"
let infoRows = try Row.fetchAll(db, sql: infoSQL)
let columns: [String] = infoRows
.sorted { ($0["seqno"] as Int) < ($1["seqno"] as Int) }
.map { $0["name"] }
indexes.append(IndexSchema(
name: indexName,
isUnique: isUnique,
columns: columns
))
}
return indexes
}
}