Overview of Blackbird
mainasync concurrency and Codable protocols to provide a fast and efficient way to interact with SQLite databases.repository·main·Indexed 21 days ago
https://github.com/marcoarment/blackbirdA SQLite database wrapper and model layer for Swift that utilizes Codable, automatic migrations, and type-safe key-paths. It provides a BlackbirdModel protocol for schema definition, Combine publishers for monitoring database changes, and specialized property wrappers like @BlackbirdLiveModels for SwiftUI integration. The library also includes Blackbird.Database for raw asynchronous SQLite access, parameterized queries, and transactions.
async concurrency and Codable protocols to provide a fast and efficient way to interact with SQLite databases.Blackbird provides specialized property wrappers for SwiftUI to enable async-loading and automatic UI updates when the underlying database changes.
@BlackbirdLiveModels: Loads an array of matching models asynchronously. The view updates automatically when the database changes.@BlackbirdLiveQuery: Executes a custom SQL query asynchronously and provides the results.@BlackbirdLiveModel: Provides an auto-updating instance of a single model.Inject the database into the environment using .environment(\.blackbirdDatabase, database) so child views can access it via the @Environment property wrapper.
struct PostListView: View {
// Async-loading, auto-updating array of matching instances
@BlackbirdLiveModels({ try await Post.read(from: $0, orderBy: .ascending(\.$id)) }) var posts
// Async-loading, auto-updating rows from a custom query
@BlackbirdLiveQuery(tableName: "Post", { try await $0.query("SELECT MAX(id) AS max FROM Post") }) var maxID
var body: some View {
VStack {
if posts.didLoad {
List {
ForEach(posts.results) { post in
NavigationLink(destination: PostView(post: post.liveModel)) {
Text(post.title)
}
}
}
} else {
ProgressView()
}
}
}
}Blackbird provides type-safe, compiler-checked querying using Swift key-paths. This avoids string-based errors and provides compile-time validation.
Post.read(from: db, id: value).Post.read(from: db, matching: \.$keyPath == value).Post.query(in: db, columns: [...], matching: ...) to retrieve a dictionary of specific columns.If needed, you can always execute raw SQL:
Post.query(in: db, "UPDATE $T SET ...", ...)Post.read(from: db, sqlWhere: "...", ...)Post.query(in: db, "SELECT ...")Note: $T can be used in SQL strings as a placeholder for the table name.
// Fetch by primary key
let post = try await Post.read(from: db, id: 2)
// Or with a WHERE condition, using compiler-checked key-paths:
let posts = try await Post.read(from: db, matching: \.$title == "Sports")
// Select custom columns, with row dictionaries typed by key-path:
for row in try await Post.query(in: db, columns: [\.$id, \.$image], matching: \.$url != nil) {
let postID = row[\.$id] // returns Int
let imageData = row[\.$image] // returns Data?
}To store structs in a Blackbird.Database, conform your struct to the BlackbirdModel protocol. Use the @BlackbirdColumn property wrapper for each field you want to persist. Blackbird handles table creation and automatic migrations at runtime based on your struct definition.
You can define custom primary keys, indexes, and unique indexes using static properties:
static var primaryKey: [BlackbirdColumnKeyPath]static var indexes: [[BlackbirdColumnKeyPath]]static var uniqueIndexes: [[BlackbirdColumnKeyPath]]For enum columns, conform your enum to BlackbirdIntegerEnum.
import Blackbird
struct Post: BlackbirdModel {
@BlackbirdColumn var id: Int
@BlackbirdColumn var title: String
@BlackbirdColumn var url: URL?
}Blackbird allows you to observe changes at the row, column, or primary key level using Combine publishers. This is useful for reactive UI updates.
Post.changePublisher(in: db) monitors all changes to the table.Post.changePublisher(in: db, columns: [\.$title]) monitors only changes to the specified key-paths.Post.changePublisher(in: db, primaryKey: 3, columns: [\.$title]) monitors a specific row and specific column.// Monitor all changes in the table
let listener = Post.changePublisher(in: db).sink { change in
if change.hasPrimaryKeyChanged(7) {
print("Post 7 has changed")
}
if change.hasColumnChanged(\.$title) {
print("A title has changed")
}
}
// Or monitor a single column by key-path
let listener = Post.changePublisher(in: db, columns: [\.$title]).sink { _ in
print("A post's title changed")
}Blackbird.Database is a lightweight async wrapper around SQLite that can be used independently of BlackbirdModel.
db.query("SELECT ... WHERE col = ?", value) to prevent SQL injection.db.execute("UPDATE ...") for commands that don't return rows.db.transaction { core in ... } to group multiple operations. Inside the closure, use the provided core object to run queries synchronously.let db = try Blackbird.Database(path: "/tmp/db.sqlite")
// SELECT with parameterized queries
for row in try await db.query("SELECT id FROM posts WHERE state = ?", 1) {
let id = row["id"]?.intValue
}
// Run direct queries
try await db.execute("UPDATE posts SET comments = NULL")
// Transactions with synchronous queries
try await db.transaction { core in
try core.query("INSERT INTO posts VALUES (?, ?)", 16, "Sports!")
try core.query("INSERT INTO posts VALUES (?, ?)", 17, "Dewey Defeats Truman")
}