Blackbird Documentation

repository·main·Indexed 21 days ago

https://github.com/marcoarment/blackbird

A 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.

Tokens
2.1K
Snippets
5
Records
6
Agent score
26%

What's inside Blackbird

  1. Use Blackbird with SwiftUI

    main

    Blackbird provides specialized property wrappers for SwiftUI to enable async-loading and automatic UI updates when the underlying database changes.

    Key SwiftUI Wrappers

    • @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.

    Integration Pattern

    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()
                }
            }
        }
    }
  2. Perform queries using BlackbirdModel

    main

    Blackbird provides type-safe, compiler-checked querying using Swift key-paths. This avoids string-based errors and provides compile-time validation.

    Common Query Patterns

    • Fetch by Primary Key: Use Post.read(from: db, id: value).
    • Filter with WHERE: Use Post.read(from: db, matching: \.$keyPath == value).
    • Select Specific Columns: Use Post.query(in: db, columns: [...], matching: ...) to retrieve a dictionary of specific columns.

    Raw SQL Support

    If needed, you can always execute raw SQL:

    • Update: Post.query(in: db, "UPDATE $T SET ...", ...)
    • Read with WHERE: Post.read(from: db, sqlWhere: "...", ...)
    • Custom SELECT: 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?
    }
  3. Define a database model with BlackbirdModel

    main

    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.

    Customizing Schema

    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?
    }
  4. Monitor database changes with Combine

    main

    Blackbird allows you to observe changes at the row, column, or primary key level using Combine publishers. This is useful for reactive UI updates.

    Observation Scopes

    • Full Table: Post.changePublisher(in: db) monitors all changes to the table.
    • Specific Columns: Post.changePublisher(in: db, columns: [\.$title]) monitors only changes to the specified key-paths.
    • Specific Primary Key: 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")
    }
  5. Use Blackbird.Database for raw SQLite access

    main

    Blackbird.Database is a lightweight async wrapper around SQLite that can be used independently of BlackbirdModel.

    Core Capabilities

    • Parameterized Queries: Use db.query("SELECT ... WHERE col = ?", value) to prevent SQL injection.
    • Direct Execution: Use db.execute("UPDATE ...") for commands that don't return rows.
    • Transactions: Use 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")
    }