Files
SwiftDBAI/Tests/SwiftDBAITests/SchemaIntrospectorTests.swift
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

235 lines
7.9 KiB
Swift

// SchemaIntrospectorTests.swift
// SwiftDBAI
import Testing
import GRDB
@testable import SwiftDBAI
@Suite("SchemaIntrospector")
struct SchemaIntrospectorTests {
// MARK: - Helper
/// Creates an in-memory database with a sample schema for testing.
private func makeTestDatabase() throws -> DatabaseQueue {
let db = try DatabaseQueue(configuration: {
var config = Configuration()
config.foreignKeysEnabled = true
return config
}())
try db.write { db in
try db.execute(sql: """
CREATE TABLE authors (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE
);
""")
try db.execute(sql: """
CREATE TABLE books (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
author_id INTEGER NOT NULL REFERENCES authors(id) ON DELETE CASCADE,
published_date TEXT,
price REAL DEFAULT 9.99
);
""")
try db.execute(sql: """
CREATE INDEX idx_books_author ON books(author_id);
""")
try db.execute(sql: """
CREATE INDEX idx_books_title ON books(title);
""")
try db.execute(sql: """
CREATE TABLE reviews (
id INTEGER PRIMARY KEY,
book_id INTEGER NOT NULL REFERENCES books(id),
rating INTEGER NOT NULL,
comment TEXT
);
""")
}
return db
}
// MARK: - Tests
@Test("Discovers all user tables")
func discoversAllTables() async throws {
let db = try makeTestDatabase()
let schema = try await SchemaIntrospector.introspect(database: db)
#expect(schema.tableNames.count == 3)
#expect(schema.tableNames.contains("authors"))
#expect(schema.tableNames.contains("books"))
#expect(schema.tableNames.contains("reviews"))
}
@Test("Excludes sqlite_ internal tables")
func excludesInternalTables() async throws {
let db = try makeTestDatabase()
let schema = try await SchemaIntrospector.introspect(database: db)
for name in schema.tableNames {
#expect(!name.hasPrefix("sqlite_"))
}
}
@Test("Introspects column names and types")
func introspectsColumns() async throws {
let db = try makeTestDatabase()
let schema = try await SchemaIntrospector.introspect(database: db)
let books = try #require(schema.tables["books"])
#expect(books.columns.count == 5)
let titleCol = try #require(books.columns.first { $0.name == "title" })
#expect(titleCol.type == "TEXT")
#expect(titleCol.isNotNull == true)
#expect(titleCol.isPrimaryKey == false)
let priceCol = try #require(books.columns.first { $0.name == "price" })
#expect(priceCol.type == "REAL")
#expect(priceCol.defaultValue == "9.99")
}
@Test("Detects primary keys")
func detectsPrimaryKeys() async throws {
let db = try makeTestDatabase()
let schema = try await SchemaIntrospector.introspect(database: db)
let authors = try #require(schema.tables["authors"])
#expect(authors.primaryKey == ["id"])
let idCol = try #require(authors.columns.first { $0.name == "id" })
#expect(idCol.isPrimaryKey == true)
}
@Test("Detects foreign keys")
func detectsForeignKeys() async throws {
let db = try makeTestDatabase()
let schema = try await SchemaIntrospector.introspect(database: db)
let books = try #require(schema.tables["books"])
#expect(books.foreignKeys.count == 1)
let fk = books.foreignKeys[0]
#expect(fk.fromColumn == "author_id")
#expect(fk.toTable == "authors")
#expect(fk.toColumn == "id")
#expect(fk.onDelete == "CASCADE")
}
@Test("Detects indexes")
func detectsIndexes() async throws {
let db = try makeTestDatabase()
let schema = try await SchemaIntrospector.introspect(database: db)
let books = try #require(schema.tables["books"])
let indexNames = books.indexes.map(\.name)
#expect(indexNames.contains("idx_books_author"))
#expect(indexNames.contains("idx_books_title"))
}
@Test("Detects NOT NULL constraints")
func detectsNotNull() async throws {
let db = try makeTestDatabase()
let schema = try await SchemaIntrospector.introspect(database: db)
let reviews = try #require(schema.tables["reviews"])
let ratingCol = try #require(reviews.columns.first { $0.name == "rating" })
#expect(ratingCol.isNotNull == true)
let commentCol = try #require(reviews.columns.first { $0.name == "comment" })
#expect(commentCol.isNotNull == false)
}
@Test("Generates LLM-friendly schema description")
func generatesSchemaDescription() async throws {
let db = try makeTestDatabase()
let schema = try await SchemaIntrospector.introspect(database: db)
let description = schema.schemaDescription
#expect(description.contains("TABLE authors"))
#expect(description.contains("TABLE books"))
#expect(description.contains("FOREIGN KEY"))
#expect(description.contains("REFERENCES authors(id)"))
#expect(description.contains("INDEX idx_books_author"))
}
@Test("Handles empty database")
func handlesEmptyDatabase() async throws {
let db = try DatabaseQueue()
let schema = try await SchemaIntrospector.introspect(database: db)
#expect(schema.tables.isEmpty)
#expect(schema.tableNames.isEmpty)
#expect(schema.schemaDescription.isEmpty)
}
@Test("Handles composite primary keys")
func handlesCompositePrimaryKey() async throws {
let db = try DatabaseQueue()
try await db.write { db in
try db.execute(sql: """
CREATE TABLE book_tags (
book_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
PRIMARY KEY (book_id, tag_id)
);
""")
}
let schema = try await SchemaIntrospector.introspect(database: db)
let bookTags = try #require(schema.tables["book_tags"])
#expect(bookTags.primaryKey.count == 2)
#expect(bookTags.primaryKey.contains("book_id"))
#expect(bookTags.primaryKey.contains("tag_id"))
}
@Test("Handles tables with no explicit types (SQLite dynamic typing)")
func handlesDynamicTyping() async throws {
let db = try DatabaseQueue()
try await db.write { db in
try db.execute(sql: """
CREATE TABLE flexible (
id INTEGER PRIMARY KEY,
data,
info BLOB
);
""")
}
let schema = try await SchemaIntrospector.introspect(database: db)
let flexible = try #require(schema.tables["flexible"])
let dataCol = try #require(flexible.columns.first { $0.name == "data" })
#expect(dataCol.type == "") // No declared type
let infoCol = try #require(flexible.columns.first { $0.name == "info" })
#expect(infoCol.type == "BLOB")
}
@Test("Synchronous introspection works within database access")
func synchronousIntrospection() async throws {
let db = try DatabaseQueue()
try await db.write { db in
try db.execute(sql: "CREATE TABLE test (id INTEGER PRIMARY KEY, val TEXT);")
}
let schema = try await db.read { db in
try SchemaIntrospector.introspect(db: db)
}
#expect(schema.tableNames == ["test"])
let table = try #require(schema.tables["test"])
#expect(table.columns.count == 2)
}
}