axiom-sqlitedata-ref

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

SQLiteData Advanced Reference

SQLiteData 高级参考

Overview

概述

Advanced query patterns and schema composition techniques for SQLiteData by Point-Free. Built on GRDB and StructuredQueries.
For core patterns (CRUD, CloudKit setup, @Table basics), see the
axiom-sqlitedata
discipline skill.
This reference covers advanced querying, schema composition, views, and custom aggregates.
Requires iOS 17+, Swift 6 strict concurrency Framework SQLiteData 1.4+

由Point-Free开发的SQLiteData的高级查询模式与模式组合技术。基于GRDBStructuredQueries构建。
核心模式(CRUD、CloudKit设置、@Table基础用法)请查看
axiom-sqlitedata
技能文档。
本参考涵盖高级查询、模式组合、视图与自定义聚合。
要求 iOS 17+、Swift 6严格并发模式 框架 SQLiteData 1.4+

Column Groups and Schema Composition

列组与模式组合

SQLiteData provides powerful tools for composing schema types, enabling reuse, better organization, and single-table inheritance patterns.
SQLiteData提供强大的工具用于组合模式类型,实现复用、更优组织及单表继承模式。

Column Groups

列组

Group related columns into reusable types with
@Selection
:
swift
// 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
}
Important: SQLite has no concept of grouped columns. Flatten all groupings in your CREATE TABLE:
sql
CREATE TABLE "remindersLists" (
    "id" TEXT PRIMARY KEY NOT NULL DEFAULT (uuid()),
    "title" TEXT NOT NULL DEFAULT '',
    "createdAt" TEXT NOT NULL,
    "updatedAt" TEXT
) STRICT
使用
@Selection
将相关列分组为可复用类型:
swift
// 定义可复用列组
@Selection
struct Timestamps {
    let createdAt: Date
    let updatedAt: Date?
}

// 在多张表中使用
@Table
nonisolated struct RemindersList: Identifiable {
    let id: UUID
    var title = ""
    let timestamps: Timestamps  // 嵌入列组
}

@Table
nonisolated struct Reminder: Identifiable {
    let id: UUID
    var title = ""
    var isCompleted = false
    let timestamps: Timestamps  // 复用同一列组
}
重要提示:SQLite没有列组的概念。在CREATE TABLE语句中需要展开所有分组:
sql
CREATE TABLE "remindersLists" (
    "id" TEXT PRIMARY KEY NOT NULL DEFAULT (uuid()),
    "title" TEXT NOT NULL DEFAULT '',
    "createdAt" TEXT NOT NULL,
    "updatedAt" TEXT
) STRICT

Querying Column Groups

查询列组

Access fields inside groups with dot syntax:
swift
// 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)
    }
使用点语法访问组内字段:
swift
// 查询组内字段
RemindersList
    .where { $0.timestamps.createdAt <= cutoffDate }
    .fetchAll(db)

// 比较整个列组(在SQL中展开为元组)
RemindersList
    .where {
        $0.timestamps <= Timestamps(createdAt: date1, updatedAt: date2)
    }

Nesting Groups in @Selection

在@Selection中嵌套列组

Use column groups in custom query results:
swift
@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)
在自定义查询结果中使用列组:
swift
@Selection
struct Row {
    let reminderTitle: String
    let listTitle: String
    let timestamps: Timestamps  // 嵌套列组
}

let results = try Reminder
    .join(RemindersList.all) { $0.remindersListID.eq($1.id) }
    .select {
        Row.Columns(
            reminderTitle: $0.title,
            listTitle: $1.title,
            timestamps: $0.timestamps  // 传递整个列组
        )
    }
    .fetchAll(db)

Single-Table Inheritance with Enums

基于枚举的单表继承

Model polymorphic data using
@CasePathable @Selection
enums — a value-type alternative to class inheritance:
swift
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)
    }
}
Note:
@CasePathable
is required and comes from Point-Free's CasePaths library.
使用
@CasePathable @Selection
枚举建模多态数据——这是类继承的值类型替代方案:
swift
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 Schema for Enum Tables

枚举表的SQL模式

Flatten all cases into nullable columns:
sql
CREATE TABLE "attachments" (
    "id" TEXT PRIMARY KEY NOT NULL DEFAULT (uuid()),
    "link" TEXT,
    "note" TEXT,
    "image" TEXT
) STRICT
将所有枚举case展开为可空列:
sql
CREATE TABLE "attachments" (
    "id" TEXT PRIMARY KEY NOT NULL DEFAULT (uuid()),
    "link" TEXT,
    "note" TEXT,
    "image" TEXT
) STRICT

Querying Enum Tables

查询枚举表

swift
// 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)
swift
// 获取所有数据——解码时自动匹配对应的case
let attachments = try Attachment.all.fetchAll(db)

// 按case过滤
let images = try Attachment
    .where { $0.kind.image.isNot(nil) }
    .fetchAll(db)

Inserting Enum Values

插入枚举值

swift
try Attachment.insert {
    Attachment.Draft(kind: .note("Hello world!"))
}
.execute(db)
// Inserts: (id, NULL, 'Hello world!', NULL)
swift
try Attachment.insert {
    Attachment.Draft(kind: .note("Hello world!"))
}
.execute(db)
// 插入结果:(id, NULL, 'Hello world!', NULL)

Updating Enum Values

更新枚举值

swift
try Attachment.find(id).update {
    $0.kind = .link(URL(string: "https://example.com")!)
}
.execute(db)
// Sets link column, NULLs note and image columns
swift
try Attachment.find(id).update {
    $0.kind = .link(URL(string: "https://example.com")!)
}
.execute(db)
// 设置link列,将note和image列设为NULL

Complex Enum Cases with Grouped Columns

包含分组列的复杂枚举Case

Enum cases can hold structured data using nested
@Selection
types:
swift
@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 schema flattens all nested fields:
sql
CREATE TABLE "attachments" (
    "id" TEXT PRIMARY KEY NOT NULL DEFAULT (uuid()),
    "link" TEXT,
    "note" TEXT,
    "caption" TEXT,
    "url" TEXT
) STRICT
枚举Case可以使用嵌套的
@Selection
类型存储结构化数据:
swift
@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)  // 完全限定嵌套类型
    }

    @Selection
    struct Image {
        var caption = ""
        var url: URL
    }
}
SQL模式会展开所有嵌套字段:
sql
CREATE TABLE "attachments" (
    "id" TEXT PRIMARY KEY NOT NULL DEFAULT (uuid()),
    "link" TEXT,
    "note" TEXT,
    "caption" TEXT,
    "url" TEXT
) STRICT

Passing Rows to Database Functions

向数据库函数传递行数据

With column groups,
@DatabaseFunction
can accept entire table rows:
swift
@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)
通过列组,
@DatabaseFunction
可以接收整个表行:
swift
@DatabaseFunction
func isPastDue(reminder: Reminder) -> Bool {
    !reminder.isCompleted && reminder.dueDate < Date()
}

// 在查询中使用——列会自动展开/重构
let pastDue = try Reminder
    .where { $isPastDue(reminder: $0) }
    .fetchAll(db)

Column Groups vs SwiftData Inheritance

列组 vs SwiftData继承

ApproachSQLiteDataSwiftData
TypeValue types (enums/structs)Reference types (classes)
ExhaustivityCompiler-enforced switchRuntime type checking
VerbosityConcise enum casesVerbose class hierarchy
InheritanceSingle-table via enum@Model class inheritance
Reusable columns
@Selection
groups
Manual repetition
SwiftData equivalent (more verbose):
swift
@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

方式SQLiteDataSwiftData
类型值类型(枚举/结构体)引用类型(类)
穷尽性编译器强制检查运行时类型检查
冗长性简洁的枚举case冗长的类层级
继承方式基于枚举的单表继承@Model类继承
列复用
@Selection
手动重复定义
SwiftData等效实现(更冗长):
swift
@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 }
// 每个类都需要显式调用super.init的初始化方法

Query Composition

查询组合

Build reusable query components as static properties and methods on your tables.
在表上构建可复用的查询组件作为静态属性和方法。

Reusable Scopes

可复用作用域

swift
extension Item {
    // Common filters as static properties
    static let active = Item.where { !$0.isArchived && !$0.isDeleted }
    static let inStock = Item.where(\.isInStock)
    static let outOfStock = Item.where { !$0.isInStock }

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

    static func inCategory(_ category: String) -> Where<Item> {
        Item.where { $0.category.eq(category) }
    }
}
swift
extension Item {
    // 作为静态属性的通用过滤器
    static let active = Item.where { !$0.isArchived && !$0.isDeleted }
    static let inStock = Item.where(\.isInStock)
    static let outOfStock = Item.where { !$0.isInStock }

    // 作为静态方法的参数化作用域
    static func createdAfter(_ date: Date) -> Where<Item> {
        Item.where { $0.createdAt > date }
    }

    static func inCategory(_ category: String) -> Where<Item> {
        Item.where { $0.category.eq(category) }
    }
}

Using Scopes

使用作用域

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

// Combine with additional filtering
let recent = try Item.active
    .createdAfter(lastWeek)
    .inCategory("Electronics")
    .fetchAll(db)
swift
// 链式调用作用域
let results = try Item.active
    .inStock
    .order(by: \.title)
    .fetchAll(db)

// 与额外过滤条件组合
let recent = try Item.active
    .createdAfter(lastWeek)
    .inCategory("Electronics")
    .fetchAll(db)

Default Query Patterns

默认查询模式

swift
extension Item {
    // Standard "all visible" query
    static let visible = Item
        .where { !$0.isDeleted }
        .order(by: \.position)

    // With eager-loaded relationships
    static let withCategory = Item
        .join(Category.all) { $0.categoryID.eq($1.id) }
}

// Use as base for all queries
@FetchAll(Item.visible) var items
swift
extension Item {
    // 标准的“所有可见数据”查询
    static let visible = Item
        .where { !$0.isDeleted }
        .order(by: \.position)

    // 预加载关联关系的查询
    static let withCategory = Item
        .join(Category.all) { $0.categoryID.eq($1.id) }
}

// 作为所有查询的基础
@FetchAll(Item.visible) var items

Composing Where Clauses

组合Where子句

swift
extension Where<Item> {
    // Add filters to existing queries
    func onlyActive() -> Where<Item> {
        self.where { !$0.isArchived }
    }

    func matching(_ search: String) -> Where<Item> {
        self.where { $0.title.contains(search) || $0.notes.contains(search) }
    }
}

// Chain compositions
let results = try Item.inStock
    .onlyActive()
    .matching(searchText)
    .fetchAll(db)
swift
extension Where<Item> {
    // 向现有查询添加过滤器
    func onlyActive() -> Where<Item> {
        self.where { !$0.isArchived }
    }

    func matching(_ search: String) -> Where<Item> {
        self.where { $0.title.contains(search) || $0.notes.contains(search) }
    }
}

// 链式组合
let results = try Item.inStock
    .onlyActive()
    .matching(searchText)
    .fetchAll(db)

Query Helpers for Common Operations

通用操作的查询助手

swift
extension Item {
    // Fetch with common options
    static func search(
        _ query: String,
        category: String? = nil,
        limit: Int = 50
    ) -> some Statement<Item> {
        var base = Item.active.where { $0.title.contains(query) }
        if let category {
            base = base.where { $0.category.eq(category) }
        }
        return base.order(by: \.title).limit(limit)
    }
}

// Clean call sites
let results = try Item.search("phone", category: "Electronics").fetchAll(db)

swift
extension Item {
    // 带通用选项的查询方法
    static func search(
        _ query: String,
        category: String? = nil,
        limit: Int = 50
    ) -> some Statement<Item> {
        var base = Item.active.where { $0.title.contains(query) }
        if let category {
            base = base.where { $0.category.eq(category) }
        }
        return base.order(by: \.title).limit(limit)
    }
}

// 简洁的调用方式
let results = try Item.search("phone", category: "Electronics").fetchAll(db)

Custom Fetch Requests with @Fetch

使用@Fetch的自定义请求

The
@Fetch
property wrapper enables complex, multi-value database requests using custom
FetchKeyRequest
types. Use this when you need to fetch multiple pieces of data in a single database read transaction.
@Fetch
属性包装器允许使用自定义
FetchKeyRequest
类型实现复杂的多值数据库请求。当你需要在单次数据库读取事务中获取多份数据时使用此功能。

Basic @Fetch Usage

@Fetch基础用法

swift
struct PlayersRequest: FetchKeyRequest {
    struct Value {
        let injuredPlayerCount: Int
        let players: [Player]
    }

    func fetch(_ db: Database) throws -> Value {
        try Value(
            injuredPlayerCount: Player
                .where(\.isInjured)
                .fetchCount(db),
            players: Player
                .where { !$0.isInjured }
                .order(by: \.name)
                .limit(10)
                .fetchAll(db)
        )
    }
}

// Use in SwiftUI views
struct PlayersView: View {
    @Fetch(PlayersRequest()) var response

    var body: some View {
        ForEach(response.players) { player in
            Text(player.name)
        }
        Button("View injured players (\(response.injuredPlayerCount))") {
            // ...
        }
    }
}
swift
struct PlayersRequest: FetchKeyRequest {
    struct Value {
        let injuredPlayerCount: Int
        let players: [Player]
    }

    func fetch(_ db: Database) throws -> Value {
        try Value(
            injuredPlayerCount: Player
                .where(\.isInjured)
                .fetchCount(db),
            players: Player
                .where { !$0.isInjured }
                .order(by: \.name)
                .limit(10)
                .fetchAll(db)
        )
    }
}

// 在SwiftUI视图中使用
struct PlayersView: View {
    @Fetch(PlayersRequest()) var response

    var body: some View {
        ForEach(response.players) { player in
            Text(player.name)
        }
        Button("查看受伤球员(\(response.injuredPlayerCount))") {
            // ...
        }
    }
}

When to Use @Fetch vs @FetchAll/@FetchOne

何时使用@Fetch vs @FetchAll/@FetchOne

Use
@FetchAll
/
@FetchOne
when:
  • Fetching a single table
  • Simple queries with one result type
  • Standard CRUD operations
Use
@Fetch
when:
  • Need multiple pieces of data from one or more tables
  • Want to combine query results into a custom type
  • Performing aggregations alongside detail fetches
  • Optimizing for fewer database round trips
当以下情况时使用
@FetchAll
/
@FetchOne
  • 查询单个表
  • 简单查询,仅一种结果类型
  • 标准CRUD操作
当以下情况时使用
@Fetch
  • 需要从一个或多个表中获取多份数据
  • 希望将查询结果组合为自定义类型
  • 在获取详情的同时执行聚合操作
  • 优化以减少数据库往返次数

Complex Example

复杂示例

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

    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),
            recentActivity: ActivityLog
                .order(by: \.timestamp.desc())
                .limit(20)
                .fetchAll(db)
        )
    }
}

@Fetch(DashboardRequest()) var dashboard
swift
struct DashboardRequest: FetchKeyRequest {
    struct Value: Sendable {
        let totalItems: Int
        let activeItems: [Item]
        let categories: [Category]
        let recentActivity: [ActivityLog]
    }

    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),
            recentActivity: ActivityLog
                .order(by: \.timestamp.desc())
                .limit(20)
                .fetchAll(db)
        )
    }
}

@Fetch(DashboardRequest()) var dashboard

Dynamic @Fetch Loading

动态加载@Fetch

Load different requests dynamically with
.load()
:
swift
@Fetch var searchResults = SearchRequest.Value()

// Load with initial query
.task {
    try? await $searchResults.load(SearchRequest(query: "Swift"))
}

// Reload with new query
Button("Search") {
    Task {
        try? await $searchResults.load(SearchRequest(query: newQuery))
    }
}
使用
.load()
动态加载不同的请求:
swift
@Fetch var searchResults = SearchRequest.Value()

// 初始加载查询
.task {
    try? await $searchResults.load(SearchRequest(query: "Swift"))
}

// 重新加载新查询
Button("搜索") {
    Task {
        try? await $searchResults.load(SearchRequest(query: newQuery))
    }
}

@Fetch with Animation

带动画的@Fetch

swift
@Fetch(
    PlayersRequest(),
    animation: .default
) var response
Key Benefits:
  • Single database read transaction (atomic, consistent)
  • Automatic observation (updates when any table changes)
  • Type-safe result structure
  • Composable with other query patterns

swift
@Fetch(
    PlayersRequest(),
    animation: .default
) var response
核心优势:
  • 单次数据库读取事务(原子性、一致性)
  • 自动观察(当任何表变化时自动更新)
  • 类型安全的结果结构
  • 可与其他查询模式组合

Advanced Query Patterns

高级查询模式

String Functions

字符串函数

swift
// Case conversion
let upper = try Item
    .select { $0.title.upper() }
    .fetchAll(db)

let lower = try Item
    .select { $0.title.lower() }
    .fetchAll(db)

// Trimming whitespace
let trimmed = try Item
    .select { $0.title.trim() }       // Both sides
    .fetchAll(db)

let leftTrimmed = try Item
    .select { $0.title.ltrim() }      // Left only
    .fetchAll(db)

// Substring extraction
let firstThree = try Item
    .select { $0.title.substr(0, 3) }  // Start index, length
    .fetchAll(db)

// String replacement
let cleaned = try Item
    .select { $0.title.replace("old", "new") }
    .fetchAll(db)

// String length
let lengths = try Item
    .select { ($0.title, $0.title.length()) }
    .fetchAll(db)

// Find substring position (1-indexed, 0 if not found)
let positions = try Item
    .where { $0.title.instr("search") > 0 }
    .fetchAll(db)

// Pattern matching
let matches = try Item
    .where { $0.title.like("%phone%") }           // SQL LIKE
    .fetchAll(db)

let prefixed = try Item
    .where { $0.title.hasPrefix("iPhone") }       // Starts with
    .fetchAll(db)

let suffixed = try Item
    .where { $0.title.hasSuffix("Pro") }          // Ends with
    .fetchAll(db)

let containing = try Item
    .where { $0.title.contains("Max") }           // Contains
    .fetchAll(db)

// Case-insensitive comparison
let caseInsensitive = try Item
    .where { $0.title.collate(.nocase).eq("IPHONE") }
    .fetchAll(db)
swift
// 大小写转换
let upper = try Item
    .select { $0.title.upper() }
    .fetchAll(db)

let lower = try Item
    .select { $0.title.lower() }
    .fetchAll(db)

// 修剪空白字符
let trimmed = try Item
    .select { $0.title.trim() }       // 两端
    .fetchAll(db)

let leftTrimmed = try Item
    .select { $0.title.ltrim() }      // 仅左侧
    .fetchAll(db)

// 提取子字符串
let firstThree = try Item
    .select { $0.title.substr(0, 3) }  // 起始索引,长度
    .fetchAll(db)

// 字符串替换
let cleaned = try Item
    .select { $0.title.replace("old", "new") }
    .fetchAll(db)

// 字符串长度
let lengths = try Item
    .select { ($0.title, $0.title.length()) }
    .fetchAll(db)

// 查找子字符串位置(1为起始索引,未找到则返回0)
let positions = try Item
    .where { $0.title.instr("search") > 0 }
    .fetchAll(db)

// 模式匹配
let matches = try Item
    .where { $0.title.like("%phone%") }           // SQL LIKE
    .fetchAll(db)

let prefixed = try Item
    .where { $0.title.hasPrefix("iPhone") }       // 以指定内容开头
    .fetchAll(db)

let suffixed = try Item
    .where { $0.title.hasSuffix("Pro") }          // 以指定内容结尾
    .fetchAll(db)

let containing = try Item
    .where { $0.title.contains("Max") }           // 包含指定内容
    .fetchAll(db)

// 大小写不敏感比较
let caseInsensitive = try Item
    .where { $0.title.collate(.nocase).eq("IPHONE") }
    .fetchAll(db)

Null Handling

Null处理

swift
// Coalesce — return first non-null value
let displayName = try User
    .select { $0.nickname ?? $0.firstName ?? "Anonymous" }
    .fetchAll(db)

// ifnull — alternative if null
let safePrice = try Item
    .select { $0.discountPrice.ifnull($0.price) }
    .fetchAll(db)

// Check for null
let withDueDate = try Reminder
    .where { $0.dueDate.isNot(nil) }
    .fetchAll(db)

let noDueDate = try Reminder
    .where { $0.dueDate.is(nil) }
    .fetchAll(db)

// Null-safe comparison in ordering
let sorted = try Item
    .order { $0.priority.desc(nulls: .last) }  // Nulls at end
    .fetchAll(db)
swift
// Coalesce — 返回第一个非null值
let displayName = try User
    .select { $0.nickname ?? $0.firstName ?? "Anonymous" }
    .fetchAll(db)

// ifnull — null时返回替代值
let safePrice = try Item
    .select { $0.discountPrice.ifnull($0.price) }
    .fetchAll(db)

// 检查是否为null
let withDueDate = try Reminder
    .where { $0.dueDate.isNot(nil) }
    .fetchAll(db)

let noDueDate = try Reminder
    .where { $0.dueDate.is(nil) }
    .fetchAll(db)

// 排序时的Null安全比较
let sorted = try Item
    .order { $0.priority.desc(nulls: .last) }  // Null值排在末尾
    .fetchAll(db)

Range and Set Membership

范围与集合成员

swift
// IN — check if value is in a set
let selected = try Item
    .where { $0.id.in(selectedIds) }
    .fetchAll(db)

// IN with subquery
let itemsInActiveCategories = 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 — range check
let midRange = try Item
    .where { $0.price.between(10, and: 100) }
    .fetchAll(db)

// Swift range syntax
let inRange = try Item
    .where { (10...100).contains($0.price) }
    .fetchAll(db)
swift
// IN — 检查值是否在集合中
let selected = try Item
    .where { $0.id.in(selectedIds) }
    .fetchAll(db)

// IN与子查询
let itemsInActiveCategories = 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 — 范围检查
let midRange = try Item
    .where { $0.price.between(10, and: 100) }
    .fetchAll(db)

// Swift范围语法
let inRange = try Item
    .where { (10...100).contains($0.price) }
    .fetchAll(db)

Dynamic Queries

动态查询

swift
struct ContentView: View {
    @Fetch(Search(), animation: .default)
    private var results = Search.Value()

    @State var query = ""

    var body: some View {
        List { /* ... */ }
            .searchable(text: $query)
            .task(id: query) {
                try await $results.load(Search(query: query), animation: .default)
            }
    }
}

struct Search: FetchKeyRequest {
    var query = ""
    struct Value { var items: [Item] = [] }

    func fetch(_ db: Database) throws -> Value {
        let search = Item
            .where { $0.title.contains(query) }
            .order { $0.title }
        return try Value(items: search.fetchAll(db))
    }
}
swift
struct ContentView: View {
    @Fetch(Search(), animation: .default)
    private var results = Search.Value()

    @State var query = ""

    var body: some View {
        List { /* ... */ }
            .searchable(text: $query)
            .task(id: query) {
                try await $results.load(Search(query: query), animation: .default)
            }
    }
}

struct Search: FetchKeyRequest {
    var query = ""
    struct Value { var items: [Item] = [] }

    func fetch(_ db: Database) throws -> Value {
        let search = Item
            .where { $0.title.contains(query) }
            .order { $0.title }
        return try Value(items: search.fetchAll(db))
    }
}

Distinct Results

去重结果

Remove duplicate rows from query results:
swift
// Get unique categories
let categories = try Item
    .select(\.category)
    .distinct()
    .fetchAll(db)

// Distinct with multiple columns
let uniquePairs = try Item
    .select { ($0.category, $0.status) }
    .distinct()
    .fetchAll(db)
从查询结果中移除重复行:
swift
// 获取唯一分类
let categories = try Item
    .select(\.category)
    .distinct()
    .fetchAll(db)

// 多列去重
let uniquePairs = try Item
    .select { ($0.category, $0.status) }
    .distinct()
    .fetchAll(db)

Pagination

分页

Use
limit()
and
offset()
for paged results:
swift
let pageSize = 20
let page = 3

let items = try Item
    .order(by: \.createdAt)
    .limit(pageSize, offset: page * pageSize)
    .fetchAll(db)
Tip: For large datasets, cursor-based pagination (using last item's ID) is more efficient than offset:
swift
// Cursor-based: more efficient for deep pages
let items = try Item
    .where { $0.id > lastSeenId }
    .order(by: \.id)
    .limit(pageSize)
    .fetchAll(db)

使用
limit()
offset()
实现分页结果:
swift
let pageSize = 20
let page = 3

let items = try Item
    .order(by: \.createdAt)
    .limit(pageSize, offset: page * pageSize)
    .fetchAll(db)
提示:对于大型数据集,基于游标的分页(使用最后一条数据的ID)比offset更高效:
swift
// 基于游标的分页:对于深层页面更高效
let items = try Item
    .where { $0.id > lastSeenId }
    .order(by: \.id)
    .limit(pageSize)
    .fetchAll(db)

RETURNING Clause

RETURNING子句

Fetch generated values from INSERT, UPDATE, or DELETE operations:
从INSERT、UPDATE或DELETE操作中获取生成的值:

Get Generated ID from Insert

从插入中获取生成的ID

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

// Insert and get the full inserted record
let newItem = try Item.insert {
    Item.Draft(title: "New Item")
}
.returning(Item.self)
.fetchOne(db)
swift
// 插入并获取自动生成的UUID
let newId = try Item.insert {
    Item.Draft(title: "New Item")
}
.returning(\.id)
.fetchOne(db)

// 插入并获取完整的插入记录
let newItem = try Item.insert {
    Item.Draft(title: "New Item")
}
.returning(Item.self)
.fetchOne(db)

Get Updated Values

获取更新后的值

swift
// Update and return the new values
let updatedTitles = try Item
    .where { $0.isInStock }
    .update { $0.title = "Updated: " + $0.title }
    .returning(\.title)
    .fetchAll(db)

// Return multiple columns
let updates = try Item.find(id)
    .update { $0.count += 1 }
    .returning { ($0.id, $0.count) }
    .fetchOne(db)
swift
// 更新并返回新值
let updatedTitles = try Item
    .where { $0.isInStock }
    .update { $0.title = "Updated: " + $0.title }
    .returning(\.title)
    .fetchAll(db)

// 返回多列
let updates = try Item.find(id)
    .update { $0.count += 1 }
    .returning { ($0.id, $0.count) }
    .fetchOne(db)

Get Deleted Records

获取删除的记录

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

print("Deleted \(deleted.count) archived items")
When to use RETURNING:
  • Get auto-generated IDs without a second query
  • Audit deleted records before removal
  • Verify updated values match expectations
  • Batch operations that need result confirmation

swift
// 在删除前捕获记录
let deleted = try Item
    .where { $0.isArchived }
    .delete()
    .returning(Item.self)
    .fetchAll(db)

print("已删除 \(deleted.count) 条归档项")
何时使用RETURNING:
  • 无需二次查询即可获取自动生成的ID
  • 在删除前审计记录
  • 验证更新后的值是否符合预期
  • 需要结果确认的批量操作

Joins

连接

Basic Joins

基础连接

swift
extension Reminder {
    static let withTags = group(by: \.id)
        .leftJoin(ReminderTag.all) { $0.id.eq($1.reminderID) }
        .leftJoin(Tag.all) { $1.tagID.eq($2.primaryKey) }
}
swift
extension Reminder {
    static let withTags = group(by: \.id)
        .leftJoin(ReminderTag.all) { $0.id.eq($1.reminderID) }
        .leftJoin(Tag.all) { $1.tagID.eq($2.primaryKey) }
}

Join Types

连接类型

swift
// INNER JOIN — only matching rows
let itemsWithCategories = try Item
    .join(Category.all) { $0.categoryID.eq($1.id) }
    .fetchAll(db)

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

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

// FULL OUTER JOIN — all from both
let allCombined = try Item
    .fullJoin(Category.all) { $0.categoryID.eq($1.id) }
    .select { ($0, $1) }  // (Item?, Category?)
    .fetchAll(db)
swift
// INNER JOIN — 仅返回匹配的行
let itemsWithCategories = try Item
    .join(Category.all) { $0.categoryID.eq($1.id) }
    .fetchAll(db)

// LEFT JOIN — 返回左表所有行,右表匹配的行(可空)
let itemsWithOptionalCategory = try Item
    .leftJoin(Category.all) { $0.categoryID.eq($1.id) }
    .select { ($0, $1) }  // (Item, Category?)
    .fetchAll(db)

// RIGHT JOIN — 返回右表所有行,左表匹配的行
let categoriesWithItems = try Item
    .rightJoin(Category.all) { $0.categoryID.eq($1.id) }
    .select { ($0, $1) }  // (Item?, Category)
    .fetchAll(db)

// FULL OUTER JOIN — 返回两张表的所有行
let allCombined = try Item
    .fullJoin(Category.all) { $0.categoryID.eq($1.id) }
    .select { ($0, $1) }  // (Item?, Category?)
    .fetchAll(db)

Self-Joins with TableAlias

使用TableAlias的自连接

Query the same table twice (e.g., employee/manager relationships):
swift
// Define an alias for the second reference
struct ManagerAlias: TableAlias {
    typealias Table = Employee
}

// Employee with their manager's name
let employeesWithManagers = try Employee
    .leftJoin(Employee.all.as(ManagerAlias.self)) { $0.managerID.eq($1.id) }
    .select {
        (
            employeeName: $0.name,
            managerName: $1.name  // From aliased table
        )
    }
    .fetchAll(db)

// Find employees who manage others
let managers = try Employee
    .join(Employee.all.as(ManagerAlias.self)) { $0.id.eq($1.managerID) }
    .select { $0 }
    .distinct()
    .fetchAll(db)

查询同一张表两次(例如员工/经理关系):
swift
// 为第二个引用定义别名
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)

// 查找管理其他员工的经理
let managers = try Employee
    .join(Employee.all.as(ManagerAlias.self)) { $0.id.eq($1.managerID) }
    .select { $0 }
    .distinct()
    .fetchAll(db)

Case Expressions

CASE表达式

CASE WHEN logic for conditional values in queries:
swift
// 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")
            .when($0.createdAt.isNot(nil), then: "Pending")
            .else("Unknown")
    }
    .fetchAll(db)

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

// Case for computed columns
let itemsWithTier = try Item
    .select {
        (
            $0.title,
            Case()
                .when($0.price < 10, then: "Budget")
                .when($0.price < 100, then: "Standard")
                .else("Premium")
        )
    }
    .fetchAll(db)

用于查询中条件值的CASE WHEN逻辑:
swift
// 简单CASE — 映射值
let labels = try Item
    .select {
        Case($0.priority)
            .when(1, then: "Low")
            .when(2, then: "Medium")
            .when(3, then: "High")
            .else("Unknown")
    }
    .fetchAll(db)

// 搜索式CASE — 布尔条件
let status = try Order
    .select {
        Case()
            .when($0.shippedAt.isNot(nil), then: "已发货")
            .when($0.paidAt.isNot(nil), then: "已付款")
            .when($0.createdAt.isNot(nil), then: "待处理")
            .else("未知")
    }
    .fetchAll(db)

// 更新中的CASE(切换模式)
try Reminder.find(id).update {
    $0.status = Case($0.status)
        .when(.incomplete, then: .completing)
        .when(.completing, then: .completed)
        .else(.incomplete)
}
.execute(db)

// 用于计算列的CASE
let itemsWithTier = try Item
    .select {
        (
            $0.title,
            Case()
                .when($0.price < 10, then: "经济型")
                .when($0.price < 100, then: "标准型")
                .else("高端型")
        )
    }
    .fetchAll(db)

Common Table Expressions (CTEs)

通用表表达式(CTE)

Non-Recursive CTEs

非递归CTE

Simplify complex queries by breaking them into named subqueries:
swift
// Define a CTE for expensive items
let expensiveItems = try With {
    Item.where { $0.price > 1000 }
} query: { expensive in
    // Use the CTE in the final query
    expensive
        .order(by: \.price)
        .limit(10)
}
.fetchAll(db)

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

// CTE for deduplication
let uniqueEmails = try With {
    Customer
        .group(by: \.email)
        .select { ($0.email, $0.id.min()) }
} query: { unique in
    Customer
        .where { $0.id.in(unique.select { $1 }) }
}
.fetchAll(db)
When to use CTEs:
  • Break complex queries into readable parts
  • Reuse a subquery multiple times
  • Improve query plan for complex joins
  • Self-documenting query structure
通过将复杂查询拆分为命名子查询来简化:
swift
// 定义高价商品的CTE
let expensiveItems = try With {
    Item.where { $0.price > 1000 }
} query: { expensive in
    // 在最终查询中使用CTE
    expensive
        .order(by: \.price)
        .limit(10)
}
.fetchAll(db)

// 多个CTE
let report = try With {
    // CTE 1: 高价值客户
    Customer.where { $0.totalSpent > 10000 }
} with: {
    // CTE 2: 近期订单
    Order.where { $0.createdAt > lastMonth }
} query: { highValue, recentOrders in
    // 连接CTE
    highValue
        .join(recentOrders) { $0.id.eq($1.customerID) }
        .select { ($0.name, $1.total) }
}
.fetchAll(db)

// 用于去重的CTE
let uniqueEmails = try With {
    Customer
        .group(by: \.email)
        .select { ($0.email, $0.id.min()) }
} query: { unique in
    Customer
        .where { $0.id.in(unique.select { $1 }) }
}
.fetchAll(db)
何时使用CTE:
  • 将复杂查询拆分为易读的部分
  • 重复使用子查询
  • 优化复杂连接的查询计划
  • 自文档化的查询结构

Recursive CTEs

递归CTE

Query hierarchical data like trees, org charts, or threaded comments:
swift
// Define a tree structure
@Table
nonisolated struct Category: Identifiable {
    let id: UUID
    var name = ""
    var parentID: UUID?  // Self-referential for hierarchy
}

// Recursive query to get all descendants
let allDescendants = try With {
    // Base case: start with root category
    Category.where { $0.id.eq(rootCategoryId) }
} recursiveUnion: { cte in
    // Recursive case: join children to CTE
    Category.all
        .join(cte) { $0.parentID.eq($1.id) }
        .select { $0 }
} query: { cte in
    // Final query from the CTE
    cte.order(by: \.name)
}
.fetchAll(db)
查询层级数据如树形结构、组织架构或线程化评论:
swift
// 定义树形结构
@Table
nonisolated struct Category: Identifiable {
    let id: UUID
    var name = ""
    var parentID: UUID?  // 自引用以实现层级
}

// 递归查询获取所有子分类
let allDescendants = try With {
    // 基础情况:从根分类开始
    Category.where { $0.id.eq(rootCategoryId) }
} recursiveUnion: { cte in
    // 递归情况:将子分类连接到CTE
    Category.all
        .join(cte) { $0.parentID.eq($1.id) }
        .select { $0 }
} query: { cte in
    // 从CTE执行最终查询
    cte.order(by: \.name)
}
.fetchAll(db)

Ancestor Path (Walking Up the Tree)

祖先路径(向上遍历树)

swift
// Get all ancestors of a category
let ancestors = try With {
    Category.where { $0.id.eq(childCategoryId) }
} recursiveUnion: { cte in
    Category.all
        .join(cte) { $0.id.eq($1.parentID) }
        .select { $0 }
} query: { cte in
    cte.all
}
.fetchAll(db)
swift
// 获取某个分类的所有祖先
let ancestors = try With {
    Category.where { $0.id.eq(childCategoryId) }
} recursiveUnion: { cte in
    Category.all
        .join(cte) { $0.id.eq($1.parentID) }
        .select { $0 }
} query: { cte in
    cte.all
}
.fetchAll(db)

Threaded Comments

线程化评论

swift
@Table
nonisolated struct Comment: Identifiable {
    let id: UUID
    var body = ""
    var parentID: UUID?
    var depth = 0
}

// Get comment thread with depth
let thread = try With {
    Comment
        .where { $0.parentID.is(nil) && $0.postID.eq(postId) }
        .select { ($0, 0) }  // depth = 0 for root
} recursiveUnion: { cte in
    Comment.all
        .join(cte) { $0.parentID.eq($1.id) }
        .select { ($0, $1.depth + 1) }
} query: { cte in
    cte.order { ($0.depth, $0.createdAt) }
}
.fetchAll(db)

swift
@Table
nonisolated struct Comment: Identifiable {
    let id: UUID
    var body = ""
    var parentID: UUID?
    var depth = 0
}

// 获取带深度的评论线程
let thread = try With {
    Comment
        .where { $0.parentID.is(nil) && $0.postID.eq(postId) }
        .select { ($0, 0) }  // 根评论深度为0
} recursiveUnion: { cte in
    Comment.all
        .join(cte) { $0.parentID.eq($1.id) }
        .select { ($0, $1.depth + 1) }
} query: { cte in
    cte.order { ($0.depth, $0.createdAt) }
}
.fetchAll(db)

Full-Text Search (FTS5)

全文搜索(FTS5)

Basic FTS5

基础FTS5

swift
@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)
swift
@Table
struct ReminderText: FTS5 {
    let rowid: Int
    let title: String
    let notes: String
    let tags: String
}

// 在迁移中创建FTS表
try #sql(
    """
    CREATE VIRTUAL TABLE "reminderTexts" USING fts5(
        "title", "notes", "tags",
        tokenize = 'trigram'
    )
    """
)
.execute(db)

Advanced FTS5 Features

高级FTS5功能

Beyond basic
match()
, FTS5 provides search UI helpers:
swift
@Table
struct ItemText: FTS5 {
    let rowid: Int
    let title: String
    let description: String
}

// Highlight search terms in results
let results = try ItemText
    .where { $0.match(searchQuery) }
    .select {
        (
            $0.rowid,
            $0.title.highlight("<b>", "</b>"),      // <b>search</b> term
            $0.description.highlight("<mark>", "</mark>")
        )
    }
    .fetchAll(db)

// Extract snippets with context
let snippets = try ItemText
    .where { $0.match(searchQuery) }
    .select {
        $0.description.snippet(
            "<b>", "</b>",  // highlight markers
            "...",          // ellipsis for truncation
            64              // max tokens
        )
    }
    .fetchAll(db)
// "...the <b>search</b> term appears in context..."

// BM25 ranking for relevance sorting
let ranked = try ItemText
    .where { $0.match(searchQuery) }
    .order { $0.bm25().desc() }  // Most relevant first
    .select {
        ($0.title, $0.bm25())
    }
    .fetchAll(db)

除了基础的
match()
,FTS5还提供搜索UI辅助功能:
swift
@Table
struct ItemText: FTS5 {
    let rowid: Int
    let title: String
    let description: String
}

// 在结果中高亮搜索词
let results = try ItemText
    .where { $0.match(searchQuery) }
    .select {
        (
            $0.rowid,
            $0.title.highlight("<b>", "</b>"),      // <b>搜索</b>词
            $0.description.highlight("<mark>", "</mark>")
        )
    }
    .fetchAll(db)

// 提取带上下文的片段
let snippets = try ItemText
    .where { $0.match(searchQuery) }
    .select {
        $0.description.snippet(
            "<b>", "</b>",  // 高亮标记
            "...",          // 截断省略号
            64              // 最大令牌数
        )
    }
    .fetchAll(db)
// "...<b>搜索</b>词出现在上下文中..."

// BM25相关性排序
let ranked = try ItemText
    .where { $0.match(searchQuery) }
    .order { $0.bm25().desc() }  // 最相关的结果排在前面
    .select {
        ($0.title, $0.bm25())
    }
    .fetchAll(db)

Aggregation

聚合

String Aggregation (groupConcat)

字符串聚合(groupConcat)

Concatenate values from multiple rows into a single string:
swift
// Get comma-separated tags for each 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")

// With ordering within the aggregate
let orderedTags = try Item
    .group(by: \.id)
    .leftJoin(Tag.all) { /* ... */ }
    .select {
        $2.name.groupConcat(separator: ", ", order: { $0.asc() })
    }
    .fetchAll(db)

// Distinct values only
let uniqueCategories = try Item
    .group(by: \.storeID)
    .select {
        $0.category.groupConcat(distinct: true, separator: " | ")
    }
    .fetchAll(db)
将多行的值连接为单个字符串:
swift
// 获取每个商品的逗号分隔标签
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")

// 聚合内排序
let orderedTags = try Item
    .group(by: \.id)
    .leftJoin(Tag.all) { /* ... */ }
    .select {
        $2.name.groupConcat(separator: ", ", order: { $0.asc() })
    }
    .fetchAll(db)

// 仅去重值
let uniqueCategories = try Item
    .group(by: \.storeID)
    .select {
        $0.category.groupConcat(distinct: true, separator: " | ")
    }
    .fetchAll(db)

JSON Aggregation

JSON聚合

Build JSON arrays and objects directly in queries:
swift
// Aggregate rows into JSON array
let itemsJson = try Store
    .group(by: \.id)
    .leftJoin(Item.all) { $0.id.eq($1.storeID) }
    .select {
        (
            $0.name,
            $1.title.jsonGroupArray()  // ["item1", "item2", ...]
        )
    }
    .fetchAll(db)

// With filtering
let activeItemsJson = try Store
    .group(by: \.id)
    .leftJoin(Item.all) { $0.id.eq($1.storeID) }
    .select {
        $1.title.jsonGroupArray(filter: $1.isActive)
    }
    .fetchAll(db)

// Build JSON objects
let storeData = try Store
    .select {
        jsonObject(
            "id", $0.id,
            "name", $0.name,
            "itemCount", $0.itemCount
        )
    }
    .fetchAll(db)
直接在查询中构建JSON数组和对象:
swift
// 将行聚合为JSON数组
let itemsJson = try Store
    .group(by: \.id)
    .leftJoin(Item.all) { $0.id.eq($1.storeID) }
    .select {
        (
            $0.name,
            $1.title.jsonGroupArray()  // ["item1", "item2", ...]
        )
    }
    .fetchAll(db)

// 带过滤的聚合
let activeItemsJson = try Store
    .group(by: \.id)
    .leftJoin(Item.all) { $0.id.eq($1.storeID) }
    .select {
        $1.title.jsonGroupArray(filter: $1.isActive)
    }
    .fetchAll(db)

// 构建JSON对象
let storeData = try Store
    .select {
        jsonObject(
            "id", $0.id,
            "name", $0.name,
            "itemCount", $0.itemCount
        )
    }
    .fetchAll(db)

Aggregate Functions with Filters

带过滤的聚合函数

All aggregate functions support conditional aggregation:
swift
let stats = try Item
    .select {
        Stats.Columns(
            total: $0.count(),
            activeCount: $0.count(filter: $0.isActive),
            inStockCount: $0.count(filter: $0.isInStock),
            avgPrice: $0.price.avg(),
            avgActivePrice: $0.price.avg(filter: $0.isActive),
            maxDiscount: $0.discount.max(filter: $0.isOnSale),
            totalRevenue: $0.revenue.sum(filter: $0.status.eq(.completed))
        )
    }
    .fetchOne(db)
所有聚合函数都支持条件聚合:
swift
let stats = try Item
    .select {
        Stats.Columns(
            total: $0.count(),
            activeCount: $0.count(filter: $0.isActive),
            inStockCount: $0.count(filter: $0.isInStock),
            avgPrice: $0.price.avg(),
            avgActivePrice: $0.price.avg(filter: $0.isActive),
            maxDiscount: $0.discount.max(filter: $0.isOnSale),
            totalRevenue: $0.revenue.sum(filter: $0.status.eq(.completed))
        )
    }
    .fetchOne(db)

HAVING Clause

HAVING子句

Filter grouped results after aggregation with
.having()
:
swift
// Customers with more than 5 orders
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)

// Categories with total sales over threshold
let topCategories = try Category
    .group(by: \.id)
    .leftJoin(Item.all) { $0.id.eq($1.categoryID) }
    .having { $1.price.sum() > 10000 }
    .select { ($0.name, $1.price.sum()) }
    .fetchAll(db)

// Combined WHERE and HAVING
// WHERE filters rows before grouping, HAVING filters after
let activeHighVolume = try Store
    .where(\.isActive)                          // Before grouping
    .group(by: \.id)
    .leftJoin(Order.all) { $0.id.eq($1.storeID) }
    .having { $1.count() >= 100 }               // After grouping
    .select { ($0.name, $1.count()) }
    .fetchAll(db)
When to use:
  • .where()
    — Filter individual rows before grouping
  • .having()
    — Filter groups after aggregation based on aggregate values

使用
.having()
在聚合后过滤分组结果:
swift
// 订单数超过5的客户
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)

// 总销售额超过阈值的分类
let topCategories = try Category
    .group(by: \.id)
    .leftJoin(Item.all) { $0.id.eq($1.categoryID) }
    .having { $1.price.sum() > 10000 }
    .select { ($0.name, $1.price.sum()) }
    .fetchAll(db)

// 组合WHERE和HAVING
// WHERE在分组前过滤行,HAVING在分组后过滤
let activeHighVolume = try Store
    .where(\.isActive)                          // 分组前
    .group(by: \.id)
    .leftJoin(Order.all) { $0.id.eq($1.storeID) }
    .having { $1.count() >= 100 }               // 分组后
    .select { ($0.name, $1.count()) }
    .fetchAll(db)
何时使用:
  • .where()
    — 分组前过滤单个行
  • .having()
    — 基于聚合值在分组后过滤组

Schema Creation with #sql Macro

使用#sql宏创建模式

The
#sql
macro from StructuredQueries enables type-safe raw SQL for schema creation, migrations, and custom DDL statements.
来自StructuredQueries
#sql
宏支持类型安全的原始SQL,用于模式创建、迁移和自定义DDL语句。

CREATE TABLE in Migrations

迁移中的CREATE TABLE

swift
func appDatabase() throws -> any DatabaseWriter {
    let databaseQueue = try DatabaseQueue()
    var migrator = DatabaseMigrator()

    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)

        try #sql(
            """
            CREATE TABLE "categories" (
                "id" TEXT PRIMARY KEY NOT NULL DEFAULT (uuid()),
                "name" TEXT NOT NULL UNIQUE,
                "position" INTEGER NOT NULL DEFAULT 0
            ) STRICT
            """
        ).execute(db)

        // Foreign key relationship
        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)
    }

    try migrator.migrate(databaseQueue)
    return databaseQueue
}
swift
func appDatabase() throws -> any DatabaseWriter {
    let databaseQueue = try DatabaseQueue()
    var migrator = DatabaseMigrator()

    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)

        try #sql(
            """
            CREATE TABLE "categories" (
                "id" TEXT PRIMARY KEY NOT NULL DEFAULT (uuid()),
                "name" TEXT NOT NULL UNIQUE,
                "position" INTEGER NOT NULL DEFAULT 0
            ) STRICT
            """
        ).execute(db)

        // 外键关系
        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)
    }

    try migrator.migrate(databaseQueue)
    return databaseQueue
}

Parameter Interpolation with (raw:)

使用(raw:)进行参数插值

Use
\(raw:)
for literal SQL values (table names, column names) and regular
\()
for query parameters:
swift
migrator.registerMigration("Create table with dynamic defaults") { db in
    let defaultListColor = Color.HexRepresentation(queryOutput: defaultColor).hexValue
    let tableName = "remindersLists"

    try #sql(
        """
        CREATE TABLE \(raw: tableName) (
            "id" TEXT PRIMARY KEY NOT NULL DEFAULT (uuid()),
            "color" INTEGER NOT NULL DEFAULT \(raw: defaultListColor ?? 0),
            "title" TEXT NOT NULL DEFAULT ''
        ) STRICT
        """
    ).execute(db)
}
⚠️ Safety:
  • \(value)
    → Automatically escaped, prevents SQL injection
  • \(raw: value)
    → Inserted literally, use ONLY for identifiers you control
  • Never use
    \(raw: userInput)
    — this creates SQL injection vulnerability
使用
\(raw:)
插入字面SQL值(表名、列名),使用常规
\()
插入查询参数:
swift
migrator.registerMigration("Create table with dynamic defaults") { db in
    let defaultListColor = Color.HexRepresentation(queryOutput: defaultColor).hexValue
    let tableName = "remindersLists"

    try #sql(
        """
        CREATE TABLE \(raw: tableName) (
            "id" TEXT PRIMARY KEY NOT NULL DEFAULT (uuid()),
            "color" INTEGER NOT NULL DEFAULT \(raw: defaultListColor ?? 0),
            "title" TEXT NOT NULL DEFAULT ''
        ) STRICT
        """
    ).execute(db)
}
⚠️ 安全提示:
  • \(value)
    → 自动转义,防止SQL注入
  • \(raw: value)
    → 字面插入,仅用于你可控的标识符
  • 绝不要使用
    \(raw: userInput)
    — 这会造成SQL注入漏洞

CREATE INDEX

CREATE INDEX

swift
migrator.registerMigration("Add indexes") { db in
    try #sql(
        """
        CREATE INDEX "idx_items_createdAt"
        ON "items" ("createdAt" DESC)
        """
    ).execute(db)

    try #sql(
        """
        CREATE INDEX "idx_items_search"
        ON "items" ("title", "isInStock")
        WHERE "isArchived" = 0
        """
    ).execute(db)
}
swift
migrator.registerMigration("Add indexes") { db in
    try #sql(
        """
        CREATE INDEX "idx_items_createdAt"
        ON "items" ("createdAt" DESC)
        """
    ).execute(db)

    try #sql(
        """
        CREATE INDEX "idx_items_search"
        ON "items" ("title", "isInStock")
        WHERE "isArchived" = 0
        """
    ).execute(db)
}

CREATE TRIGGER

CREATE TRIGGER

swift
migrator.registerMigration("Add audit triggers") { db in
    try #sql(
        """
        CREATE TRIGGER "update_item_timestamp"
        AFTER UPDATE ON "items"
        BEGIN
            UPDATE "items"
            SET "updatedAt" = datetime('now')
            WHERE "id" = NEW."id";
        END
        """
    ).execute(db)
}
swift
migrator.registerMigration("Add audit triggers") { db in
    try #sql(
        """
        CREATE TRIGGER "update_item_timestamp"
        AFTER UPDATE ON "items"
        BEGIN
            UPDATE "items"
            SET "updatedAt" = datetime('now')
            WHERE "id" = NEW."id";
        END
        """
    ).execute(db)
}

ALTER TABLE

ALTER TABLE

swift
migrator.registerMigration("Add notes column") { db in
    try #sql(
        """
        ALTER TABLE "items"
        ADD COLUMN "notes" TEXT NOT NULL DEFAULT ''
        """
    ).execute(db)
}
swift
migrator.registerMigration("Add notes column") { db in
    try #sql(
        """
        ALTER TABLE "items"
        ADD COLUMN "notes" TEXT NOT NULL DEFAULT ''
        """
    ).execute(db)
}

When to Use #sql for Schema

何时使用#sql创建模式

Use #sql when:
  • Creating tables in migrations
  • Adding indexes, triggers, views
  • Complex DDL that query builder doesn't support
  • Need full control over SQLite STRICT tables
Don't use #sql for:
  • Regular queries (use query builder:
    Item.where(...)
    )
  • Simple inserts/updates/deletes (use
    .insert()
    ,
    .update()
    ,
    .delete()
    )
  • Anything available in type-safe query builder

当以下情况时使用#sql:
  • 在迁移中创建表
  • 添加索引、触发器、视图
  • 查询构建器不支持的复杂DDL
  • 需要完全控制SQLite STRICT表
不要在以下情况使用#sql:
  • 常规查询(使用查询构建器:
    Item.where(...)
  • 简单的插入/更新/删除(使用
    .insert()
    .update()
    .delete()
  • 任何类型安全查询构建器已支持的操作

Database Views

数据库视图

SQLiteData provides type-safe, schema-safe wrappers around SQLite Views — pre-packaged SELECT statements that can be queried like tables.
SQLiteData为SQLite视图提供类型安全、模式安全的包装器——预定义的SELECT语句,可以像表一样查询。

Understanding @Selection

理解@Selection

The
@Selection
macro defines custom query result types. Use it for:
  1. Custom query results — Shape data from joins without a view
  2. Combined with
    @Table
    — Define a view-backed type
@Selection
宏定义自定义查询结果类型。用于:
  1. 自定义查询结果 — 无需视图即可从连接中塑造数据
  2. @Table
    结合
    — 定义视图支持的类型

@Selection for Custom Query Results

用于自定义查询结果的@Selection

swift
// Define a custom result shape for a join query
@Selection
struct ReminderWithList: Identifiable {
    var id: Reminder.ID { reminder.id }
    let reminder: Reminder
    let remindersList: RemindersList
    let isPastDue: Bool
    let tags: String
}

// Use in a join query
@FetchAll(
    Reminder
        .join(RemindersList.all) { $0.remindersListID.eq($1.id) }
        .select {
            ReminderWithList.Columns(
                reminder: $0,
                remindersList: $1,
                isPastDue: $0.isPastDue,
                tags: ""  // computed elsewhere
            )
        }
)
var reminders: [ReminderWithList]
Key insight:
@Selection
generates a
.Columns
type for use in
.select { }
closures, providing compile-time verification that your query results match your Swift type.
swift
// 为连接查询定义自定义结果结构
@Selection
struct ReminderWithList: Identifiable {
    var id: Reminder.ID { reminder.id }
    let reminder: Reminder
    let remindersList: RemindersList
    let isPastDue: Bool
    let tags: String
}

// 在连接查询中使用
@FetchAll(
    Reminder
        .join(RemindersList.all) { $0.remindersListID.eq($1.id) }
        .select {
            ReminderWithList.Columns(
                reminder: $0,
                remindersList: $1,
                isPastDue: $0.isPastDue,
                tags: ""  // 其他地方计算
            )
        }
)
var reminders: [ReminderWithList]
核心要点:
@Selection
生成
.Columns
类型,用于
.select { }
闭包中,提供编译时验证确保查询结果与Swift类型匹配。

@Selection for Aggregate Queries

用于聚合查询的@Selection

swift
@Selection
struct Stats {
    var allCount = 0
    var flaggedCount = 0
    var scheduledCount = 0
    var todayCount = 0
}

// Single query returns all stats
@FetchOne(
    Reminder.select {
        Stats.Columns(
            allCount: $0.count(filter: !$0.isCompleted),
            flaggedCount: $0.count(filter: $0.isFlagged && !$0.isCompleted),
            scheduledCount: $0.count(filter: $0.isScheduled),
            todayCount: $0.count(filter: $0.isToday)
        )
    }
)
var stats = Stats()
swift
@Selection
struct Stats {
    var allCount = 0
    var flaggedCount = 0
    var scheduledCount = 0
    var todayCount = 0
}

// 单次查询返回所有统计数据
@FetchOne(
    Reminder.select {
        Stats.Columns(
            allCount: $0.count(filter: !$0.isCompleted),
            flaggedCount: $0.count(filter: $0.isFlagged && !$0.isCompleted),
            scheduledCount: $0.count(filter: $0.isScheduled),
            todayCount: $0.count(filter: $0.isToday)
        )
    }
)
var stats = Stats()

Creating Temporary Views

创建临时视图

For complex queries you'll reuse, create an actual SQLite view using
@Table @Selection
together:
swift
// 1. Define the view type with BOTH macros
@Table @Selection
private struct ReminderWithList {
    let reminderTitle: String
    let remindersListTitle: String
}

// 2. Create the temporary view
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)
}
Generated SQL:
sql
CREATE TEMPORARY VIEW "reminderWithLists"
("reminderTitle", "remindersListTitle")
AS
SELECT
  "reminders"."title",
  "remindersLists"."title"
FROM "reminders"
JOIN "remindersLists"
  ON "reminders"."remindersListID" = "remindersLists"."id"
对于需要复用的复杂查询,使用
@Table @Selection
一起创建实际的SQLite视图:
swift
// 1. 同时使用两个宏定义视图类型
@Table @Selection
private struct ReminderWithList {
    let reminderTitle: String
    let remindersListTitle: String
}

// 2. 创建临时视图
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)
}
生成的SQL:
sql
CREATE TEMPORARY VIEW "reminderWithLists"
("reminderTitle", "remindersListTitle")
AS
SELECT
  "reminders"."title",
  "remindersLists"."title"
FROM "reminders"
JOIN "remindersLists"
  ON "reminders"."remindersListID" = "remindersLists"."id"

Querying Views

查询视图

Once created, query the view like any table — the JOIN is hidden:
swift
// The join complexity is encapsulated in the view
let results = try ReminderWithList
    .order { ($0.remindersListTitle, $0.reminderTitle) }
    .limit(10)
    .fetchAll(db)
Generated SQL:
sql
SELECT "reminderWithLists"."reminderTitle",
       "reminderWithLists"."remindersListTitle"
FROM "reminderWithLists"
ORDER BY "reminderWithLists"."remindersListTitle",
         "reminderWithLists"."reminderTitle"
LIMIT 10
创建完成后,像查询表一样查询视图——连接逻辑被隐藏:
swift
// 连接复杂度被封装在视图中
let results = try ReminderWithList
    .order { ($0.remindersListTitle, $0.reminderTitle) }
    .limit(10)
    .fetchAll(db)
生成的SQL:
sql
SELECT "reminderWithLists"."reminderTitle",
       "reminderWithLists"."remindersListTitle"
FROM "reminderWithLists"
ORDER BY "reminderWithLists"."remindersListTitle",
         "reminderWithLists"."reminderTitle"
LIMIT 10

Updatable Views with INSTEAD OF Triggers

使用INSTEAD OF触发器的可更新视图

SQLite views are read-only by default. To enable INSERT/UPDATE/DELETE, use
INSTEAD OF
triggers that reroute operations to the underlying tables:
swift
// Enable inserts on the view
try database.write { db in
    try ReminderWithList.createTemporaryTrigger(
        insteadOf: .insert { new in
            // Reroute insert to actual tables
            Reminder.insert {
                ($0.title, $0.remindersListID)
            } values: {
                (
                    new.reminderTitle,
                    // Find existing list by title
                    RemindersList
                        .select(\.id)
                        .where { $0.title.eq(new.remindersListTitle) }
                )
            }
        }
    )
    .execute(db)
}

// Now you can insert into the view!
try ReminderWithList.insert {
    ReminderWithList(
        reminderTitle: "Morning sync",
        remindersListTitle: "Business"  // Must match existing list
    )
}
.execute(db)
Key concepts:
  • INSTEAD OF
    triggers intercept operations on the view
  • You define how to reroute to the real tables
  • The rerouting logic is application-specific (create new? find existing? fail?)
SQLite视图默认是只读的。要启用INSERT/UPDATE/DELETE,使用
INSTEAD OF
触发器将操作重定向到基础表:
swift
// 启用视图的插入操作
try database.write { db in
    try ReminderWithList.createTemporaryTrigger(
        insteadOf: .insert { new in
            // 将插入重定向到实际表
            Reminder.insert {
                ($0.title, $0.remindersListID)
            } values: {
                (
                    new.reminderTitle,
                    // 通过标题查找现有列表
                    RemindersList
                        .select(\.id)
                        .where { $0.title.eq(new.remindersListTitle) }
                )
            }
        }
    )
    .execute(db)
}

// 现在可以向视图插入数据了!
try ReminderWithList.insert {
    ReminderWithList(
        reminderTitle: "Morning sync",
        remindersListTitle: "Business"  // 必须匹配现有列表
    )
}
.execute(db)
核心概念:
  • INSTEAD OF
    触发器拦截对视图的操作
  • 你需要定义如何重定向到实际表
  • 重定向逻辑是应用特定的(创建新表?查找现有表?失败?)

When to Use Views vs @Selection

何时使用视图 vs @Selection

Use CaseApproach
One-off join query
@Selection
only
Reusable complex query
@Table @Selection
+
createTemporaryView
Need to insert/update via viewAdd
createTemporaryTrigger(insteadOf:)
Simple aggregates
@Selection
with
.select { }
Hide join complexity from callersTemporary view
使用场景方式
一次性连接查询仅使用
@Selection
可复用的复杂查询
@Table @Selection
+
createTemporaryView
需要通过视图插入/更新添加
createTemporaryTrigger(insteadOf:)
简单聚合结合
.select { }
使用
@Selection
向调用者隐藏连接复杂度临时视图

Temporary vs Permanent Views

临时视图 vs 永久视图

SQLiteData creates temporary views that exist only for the database connection lifetime:
swift
// Temporary view — gone when connection closes
ReminderWithList.createTemporaryView(as: ...)

// For permanent views, use raw SQL in migrations
migrator.registerMigration("Create view") { db in
    try #sql(
        """
        CREATE VIEW "reminderWithLists" AS
        SELECT r.title as reminderTitle, l.title as remindersListTitle
        FROM reminders r
        JOIN remindersLists l ON r.remindersListID = l.id
        """
    )
    .execute(db)
}
When to use permanent views:
  • Query is used across app restarts
  • View definition rarely changes
  • Performance benefit from persistent query plan
When to use temporary views:
  • Query varies by runtime conditions
  • Testing different view definitions
  • View needs to be dropped/recreated dynamically

SQLiteData创建临时视图,仅在数据库连接生命周期内存在:
swift
// 临时视图 — 连接关闭后消失
ReminderWithList.createTemporaryView(as: ...)

// 永久视图,在迁移中使用原始SQL
migrator.registerMigration("Create view") { db in
    try #sql(
        """
        CREATE VIEW "reminderWithLists" AS
        SELECT r.title as reminderTitle, l.title as remindersListTitle
        FROM reminders r
        JOIN remindersLists l ON r.remindersListID = l.id
        """
    )
    .execute(db)
}
何时使用永久视图:
  • 查询在应用重启后仍需使用
  • 视图定义很少更改
  • 持久查询计划带来性能提升
何时使用临时视图:
  • 查询随运行时条件变化
  • 测试不同的视图定义
  • 需要动态删除/重建视图

Custom Aggregate Functions

自定义聚合函数

SQLiteData lets you write complex aggregation logic in Swift using the
@DatabaseFunction
macro, then invoke it directly from SQL queries. This avoids contorted SQL subqueries for operations like mode, median, or custom statistics.
SQLiteData允许使用
@DatabaseFunction
宏在Swift中编写复杂的聚合逻辑,然后直接在SQL查询中调用。这避免了使用扭曲的SQL子查询来实现众数、中位数或自定义统计等操作。

Defining a Custom Aggregate

定义自定义聚合

swift
import StructuredQueries

// 1. Define the function with @DatabaseFunction macro
@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
}
Key points:
  • Takes
    some Sequence<T?>
    as input (receives all values from the grouped rows)
  • Returns the aggregated result
  • The macro generates a
    $mode
    function for use in queries
swift
import StructuredQueries

// 1. 使用@DatabaseFunction宏定义函数
@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
}
核心要点:
  • 接收
    some Sequence<T?>
    作为输入(接收分组行的所有值)
  • 返回聚合结果
  • 宏生成
    $mode
    函数用于查询

Registering the Function

注册函数

Add the function to your database configuration:
swift
func appDatabase() throws -> any DatabaseWriter {
    var configuration = Configuration()
    configuration.prepareDatabase { db in
        db.add(function: $mode)  // Register the $mode function
    }

    let database = try DatabaseQueue(configuration: configuration)
    // ... migrations
    return database
}
将函数添加到数据库配置:
swift
func appDatabase() throws -> any DatabaseWriter {
    var configuration = Configuration()
    configuration.prepareDatabase { db in
        db.add(function: $mode)  // 注册$mode函数
    }

    let database = try DatabaseQueue(configuration: configuration)
    // ... 迁移
    return database
}

Using in Queries

在查询中使用

Once registered, invoke with
$functionName(arg: $column)
:
swift
// Find the most common priority per reminders list
let results = try RemindersList
    .group(by: \.id)
    .leftJoin(Reminder.all) { $0.id.eq($1.remindersListID) }
    .select { ($0.title, $mode(priority: $1.priority)) }
    .fetchAll(db)
Without custom aggregate (raw SQL):
sql
-- This messy subquery is what @DatabaseFunction replaces
SELECT
  remindersLists.title,
  (
    SELECT reminders.priority
    FROM reminders
    WHERE reminders.remindersListID = remindersLists.id
      AND reminders.priority IS NOT NULL
    GROUP BY reminders.priority
    ORDER BY count(*) DESC
    LIMIT 1
  )
FROM remindersLists;
注册完成后,使用
$functionName(arg: $column)
调用:
swift
// 查找每个提醒列表中最常见的优先级
let results = try RemindersList
    .group(by: \.id)
    .leftJoin(Reminder.all) { $0.id.eq($1.remindersListID) }
    .select { ($0.title, $mode(priority: $1.priority)) }
    .fetchAll(db)
不使用自定义聚合的实现(原始SQL):
sql
-- @DatabaseFunction替代了这个繁琐的子查询
SELECT
  remindersLists.title,
  (
    SELECT reminders.priority
    FROM reminders
    WHERE reminders.remindersListID = remindersLists.id
      AND reminders.priority IS NOT NULL
    GROUP BY reminders.priority
    ORDER BY count(*) DESC
    LIMIT 1
  )
FROM remindersLists;

Common Use Cases

常见用例

AggregateDescription
ModeMost frequently occurring value
MedianMiddle value in sorted sequence
Weighted averageAverage with per-row weights
Custom filteringComplex conditional aggregation
String concatenationJoin strings with custom logic
聚合描述
众数出现频率最高的值
中位数排序序列中的中间值
加权平均值带每行权重的平均值
自定义过滤复杂条件聚合
字符串连接使用自定义逻辑连接字符串

Example: Median Function

示例:中位数函数

swift
@DatabaseFunction
func median(values: some Sequence<Double?>) -> Double? {
    let sorted = values.compactMap { $0 }.sorted()
    guard !sorted.isEmpty else { return nil }

    let mid = sorted.count / 2
    if sorted.count.isMultiple(of: 2) {
        return (sorted[mid - 1] + sorted[mid]) / 2
    } else {
        return sorted[mid]
    }
}

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

// Use
let medianPrices = try Product
    .group(by: \.categoryID)
    .select { ($0.categoryID, $median(values: $0.price)) }
    .fetchAll(db)
swift
@DatabaseFunction
func median(values: some Sequence<Double?>) -> Double? {
    let sorted = values.compactMap { $0 }.sorted()
    guard !sorted.isEmpty else { return nil }

    let mid = sorted.count / 2
    if sorted.count.isMultiple(of: 2) {
        return (sorted[mid - 1] + sorted[mid]) / 2
    } else {
        return sorted[mid]
    }
}

// 注册
configuration.prepareDatabase { db in
    db.add(function: $median)
}

// 使用
let medianPrices = try Product
    .group(by: \.categoryID)
    .select { ($0.categoryID, $median(values: $0.price)) }
    .fetchAll(db)

Performance Considerations

性能注意事项

  • Swift execution: The function runs in Swift, not SQLite's C engine
  • Row iteration: All grouped values are passed to your function
  • Memory: Large groups load all values into memory
  • Use sparingly: Best for complex logic that's awkward in SQL; use built-in aggregates (
    count
    ,
    sum
    ,
    avg
    ,
    min
    ,
    max
    ) when possible

  • Swift执行: 函数在Swift中运行,而非SQLite的C引擎
  • 行迭代: 所有分组值都会传递到你的函数
  • 内存: 大分组会将所有值加载到内存中
  • 谨慎使用: 最适合SQL中难以实现的复杂逻辑;可能的话使用内置聚合函数(
    count
    sum
    avg
    min
    max

Batch Upsert Performance

批量Upsert性能

For high-volume sync (50K+ records), the type-safe upsert API may be too slow. Use raw SQL with cached statements for maximum throughput.
对于高容量同步(5万+记录),类型安全的upsert API可能太慢。使用带缓存语句的原始SQL以获得最大吞吐量。

Cached Statement Upsert

缓存语句Upsert

swift
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
        ])
    }
}
Why this is faster:
  • Statement compiled once, reused for all rows
  • No Swift type-checking overhead per row
  • cachedStatement
    reuses prepared statements across calls
swift
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
        ])
    }
}
为什么更快:
  • 语句仅编译一次,所有行复用
  • 每行无Swift类型检查开销
  • cachedStatement
    在多次调用间复用预编译语句

Multi-Row Batch Upsert

多行批量Upsert

Reduce statement count further with multi-row VALUES:
swift
import SQLite3  // Required for sqlite3_limit

func batchUpsert(_ items: [Item], in db: Database) throws {
    guard !items.isEmpty else { return }

    // Query SQLite variable limit at runtime (requires import SQLite3)
    let maxVars = Int(sqlite3_limit(db.sqliteConnection, SQLITE_LIMIT_VARIABLE_NUMBER, -1))
    let columnsPerRow = 5  // id, name, libraryID, remoteID, updatedAt
    let maxRowsPerBatch = max(1, maxVars / columnsPerRow)

    for batchStart in stride(from: 0, to: items.count, by: maxRowsPerBatch) {
        let batchEnd = min(batchStart + maxRowsPerBatch, items.count)
        let batch = Array(items[batchStart..<batchEnd])

        // Build multi-row VALUES clause
        let placeholders = Array(repeating: "(?, ?, ?, ?, ?)", count: batch.count)
            .joined(separator: ", ")

        let sql = """
            INSERT INTO items (id, name, libraryID, remoteID, updatedAt)
            VALUES \(placeholders)
            ON CONFLICT(libraryID, remoteID) DO UPDATE SET
                name = excluded.name,
                updatedAt = excluded.updatedAt
            WHERE excluded.updatedAt >= items.updatedAt
            """

        var arguments: [DatabaseValueConvertible?] = []
        for item in batch {
            arguments.append(contentsOf: [
                item.id, item.name, item.libraryID,
                item.remoteID, item.updatedAt
            ] as [DatabaseValueConvertible?])
        }

        try db.execute(sql: sql, arguments: StatementArguments(arguments))
    }
}
SQLite variable limits:
  • iOS 14+: 32,766 variables (SQLite 3.32+)
  • iOS 13 and earlier: 999 variables
  • Query at runtime:
    sqlite3_limit(db.sqliteConnection, SQLITE_LIMIT_VARIABLE_NUMBER, -1)
通过多行VALUES进一步减少语句数量:
swift
import SQLite3  // 需要导入以使用sqlite3_limit

func batchUpsert(_ items: [Item], in db: Database) throws {
    guard !items.isEmpty else { return }

    // 运行时查询SQLite变量限制(需要导入SQLite3)
    let maxVars = Int(sqlite3_limit(db.sqliteConnection, SQLITE_LIMIT_VARIABLE_NUMBER, -1))
    let columnsPerRow = 5  // id, name, libraryID, remoteID, updatedAt
    let maxRowsPerBatch = max(1, maxVars / columnsPerRow)

    for batchStart in stride(from: 0, to: items.count, by: maxRowsPerBatch) {
        let batchEnd = min(batchStart + maxRowsPerBatch, items.count)
        let batch = Array(items[batchStart..<batchEnd])

        // 构建多行VALUES子句
        let placeholders = Array(repeating: "(?, ?, ?, ?, ?)", count: batch.count)
            .joined(separator: ", ")

        let sql = """
            INSERT INTO items (id, name, libraryID, remoteID, updatedAt)
            VALUES \(placeholders)
            ON CONFLICT(libraryID, remoteID) DO UPDATE SET
                name = excluded.name,
                updatedAt = excluded.updatedAt
            WHERE excluded.updatedAt >= items.updatedAt
            """

        var arguments: [DatabaseValueConvertible?] = []
        for item in batch {
            arguments.append(contentsOf: [
                item.id, item.name, item.libraryID,
                item.remoteID, item.updatedAt
            ] as [DatabaseValueConvertible?])
        }

        try db.execute(sql: sql, arguments: StatementArguments(arguments))
    }
}
SQLite变量限制:
  • iOS 14+:32766个变量(SQLite 3.32+)
  • iOS 13及更早:999个变量
  • 运行时查询:
    sqlite3_limit(db.sqliteConnection, SQLITE_LIMIT_VARIABLE_NUMBER, -1)

When to Use Each Pattern

何时使用每种模式

PatternUse CaseThroughput
Type-safe upsertSmall batches, type safety priority~1K rows/sec
Cached statementMedium batches (1K-10K rows)~10K rows/sec
Multi-row VALUESLarge batches (10K+ rows)~50K rows/sec
Note: Throughput varies by device, row size, and index count. Profile your workload.
Trade-offs:
  • Type-safe: Best DX, compile-time checks, slowest
  • Cached statement: Good balance, manual column maintenance
  • Multi-row: Fastest, most complex, requires variable limit handling

模式用例吞吐量
类型安全upsert小批量,优先考虑类型安全~1000行/秒
缓存语句中批量(1000-10000行)~10000行/秒
多行VALUES大批量(10000+行)~50000行/秒
注意: 吞吐量因设备、行大小和索引数量而异。请分析你的工作负载。
权衡:
  • 类型安全:最佳开发体验,编译时检查,速度最慢
  • 缓存语句:平衡良好,需手动维护列
  • 多行:速度最快,最复杂,需处理变量限制

Miscellaneous Advanced Patterns

其他高级模式

Database Triggers

数据库触发器

swift
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)
}
swift
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)
}

Custom Update Logic

自定义更新逻辑

swift
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)
swift
extension Updates<Reminder> {
    mutating func toggleStatus() {
        self.status = Case(self.status)
            .when(#bind(.incomplete), then: #bind(.completing))
            .else(#bind(.incomplete))
    }
}

// 使用
try Reminder.find(reminder.id).update { $0.toggleStatus() }.execute(db)

Enum Support

枚举支持

swift
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
}
swift
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
}

Compound Selects (UNION, INTERSECT, EXCEPT)

复合选择(UNION、INTERSECT、EXCEPT)

Combine multiple queries into a single result set:
swift
// UNION — combine results, remove duplicates
let allContacts = try Customer.select(\.email)
    .union(Supplier.select(\.email))
    .fetchAll(db)

// UNION ALL — combine results, keep duplicates
let allEmails = try Customer.select(\.email)
    .union(all: true, Supplier.select(\.email))
    .fetchAll(db)

// INTERSECT — only rows in both queries
let sharedEmails = try Customer.select(\.email)
    .intersect(Supplier.select(\.email))
    .fetchAll(db)

// EXCEPT — rows in first but not second
let customerOnlyEmails = try Customer.select(\.email)
    .except(Supplier.select(\.email))
    .fetchAll(db)
Use cases:
  • Combine data from multiple tables with same structure
  • Find common or unique values across tables
  • Build "all activity" feeds from different event types

将多个查询组合为单个结果集:
swift
// UNION — 组合结果,去重
let allContacts = try Customer.select(\.email)
    .union(Supplier.select(\.email))
    .fetchAll(db)

// UNION ALL — 组合结果,保留重复
let allEmails = try Customer.select(\.email)
    .union(all: true, Supplier.select(\.email))
    .fetchAll(db)

// INTERSECT — 仅返回同时存在于两个查询的行
let sharedEmails = try Customer.select(\.email)
    .intersect(Supplier.select(\.email))
    .fetchAll(db)

// EXCEPT — 返回第一个查询有但第二个查询没有的行
let customerOnlyEmails = try Customer.select(\.email)
    .except(Supplier.select(\.email))
    .fetchAll(db)
用例:
  • 组合结构相同的多个表的数据
  • 查找表之间的公共或唯一值
  • 从不同事件类型构建“所有活动”信息流

Resources

资源

GitHub: pointfreeco/sqlite-data, pointfreeco/swift-structured-queries, groue/GRDB.swift
Skills: axiom-sqlitedata, axiom-sqlitedata-migration, axiom-database-migration, axiom-grdb

Targets: iOS 17+, Swift 6 Framework: SQLiteData 1.4+ History: See git log for changes
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了解变更