mcpField
A
axiom-sqlitedata-ref

axiom-sqlitedata-ref

github.com/charleswiltgenデータ・データベース1.2k184 回インストール今日更新
リポジトリ

axiom-sqlitedata-ref について

Swift 開発者向けの SQLiteData 上級リファレンスです。GRDB と StructuredQueries に基づく型安全なクエリ構成、スキーマ設計、集計などを扱います。

主な機能

  1. 01再利用可能なカラムグループと列挙型スキーマ
  2. 02合成可能なクエリスコープとフィルター
  3. 03原子的な読み取りを行うカスタムフェッチ
  4. 04結合、CTE、再帰クエリ
  5. 05FTS5 検索と集計
  6. 06RETURNING と SQL スキーママクロ

活用シーン

  1. 01列挙型ケースで多相レコードをモデル化
  2. 02再利用可能なデータベースフィルターの合成
  3. 031回の読み取りでダッシュボード関連データを取得
  4. 04SQLite レコードの検索、ページング、集計

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-sqlitedata-ref

インストール後、Claude Code で/axiom-sqlitedata-refを入力して読み込まれたか確認します。

詳細

SQLiteData 高度なリファレンス

概要

Point-Free の SQLiteData の高度なクエリパターンとスキーマ構成のテクニックです。GRDB と StructuredQueries を基盤としています。

基本的なパターン(CRUD、CloudKit のセットアップ、@Table の基本)については、axiom-sqlitedata ディシプリンスキルを参照してください。

このリファレンスでは、高度なクエリ、スキーマ構成、ビュー、カスタム集計関数を扱います。

要件 iOS 17 以降、Swift 6 の厳密な並行性 フレームワーク SQLiteData 1.4 以降


カラムグループとスキーマ構成

SQLiteData にはスキーマ型を構成する強力なツールが用意されており、再利用や整理の改善、単一テーブル継承パターンを実現できます。

カラムグループ

関連するカラムを @Selection で再利用可能な型にまとめます。

// Define a reusable column group
@Selection
struct Timestamps {
    let createdAt: Date
    let updatedAt: Date?
}

// Use in multiple tables
@Table
nonisolated struct RemindersList: Identifiable {
    let id: UUID
    var title = ""
    let timestamps: Timestamps  // Embedded column group
}

@Table
nonisolated struct Reminder: Identifiable {
    let id: UUID
    var title = ""
    var isCompleted = false
    let timestamps: Timestamps  // Same group, reused
}

重要: SQLite にはカラムをグループ化する概念がありません。CREATE TABLE では、すべてのグループをフラット化してください。

CREATE TABLE "remindersLists" (
    "id" TEXT PRIMARY KEY NOT NULL DEFAULT (uuid()),
    "title" TEXT NOT NULL DEFAULT '',
    "createdAt" TEXT NOT NULL,
    "updatedAt" TEXT
) STRICT
カラムグループへのクエリ

ドット構文でグループ内のフィールドにアクセスします。

// Query a field inside the group
RemindersList
    .where { $0.timestamps.createdAt <= cutoffDate }
    .fetchAll(db)

// Compare entire group (flattens to tuple in SQL)
RemindersList
    .where {
        $0.timestamps <= Timestamps(createdAt: date1, updatedAt: date2)
    }
@Selection 内でのグループのネスト

カスタムクエリ結果でカラムグループを使用します。

@Selection
struct Row {
    let reminderTitle: String
    let listTitle: String
    let timestamps: Timestamps  // Nested group
}

let results = try Reminder
    .join(RemindersList.all) { $0.remindersListID.eq($1.id) }
    .select {
        Row.Columns(
            reminderTitle: $0.title,
            listTitle: $1.title,
            timestamps: $0.timestamps  // Pass entire group
        )
    }
    .fetchAll(db)
列挙型による単一テーブル継承

@CasePathable @Selection 列挙型を使って多相データをモデル化します。これはクラス継承に代わる値型のアプローチです。

import CasePaths

@Table
nonisolated struct Attachment: Identifiable {
    let id: UUID
    let kind: Kind

    @CasePathable @Selection
    enum Kind {
        case link(URL)
        case note(String)
        case image(URL)
    }
}

注: @CasePathable は必須であり、Point-Free の CasePaths ライブラリに含まれています。

列挙型テーブルの SQL スキーマ

すべてのケースを NULL 許容カラムにフラット化します。

CREATE TABLE "attachments" (
    "id" TEXT PRIMARY KEY NOT NULL DEFAULT (uuid()),
    "link" TEXT,
    "note" TEXT,
    "image" TEXT
) STRICT
列挙型テーブルへのクエリ
// Fetch all — decoding determines which case
let attachments = try Attachment.all.fetchAll(db)

// Filter by case
let images = try Attachment
    .where { $0.kind.image.isNot(nil) }
    .fetchAll(db)
列挙型の値の挿入
try Attachment.insert {
    Attachment.Draft(kind: .note("Hello world!"))
}
.execute(db)
// Inserts: (id, NULL, 'Hello world!', NULL)
列挙型の値の更新
try Attachment.find(id).update {
    $0.kind = #bind(.link(URL(string: "https://example.com")!))
}
.execute(db)
// Sets link column, NULLs note and image columns
グループ化されたカラムを持つ複雑な列挙型ケース

ネストした @Selection 型を使うことで、列挙型のケースに構造化データを格納できます。

@Table
nonisolated struct Attachment: Identifiable {
    let id: UUID
    let kind: Kind

    @CasePathable @Selection
    enum Kind {
        case link(URL)
        case note(String)
        case image(Attachment.Image)  // Fully qualify nested types
    }

    @Selection
    struct Image {
        var caption = ""
        var url: URL
    }
}

SQL スキーマでは、ネストしたフィールドをすべてフラット化します。

CREATE TABLE "attachments" (
    "id" TEXT PRIMARY KEY NOT NULL DEFAULT (uuid()),
    "link" TEXT,
    "note" TEXT,
    "caption" TEXT,
    "url" TEXT
) STRICT
データベース関数への行の受け渡し

カラムグループを使うと、@DatabaseFunction にテーブルの行全体を渡すことができます。

@DatabaseFunction
func isPastDue(reminder: Reminder) -> Bool {
    !reminder.isCompleted && reminder.dueDate < Date()
}

// Use in queries — columns are flattened/reconstituted automatically
let pastDue = try Reminder
    .where { $isPastDue(reminder: $0) }
    .fetchAll(db)
カラムグループと SwiftData の継承の比較
アプローチSQLiteDataSwiftData
型値型(列挙型/構造体)参照型(クラス)
網羅性コンパイラによる switch の検証実行時の型チェック
冗長さ簡潔な列挙型ケース冗長なクラス階層
継承列挙型による単一テーブル@Model クラスの継承
再利用可能なカラム@Selection グループ手動での繰り返し

SwiftData での同等の実装(より冗長):

@Model class Attachment { var isActive: Bool }
@Model class Link: Attachment { var url: URL }
@Model class Note: Attachment { var note: String }
@Model class Image: Attachment { var url: URL }
// Each needs explicit init calling super.init

クエリの構成

静的プロパティ/メソッドとして再利用可能なスコープを作成します。

extension Item {
    static let active = Item.where { !$0.isArchived && !$0.isDeleted }
    static let inStock = Item.where(\.isInStock)

    static func createdAfter(_ date: Date) -> Where<Item> {
        Item.where { $0.createdAt > date }
    }
}

// Chain scopes
let results = try Item.active.inStock.order(by: \.title).fetchAll(db)

// Use as base for @FetchAll
@FetchAll(Item.active) var items

Where<Item> を拡張して、組み合わせ可能なフィルターを追加します。

extension Where<Item> {
    func matching(_ search: String) -> Where<Item> {
        self.where { $0.title.contains(search) || $0.notes.contains(search) }
    }
}
let results = try Item.inStock.matching(searchText).fetchAll(db)

@Fetch によるカスタムフェッチリクエスト

単一の読み取りトランザクションで複数のデータを取得する必要がある場合は @Fetch を使用します(単一テーブルのクエリには @FetchAll/@FetchOne を使用してください)。

struct DashboardRequest: FetchKeyRequest {
    struct Value: Sendable {
        let totalItems: Int
        let activeItems: [Item]
        let categories: [Category]
    }

    func fetch(_ db: Database) throws -> Value {
        try Value(
            totalItems: Item.count().fetchOne(db) ?? 0,
            activeItems: Item.where { !$0.isArchived }.order(by: \.updatedAt.desc()).limit(10).fetchAll(db),
            categories: Category.order(by: \.name).fetchAll(db)
        )
    }
}

@Fetch(DashboardRequest()) var dashboard

.load() による動的な読み込み:

@Fetch var results = SearchRequest.Value()

.task(id: query) {
    try? await $results.load(SearchRequest(query: query), animation: .default)
}

主な利点:アトミックな読み取り、自動的な監視、型安全な結果。


高度なクエリパターン

文字列関数
関数使用例SQL
upper() / lower()$0.title.upper()UPPER/LOWER
trim() / ltrim() / rtrim()$0.title.trim()TRIM
substr(start, len)$0.title.substr(0, 3)SUBSTR
replace(old, new)$0.title.replace("old", "new")REPLACE
length()$0.title.length()LENGTH
instr(search)$0.title.instr("search") > 0INSTR
like(pattern)$0.title.like("%phone%")LIKE
hasPrefix / hasSuffix / contains$0.title.contains("Max")Swift 形式
collate(.nocase)$0.title.collate(.nocase).eq(#bind("X"))COLLATE
NULL の処理
// Coalesce — first non-null value
let name = try User.select { $0.nickname ?? $0.firstName ?? "Anonymous" }.fetchAll(db)

// Null checks
let withDue = try Reminder.where { $0.dueDate.isNot(nil) }.fetchAll(db)
let noDue = try Reminder.where { $0.dueDate.is(nil) }.fetchAll(db)

// Null-safe ordering
let sorted = try Item.order { $0.priority.desc(nulls: .last) }.fetchAll(db)
範囲と集合のメンバーシップ
// IN (set or subquery)
let selected = try Item.where { $0.id.in(selectedIds) }.fetchAll(db)
let inActive = try Item.where { $0.categoryID.in(
    Category.where(\.isActive).select(\.id)
)}.fetchAll(db)

// NOT IN
let excluded = try Item.where { !$0.id.in(excludedIds) }.fetchAll(db)

// BETWEEN (or Swift range syntax)
let midRange = try Item.where { $0.price.between(10, and: 100) }.fetchAll(db)
ページネーション
// Offset-based
let items = try Item.order(by: \.createdAt).limit(20, offset: page * 20).fetchAll(db)

// Cursor-based (more efficient for deep pages)
let items = try Item.where { $0.id > lastSeenId }.order(by: \.id).limit(20).fetchAll(db)
重複のない結果
let categories = try Item.select(\.category).distinct().fetchAll(db)

RETURNING 句

INSERT、UPDATE、DELETE 操作で生成された値を取得します。

// Insert and get auto-generated ID
let newId = try Item.insert { Item.Draft(title: "New Item") }
    .returning(\.id).fetchOne(db)

// Update and return new values
let updates = try Item.find(id).update { $0.count += 1 }
    .returning { ($0.id, $0.count) }.fetchOne(db)

// Capture deleted records before removal
let deleted = try Item.where { $0.isArchived }.delete()
    .returning(Item.self).fetchAll(db)

RETURNING を使うと、自動生成 ID の取得、削除の監査、更新の確認のために別のクエリを実行する必要がなくなります。


結合

結合の種類
// INNER JOIN — only matching rows
let items = try Item.join(Category.all) { $0.categoryID.eq($1.id) }.fetchAll(db)

// LEFT JOIN — all from left, matching from right (nullable)
let items = try Item.leftJoin(Category.all) { $0.categoryID.eq($1.id) }
    .select { ($0, $1) }  // (Item, Category?)
    .fetchAll(db)

ほかに、.rightJoin()(右側のすべての行)と .fullJoin()(両側のすべての行)も使用できます。

複数テーブルの結合は自然に連結できます。

extension Reminder {
    static let withTags = group(by: \.id)
        .leftJoin(ReminderTag.all) { $0.id.eq($1.reminderID) }
        .leftJoin(Tag.all) { $1.tagID.eq($2.primaryKey) }
}
TableAlias を使った自己結合
struct ManagerAlias: TableAlias { typealias Table = Employee }

let employeesWithManagers = try Employee
    .leftJoin(Employee.all.as(ManagerAlias.self)) { $0.managerID.eq($1.id) }
    .select { (employeeName: $0.name, managerName: $1.name) }
    .fetchAll(db)

CASE 式

// Simple case — map values
let labels = try Item.select {
    Case($0.priority).when(1, then: "Low").when(2, then: "Medium")
        .when(3, then: "High").else("Unknown")
}.fetchAll(db)

// Searched case — boolean conditions
let status = try Order.select {
    Case().when($0.shippedAt.isNot(nil), then: "Shipped")
        .when($0.paidAt.isNot(nil), then: "Paid").else("Unknown")
}.fetchAll(db)

// Case in updates (toggle pattern)
try Reminder.find(id).update {
    $0.status = Case($0.status)
        .when(#bind(.incomplete), then: #bind(.completing))
        .when(#bind(.completing), then: #bind(.completed))
        .else(#bind(.incomplete))
}.execute(db)

共通テーブル式(CTE)

非再帰 CTE
// Single CTE
let expensiveItems = try With {
    Item.where { $0.price > 1000 }
} query: { expensive in
    expensive.order(by: \.price).limit(10)
}.fetchAll(db)

// Multiple CTEs
let report = try With {
    Customer.where { $0.totalSpent > 10000 }
} with: {
    Order.where { $0.createdAt > lastMonth }
} query: { highValue, recentOrders in
    highValue.join(recentOrders) { $0.id.eq($1.customerID) }
        .select { ($0.name, $1.total) }
}.fetchAll(db)

CTE を使うと、複雑なクエリを読みやすい部分に分割したり、サブクエリを再利用したり、クエリプランを改善したりできます。

再帰 CTE

階層データ(ツリー、組織図、スレッド形式のコメント)をクエリします。

@Table
nonisolated struct Category: Identifiable {
    let id: UUID
    var name = ""
    var parentID: UUID?  // Self-referential
}

// Get all descendants of a root category
let allDescendants = try With {
    Category.where { $0.id.eq(#bind(rootCategoryId)) }  // Base case
} recursiveUnion: { cte in
    Category.all.join(cte) { $0.parentID.eq($1.id) }.select { $0 }  // Recursive case
} query: { cte in
    cte.order(by: \.name)
}.fetchAll(db)

結合条件($0.id.eq($1.parentID))を逆にすると、ツリーを下方向ではなく上方向にたどれます。


全文検索(FTS5)

FTS5 の基本
@Table
struct ReminderText: FTS5 {
    let rowid: Int
    let title: String
    let notes: String
    let tags: String
}

// Create FTS table in migration
try #sql(
    """
    CREATE VIRTUAL TABLE "reminderTexts" USING fts5(
        "title", "notes", "tags",
        tokenize = 'trigram'
    )
    """
)
.execute(db)
FTS5 の高度な機能
// Highlight search terms
let results = try ItemText.where { $0.match(query) }
    .select { ($0.rowid, $0.title.highlight("<b>", "</b>")) }.fetchAll(db)

// Snippets with context
let snippets = try ItemText.where { $0.match(query) }
    .select { $0.description.snippet("<b>", "</b>", "...", 64) }.fetchAll(db)

// BM25 relevance ranking
let ranked = try ItemText.where { $0.match(query) }
    .order { $0.bm25().desc() }.fetchAll(db)

集計

文字列と JSON の集計
// groupConcat — comma-separated tags per item
let itemsWithTags = try Item.group(by: \.id)
    .leftJoin(ItemTag.all) { $0.id.eq($1.itemID) }
    .leftJoin(Tag.all) { $1.tagID.eq($2.id) }
    .select { ($0.title, $2.name.groupConcat(separator: ", ")) }
    .fetchAll(db)
// ("iPhone", "electronics, mobile, apple")

// jsonGroupArray — aggregate into JSON array
let itemsJson = try Store.group(by: \.id)
    .leftJoin(Item.all) { $0.id.eq($1.storeID) }
    .select { ($0.name, $1.title.jsonGroupArray()) }
    .fetchAll(db)

オプション:.groupConcat(distinct: true)、.groupConcat(order: { $0.asc() })、.jsonGroupArray(filter: $1.isActive)、jsonObject("key", $0.value)。

条件付き集計

すべての集計関数は filter: パラメーターを受け取ります。

let stats = try Item.select {
    Stats.Columns(
        total: $0.count(),
        activeCount: $0.count(filter: $0.isActive),
        avgActivePrice: $0.price.avg(filter: $0.isActive),
        totalRevenue: $0.revenue.sum(filter: $0.status.eq(#bind(.completed)))
    )
}.fetchOne(db)
HAVING 句

.where() はグループ化の前に行をフィルターし、.having() は集計後にグループをフィルターします。

let frequentCustomers = try Customer.group(by: \.id)
    .leftJoin(Order.all) { $0.id.eq($1.customerID) }
    .having { $1.count() > 5 }
    .select { ($0.name, $1.count()) }
    .fetchAll(db)

#sql マクロによるスキーマ作成

#sql マクロを使用すると、スキーマの作成やマイグレーションで型安全な生 SQL を使用できます。

CREATE TABLE
migrator.registerMigration("Create initial tables") { db in
    try #sql("""
        CREATE TABLE "items" (
            "id" TEXT PRIMARY KEY NOT NULL DEFAULT (uuid()),
            "title" TEXT NOT NULL DEFAULT '',
            "isInStock" INTEGER NOT NULL DEFAULT 1,
            "price" REAL NOT NULL DEFAULT 0.0,
            "createdAt" TEXT NOT NULL DEFAULT (datetime('now'))
        ) STRICT
        """).execute(db)
}
パラメーターの補間
  • \(value) → 自動的にエスケープされます(ユーザー入力に対して安全です)
  • \(raw: value) → リテラルとして挿入されます(自身で管理する識別子に限って使用してください)
  • 絶対に \(raw: userInput) を使用しないでください — SQL インジェクションの脆弱性につながります
その他の DDL
// CREATE INDEX (with optional WHERE for partial indexes)
try #sql("""CREATE INDEX "idx_items_search" ON "items" ("title") WHERE "isArchived" = 0""").execute(db)

// CREATE TRIGGER
try #sql("""
    CREATE TRIGGER "update_timestamp" AFTER UPDATE ON "items"
    BEGIN UPDATE "items" SET "updatedAt" = datetime('now') WHERE "id" = NEW."id"; END
    """).execute(db)

// ALTER TABLE
try #sql("""ALTER TABLE "items" ADD COLUMN "notes" TEXT NOT NULL DEFAULT ''""").execute(db)

DDL(CREATE、ALTER、インデックス、トリガー)には #sql を使用してください。通常の CRUD にはクエリビルダーを使用してください。

外部キーの関係
migrator.registerMigration("Create tables with foreign keys") { db in
    try #sql("""
        CREATE TABLE "itemCategories" (
            "itemID" TEXT NOT NULL REFERENCES "items"("id") ON DELETE CASCADE,
            "categoryID" TEXT NOT NULL REFERENCES "categories"("id") ON DELETE CASCADE,
            PRIMARY KEY ("itemID", "categoryID")
        ) STRICT
        """).execute(db)
}

重要:外部キー制約を有効にしてください。SQLite ではデフォルトで無効になっています。

var configuration = Configuration()
configuration.prepareDatabase { db in
    try db.execute(sql: "PRAGMA foreign_keys = ON")
}

PRAGMA foreign_keys = ON がないと、REFERENCES と ON DELETE CASCADE は警告なしに無視されます。

バッチ操作のトランザクションコンテキスト

原子性とパフォーマンスを確保するため、バッチ操作は明示的なトランザクションで囲んでください。

try database.write { db in
    // All operations share one transaction
    for item in items {
        try Item.insert { Item.Draft(title: item.title) }.execute(db)
    }
}
// Commits once on success, rolls back entirely on failure

database.write { } ブロックはすでにトランザクションです。読み取り中心のバッチ分析には、一貫性のあるスナップショットを提供する database.read { } を使用してください。


データベースビュー

カスタムクエリ結果のための @Selection

@Selection は、コンパイル時に検証されるクエリ結果用の .Columns 型を生成します。

@Selection
struct ReminderWithList: Identifiable {
    var id: Reminder.ID { reminder.id }
    let reminder: Reminder
    let remindersList: RemindersList
}

@FetchAll(
    Reminder.join(RemindersList.all) { $0.remindersListID.eq($1.id) }
        .select { ReminderWithList.Columns(reminder: $0, remindersList: $1) }
)
var reminders: [ReminderWithList]

集計クエリでも使用できます。上記の「条件付き集計」セクションを参照してください。

一時ビュー

再利用可能な複雑なクエリには、@Table @Selection と createTemporaryView を組み合わせます。

@Table @Selection
private struct ReminderWithList {
    let reminderTitle: String
    let remindersListTitle: String
}

try database.write { db in
    try ReminderWithList.createTemporaryView(
        as: Reminder.join(RemindersList.all) { $0.remindersListID.eq($1.id) }
            .select { ReminderWithList.Columns(reminderTitle: $0.title, remindersListTitle: $1.title) }
    ).execute(db)
}

// Query like a table — join complexity hidden
let results = try ReminderWithList.order { ($0.remindersListTitle, $0.reminderTitle) }.fetchAll(db)

一時ビューは接続が存続する間だけ存在します。永続ビューには、マイグレーションで #sql("CREATE VIEW ...") を使用してください。

ビューを書き込み可能にするには、createTemporaryTrigger(insteadOf: .insert { ... }) を追加し、操作を基になるテーブルに振り分けます。


カスタム集計関数

複雑な集計を Swift の @DatabaseFunction で記述すると、無理のある SQL サブクエリを避けられます。

// 1. Define — takes Sequence<T?>, returns aggregate result
@DatabaseFunction
func mode(priority priorities: some Sequence<Reminder.Priority?>) -> Reminder.Priority? {
    var occurrences: [Reminder.Priority: Int] = [:]
    for priority in priorities {
        guard let priority else { continue }
        occurrences[priority, default: 0] += 1
    }
    return occurrences.max { $0.value < $1.value }?.key
}

// 2. Register
configuration.prepareDatabase { db in db.add(function: $mode) }

// 3. Use in queries
let results = try RemindersList.group(by: \.id)
    .leftJoin(Reminder.all) { $0.id.eq($1.remindersListID) }
    .select { ($0.title, $mode(priority: $1.priority)) }
    .fetchAll(db)

よくある用途:最頻値、中央値、加重平均、カスタムフィルタリング。関数は SQLite の C エンジンではなく Swift で実行されるため、可能な場合は組み込み集計関数(count、sum、avg、min、max)を使用してください。


バッチ Upsert のパフォーマンス

大量の同期(50K 以上のレコード)には、型安全 API の代わりにキャッシュ済みステートメントを使用してください。

func batchUpsert(_ items: [Item], in db: Database) throws {
    let statement = try db.cachedStatement(sql: """
        INSERT INTO items (id, name, libraryID, remoteID, updatedAt)
        VALUES (?, ?, ?, ?, ?)
        ON CONFLICT(libraryID, remoteID) DO UPDATE SET
            name = excluded.name, updatedAt = excluded.updatedAt
        WHERE excluded.updatedAt >= items.updatedAt
        """)
    for item in items {
        try statement.execute(arguments: [item.id, item.name, item.libraryID, item.remoteID, item.updatedAt])
    }
}

さらに高いスループットが必要な場合は、複数行の VALUES 句を構築してください。実行時に変数の上限を確認します:sqlite3_limit(db.sqliteConnection, SQLITE_LIMIT_VARIABLE_NUMBER, -1)(iOS 14 以降では 32,766、iOS 13 では 999)。

パターンスループットトレードオフ
型安全な upsert約 1K 行/秒優れた開発者体験、コンパイル時チェック
キャッシュ済みステートメント約 10K 行/秒バランスが良い
複数行 VALUES約 50K 行/秒最も複雑

その他の高度なパターン

データベーストリガー
try database.write { db in
    try Reminder.createTemporaryTrigger(
        after: .insert { new in
            Reminder
                .find(new.id)
                .update {
                    $0.position = Reminder.select { ($0.position.max() ?? -1) + 1 }
                }
        }
    )
    .execute(db)
}
カスタム更新ロジック
extension Updates<Reminder> {
    mutating func toggleStatus() {
        self.status = Case(self.status)
            .when(#bind(.incomplete), then: #bind(.completing))
            .else(#bind(.incomplete))
    }
}

// Usage
try Reminder.find(reminder.id).update { $0.toggleStatus() }.execute(db)
Enum のサポート
enum Priority: Int, QueryBindable {
    case low = 1
    case medium = 2
    case high = 3
}

enum Status: Int, QueryBindable {
    case incomplete = 0
    case completing = 1
    case completed = 2
}

@Table
nonisolated struct Reminder: Identifiable {
    let id: UUID
    var priority: Priority?
    var status: Status = .incomplete
}
複合 SELECT
// UNION (deduplicated), UNION ALL (keep duplicates)
let all = try Customer.select(\.email).union(Supplier.select(\.email)).fetchAll(db)

// INTERSECT (in both), EXCEPT (in first but not second)
let shared = try Customer.select(\.email).intersect(Supplier.select(\.email)).fetchAll(db)

リソース

GitHub: pointfreeco/sqlite-data、pointfreeco/swift-structured-queries、groue/GRDB.swift

スキル: axiom-sqlitedata、axiom-sqlitedata-migration、axiom-database-migration、axiom-grdb


対象: iOS 17 以降、Swift 6
フレームワーク: SQLiteData 1.4 以降
履歴: 変更内容については git log を参照してください

コメント