Token导航 LogoToken导航TokenDH.com
研究检索需要联网github未标认证来源可访问许可证需确认审计提醒

sqlitedata-usage-guidesqlite 数据使用指南

Agent Skill

用于辅助数据库表结构、查询语句、迁移脚本和数据维护任务。它适合让 Agent 分析 schema、编写 SQL、排查查询问题、整理索引或生成迁移建议。使用时需要明确数据库类型、连接环境和目标表,区分只读分析与写入变更;涉及删除、更新、迁移和批量导入时,应优先 dry-run、备份或事务保护,避免误操作。

总安装

13,439

周安装

349

GitHub Stars

公开资料未说明

下载量

4,087
CodexClaudeCursorGemini CLI

安装说明

本站只整理中文说明和来源信息,不托管安装包,也不代用户安装。

GitHub

来源数

2

许可证

unknown

最后核验

2026-05-01

来源状态

来源可访问

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:sqlitedata-usage-guide(sqlite 数据使用指南)
来源仓库:https://github.com/bitxeno/sqlite-data-skill
仓库路径:skills/sqlitedata-usage-guide
安装命令:
npx skills add https://github.com/bitxeno/sqlite-data-skill --skill 'SQLiteData Usage Guide'
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。该命令会通过 npx skills 从第三方来源获取 Skill;本站只展示命令,不托管安装包,也不自动执行。

skills.shnpx skills
npx skills add https://github.com/bitxeno/sqlite-data-skill --skill 'SQLiteData Usage Guide'

简介

提供 SQLite 数据使用的详细操作指南。

  • 适合在初始化项目或学习数据库用法时参考。
  • 包含常见操作示例和最佳实践说明。适用宿主包括 Codex、Claude、Cursor、Gemini CLI,接入前应确认版本、权限和运行环境要求。
  • 不涉及实际数据操作,无需担心权限问题。
  • sqlitedata-usage-guide 属于研究检索类 Skill,可作为该场景下的辅助能力补充。

SKILL.md

SQLiteData Usage Guide

SQLiteData is a fast, lightweight replacement for SwiftData from Point-Free, powered by SQL and supporting CloudKit synchronization (and even CloudKit sharing). It is built on top of GRDB and StructuredQueries.

Key Dependencies

  • GRDB: The underlying SQLite interface library. Used for database connections, transactions, migrations, and observation.
  • StructuredQueries: Provides the @Table macro and type-safe query building APIs.
  • Swift Dependencies: Used for dependency injection (@Dependency, prepareDependencies).

Table of Contents

  1. Defining Your Schema
  2. Preparing the Database
  3. Fetching Data
  4. Observing Changes
  5. Dynamic Queries
  6. CRUD Operations
  7. Associations & Joins
  8. CloudKit Synchronization
  9. Testing & Previews
  10. Complete Examples

1. Defining Your Schema

Use the @Table macro from StructuredQueries to define your data types. Unlike SwiftData's @Model (which requires classes), @Table works with structs.

Basic Table Definition

import SQLiteData

@Table
struct Item: Identifiable {
  let id: Int            // Primary key (auto-generated for Int)
  var title = ""
  var isInStock = true
  var notes = ""
}

UUID Primary Key

@Table
struct RemindersList: Identifiable {
  let id: UUID
  var title = ""
  var position = 0
}

Custom Column Mapping

@Table
struct RemindersList: Hashable, Identifiable {
  let id: UUID
  @Column(as: Color.HexRepresentation.self)
  var color: Color = Self.defaultColor
  var position = 0
  var title = ""
}

Custom Primary Key

@Table
struct Tag: Hashable, Identifiable {
  @Column(primaryKey: true)
  var title: String
  var id: String { title }
}

Enums in Tables

Enums must conform to QueryBindable:

@Table
struct Reminder: Identifiable {
  let id: UUID
  var priority: Priority?
  var status: Status = .incomplete

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

Computed Query Expressions

You can define computed query expressions on your table's TableColumns:

nonisolated extension Reminder.TableColumns {
  var isCompleted: some QueryExpression<Bool> {
    status.neq(Reminder.Status.incomplete)
  }
  var isPastDue: some QueryExpression<Bool> {
    @Dependency(\.date.now) var now
    return !isCompleted && #sql("coalesce(date(\(dueDate)) < date(\(now)), 0)")
  }
  var isToday: some QueryExpression<Bool> {
    @Dependency(\.date.now) var now
    return !isCompleted && #sql("coalesce(date(\(dueDate)) = date(\(now)), 0)")
  }
  var isScheduled: some QueryExpression<Bool> {
    !isCompleted && dueDate.isNot(nil)
  }
}

Pre-defined Query Shortcuts

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

@Selection Macro for Custom Result Types

Use @Selection for custom types that hold the results of joins or partial selects:

@Selection
struct ReminderListState: Identifiable {
  var id: RemindersList.ID { remindersList.id }
  var remindersCount: Int
  var remindersList: RemindersList
  @Column(as: CKShare?.SystemFieldsRepresentation.self)
  var share: CKShare?
}

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

Junction Tables (Many-to-Many)

@Table("remindersTags")
struct ReminderTag: Identifiable {
  let id: UUID
  let reminderID: Reminder.ID
  let tagID: Tag.ID
}

Full-Text Search (FTS5)

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

2. Preparing the Database

Step 1: Create appDatabase() Function

import OSLog
import SQLiteData

func appDatabase() throws -> any DatabaseWriter {
  @Dependency(\.context) var context
  var configuration = Configuration()

  // Optional: Enable query tracing for debugging
  #if DEBUG
    configuration.prepareDatabase { db in
      db.trace(options: .profile) {
        if context == .preview {
          print("\($0.expandedDescription)")
        } else {
          logger.debug("\($0.expandedDescription)")
        }
      }
    }
  #endif

  // Create database (auto-provisions unique temp DBs for previews/tests)
  let database = try defaultDatabase(configuration: configuration)
  logger.info("open '\(database.path)'")

  // Migrate
  var migrator = DatabaseMigrator()
  #if DEBUG
    migrator.eraseDatabaseOnSchemaChange = true
  #endif

  migrator.registerMigration("Create tables") { db in
    try #sql("""
      CREATE TABLE "items" (
        "id" INTEGER PRIMARY KEY AUTOINCREMENT,
        "title" TEXT NOT NULL DEFAULT '',
        "isInStock" INTEGER NOT NULL DEFAULT 1,
        "notes" TEXT NOT NULL DEFAULT ''
      ) STRICT
      """)
      .execute(db)
  }

  // Register more migrations as your app evolves
  migrator.registerMigration("Add 'description' column") { db in
    try #sql("""
      ALTER TABLE "items"
      ADD COLUMN "description" TEXT
      """)
      .execute(db)
  }

  try migrator.migrate(database)
  return database
}

private let logger = Logger(subsystem: "MyApp", category: "Database")

Step 2: Set Default Database in App Entry Point

SwiftUI:

import SQLiteData
import SwiftUI

@main
struct MyApp: App {
  init() {
    prepareDependencies {
      $0.defaultDatabase = try! appDatabase()
    }
  }

  var body: some Scene {
    WindowGroup {
      ContentView()
    }
  }
}

UIKit AppDelegate:

class AppDelegate: NSObject, UIApplicationDelegate {
  func applicationDidFinishLaunching(_ application: UIApplication) {
    prepareDependencies {
      $0.defaultDatabase = try! appDatabase()
    }
  }
}

Bootstrap Pattern (Recommended for Multiple Dependencies)

Group database + sync engine setup:

extension DependencyValues {
  mutating func bootstrapDatabase() throws {
    defaultDatabase = try appDatabase()
    defaultSyncEngine = try SyncEngine(
      for: defaultDatabase,
      tables: RemindersList.self, Reminder.self, Tag.self, ReminderTag.self
    )
  }
}

// In App entry point:
@main
struct MyApp: App {
  init() {
    try! prepareDependencies {
      try $0.bootstrapDatabase()
    }
  }
  // ...
}

3. Fetching Data

@FetchAll — Fetch a Collection

Fetch all rows with default ordering:

@FetchAll var items: [Item]

With ordering:

@FetchAll(Item.order(by: \.title))
var items

With ordering by multiple columns:

@FetchAll(Item.order(by: \.isInStock, \.title))
var items

With filtering:

@FetchAll(Reminder.where(\.isCompleted).order { $0.title.desc() })
var completedReminders

With ordering by multiple columns using a closure:

@FetchAll(
  Item.order {
    ($0.isInStock,
     $0.title)
  }
)
var items

With mixed sort directions across multiple columns:

@FetchAll(
  Item.order {
    ($0.isInStock,
     $0.title.desc())
  }
)
var items

With animation:

@FetchAll(Item.order { $0.id.desc() }, animation: .default)
private var items

Using SQL string (#sql macro):

@FetchAll(#sql("SELECT * FROM reminders WHERE isCompleted ORDER BY title DESC"))
var completedReminders: [Reminder]

Using schema-safe SQL interpolation:

@FetchAll(
  #sql("""
    SELECT \(Reminder.columns)
    FROM \(Reminder.self)
    WHERE \(Reminder.isCompleted)
    ORDER BY \(Reminder.title) DESC
    """)
)
var completedReminders: [Reminder]

@FetchOne — Fetch a Single Value

Count:

@FetchOne(Reminder.count())
var remindersCount = 0

Filtered count:

@FetchOne(Reminder.where(\.isCompleted).count())
var completedRemindersCount = 0

Aggregate computation with @Selection:

@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()

@Fetch — Multiple Queries in One Transaction

Define a FetchKeyRequest:

struct Facts: FetchKeyRequest {
  struct Value {
    var facts: [Fact] = []
    var count = 0
  }
  func fetch(_ db: Database) throws -> Value {
    try Value(
      facts: Fact.order { $0.id.desc() }.fetchAll(db),
      count: Fact.fetchCount(db)
    )
  }
}

Use it:

@Fetch(Facts(), animation: .default)
private var facts = Facts.Value()

// Access:
facts.facts   // [Fact]
facts.count   // Int

Joins and Custom Selections

@FetchAll(
  RemindersList
    .group(by: \.id)
    .order(by: \.position)
    .leftJoin(Reminder.all) { $0.id.eq($1.remindersListID) && !$1.isCompleted }
    .leftJoin(SyncMetadata.all) { $0.syncMetadataID.eq($2.id) }
    .select {
      ReminderListState.Columns(
        remindersCount: $1.id.count(),
        remindersList: $0,
        share: $2.share
      )
    },
  animation: .default
)
var remindersLists

4. Observing Changes

SwiftUI Views

Property wrappers automatically observe database changes and re-render:

struct ItemsView: View {
  @FetchAll var items: [Item]

  var body: some View {
    ForEach(items) { item in
      Text(item.name)
    }
  }
}

@Observable Models

Important: Must annotate with @ObservationIgnored due to macro interactions; SQLiteData handles its own observation.
@Observable
@MainActor
class ItemsModel {
  @ObservationIgnored
  @FetchAll(Item.order { $0.id.desc() }, animation: .default)
  var items

  @ObservationIgnored
  @FetchOne(Item.count(), animation: .default)
  var itemsCount = 0

  @ObservationIgnored
  @Dependency(\.defaultDatabase) var database

  func deleteItem(indices: IndexSet) {
    withErrorReporting {
      try database.write { db in
        let ids = indices.map { items[$0].id }
        try Item
          .where { $0.id.in(ids) }
          .delete()
          .execute(db)
      }
    }
  }
}

UIKit View Controllers

Use $items.publisher (Combine) or observe (Swift Navigation):

class ItemsViewController: UICollectionViewController {
  @FetchAll(Fact.order { $0.id.desc() }, animation: .default)
  private var facts

  override func viewDidLoad() {
    super.viewDidLoad()
    // Using Swift Navigation's observe:
    observe { [weak self] in
      guard let self else { return }
      var snapshot = NSDiffableDataSourceSnapshot<Section, Fact>()
      snapshot.appendSections([.facts])
      snapshot.appendItems(facts, toSection: .facts)
      dataSource.apply(snapshot, animatingDifferences: true)
    }
  }
}

5. Dynamic Queries

Use $items.load(...) to dynamically change the query based on user input:

In a SwiftUI View

struct ContentView: View {
  @FetchAll var items: [Item]
  @State var filterDate: Date?
  @State var order: SortOrder = .reverse

  var body: some View {
    List { /* ... */ }
    .task(id: [filterDate, order] as [AnyHashable]) {
      await updateQuery()
    }
  }

  private func updateQuery() async {
    do {
      try await $items.load(
        Item
          .where { $0.timestamp > #bind(filterDate ?? .distantPast) }
          .order {
            if order == .forward {
              $0.timestamp
            } else {
              $0.timestamp.desc()
            }
          }
          .limit(10)
      )
    } catch { /* Handle error */ }
  }
}

Dynamic Query with @Fetch

@Fetch(Facts(), animation: .default) private var facts = Facts.Value()
@State var query = ""

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

private struct Facts: FetchKeyRequest {
  var query = ""
  struct Value {
    var facts: [Fact] = []
    var searchCount = 0
    var totalCount = 0
  }
  func fetch(_ db: Database) throws -> Value {
    let search = Fact
      .where { $0.body.contains(query) }
      .order { $0.id.desc() }
    return try Value(
      facts: search.fetchAll(db),
      searchCount: search.fetchCount(db),
      totalCount: Fact.fetchCount(db)
    )
  }
}
Important: If a parent view refreshes, a dynamically-updated query can be overwritten with the initial @FetchAll's value. Use @State @FetchAll to keep query state local to the view.

Pagination

Offset-based Pagination

Use .limit(_:offset:) to fetch a specific page of results:

let pageSize = 20
let page = 2  // 0-indexed

@FetchAll(
  Item
    .order { $0.title }
    .limit(pageSize, offset: page * pageSize)
)
var items

Dynamically load a page in response to user input:

struct ItemsView: View {
  @State @FetchAll var items: [Item]
  @State var page = 0
  let pageSize = 20

  var body: some View {
    List(items) { item in Text(item.title) }
    HStack {
      Button("Previous") { page = max(0, page - 1) }
      Button("Next") { page += 1 }
    }
    .task(id: page) {
      await withErrorReporting {
        try await $items.load(
          Item
            .order { $0.title }
            .limit(pageSize, offset: page * pageSize)
        )
      }
    }
  }
}

Cursor (Keyset) Pagination

Keyset pagination uses the last seen value as a cursor, which is more efficient and stable than offset pagination for large datasets:

// First page — no cursor
@FetchAll(
  Item
    .order { $0.id }
    .limit(20)
)
var items
// Next page — pass the last seen id as cursor
let lastID = items.last?.id

@FetchAll(
  Item
    .where { $0.id > #bind(lastID ?? 0) }
    .order { $0.id }
    .limit(20)
)
var nextItems

Dynamically load the next page inside a model:

@Observable
@MainActor
class ItemsModel {
  @ObservationIgnored
  @State @FetchAll var items: [Item]

  @Dependency(\.defaultDatabase) var database

  var lastID: Item.ID? = nil

  func loadNextPage() async {
    await withErrorReporting {
      try await $items.load(
        Item
          .where { $0.id > #bind(lastID ?? 0) }
          .order { $0.id }
          .limit(20)
      )
      lastID = items.last?.id
    }
  }
}

6. CRUD Operations

All write operations go through the defaultDatabase dependency.

Access Database

@Dependency(\.defaultDatabase) var database

Create (Insert)

try database.write { db in
  try Item.insert {
    Item(id: UUID(), title: "New Item", isInStock: true, notes: "")
  }
  .execute(db)
}

Using Draft (auto-generated type without primary key):

try database.write { db in
  try Fact.insert {
    Fact.Draft(body: "Some fact text")
  }
  .execute(db)
}

Insert with defaults (using table defaults):

try database.write { db in
  try Item.insert().execute(db)
}

Read (Fetch)

Use @FetchAll, @FetchOne, or @Fetch property wrappers (covered above).

For one-off reads within a write transaction:

try database.write { db in
  let count = try Reminder.fetchCount(db)
  let items = try Item.where(\.isInStock).fetchAll(db)
}

Update

Update an existing row:

existingItem.title = "New Title"
try database.write { db in
  try Item.update(existingItem).execute(db)
}

Conditional update with query:

try database.write { db in
  try Reminder
    .where { $0.status.eq(#bind(.completing)) }
    .update { $0.status = #bind(.completed) }
    .execute(db)
}

Delete

Delete by value:

try database.write { db in
  try Item.delete(existingItem).execute(db)
}

Delete by query:

try database.write { db in
  try Item
    .where { $0.id.in(idsToDelete) }
    .delete()
    .execute(db)
}

Delete with subquery:

try database.write { db in
  try Tag
    .where { $0.title.in(tagTitles) }
    .delete()
    .execute(db)
}

Batch Reordering

try database.write { db in
  var ids = items.map(\.id)
  ids.move(fromOffsets: source, toOffset: destination)
  try Item
    .where { $0.id.in(ids) }
    .update {
      let indexedIDs = Array(ids.enumerated())
      let (first, rest) = (indexedIDs.first!, indexedIDs.dropFirst())
      $0.position = rest
        .reduce(Case($0.id).when(first.element, then: first.offset)) { cases, id in
          cases.when(id.element, then: id.offset)
        }
        .else($0.position)
    }
    .execute(db)
}

7. Associations & Joins

SQLiteData does not use an ORM pattern. Instead, use SQL joins to efficiently fetch related data.

One-to-Many (Join + Group)

@FetchAll(
  Sport
    .group(by: \.id)
    .leftJoin(Team.all) { $0.id.eq($1.sportID) }
    .select {
      SportWithTeamCount.Columns(sport: $0, teamCount: $1.count())
    }
)
var sportsWithTeamCounts

Many-to-Many via Junction Table

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

// Use:
@FetchAll(
  Tag
    .order(by: \.title)
    .withReminders
    .having { $2.count().gt(0) }
    .select { tag, _, _ in tag },
  animation: .default
)
var tags

Three-Way Join

@FetchAll(
  RemindersList
    .group(by: \.id)
    .order(by: \.position)
    .leftJoin(Reminder.all) { $0.id.eq($1.remindersListID) && !$1.isCompleted }
    .leftJoin(SyncMetadata.all) { $0.syncMetadataID.eq($2.id) }
    .select {
      ReminderListState.Columns(
        remindersCount: $1.id.count(),
        remindersList: $0,
        share: $2.share
      )
    },
  animation: .default
)
var remindersLists

8. CloudKit Synchronization

Setup SyncEngine

@main
struct MyApp: App {
  init() {
    try! prepareDependencies {
      $0.defaultDatabase = try appDatabase()
      $0.defaultSyncEngine = try SyncEngine(
        for: $0.defaultDatabase,
        tables: RemindersList.self, Reminder.self, Tag.self
      )
    }
  }
}

Attach Metadatabase

In your database configuration, attach the metadata database required for sync:

configuration.prepareDatabase { db in
  try db.attachMetadatabase()
}

SyncEngine Delegate

@MainActor
@Observable
class MySyncEngineDelegate: SyncEngineDelegate {
  var isDeleteLocalDataAlertPresented = false
  func syncEngine(
    _ syncEngine: SQLiteData.SyncEngine,
    accountChanged changeType: CKSyncEngine.Event.AccountChange.ChangeType
  ) async {
    switch changeType {
    case .signIn: break
    case .signOut, .switchAccounts:
      isDeleteLocalDataAlertPresented = true
    @unknown default: break
    }
  }
}

Scene Delegate for Sharing

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
  @Dependency(\.defaultSyncEngine) var syncEngine

  func windowScene(
    _ windowScene: UIWindowScene,
    userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata
  ) {
    Task {
      try await syncEngine.acceptShare(metadata: cloudKitShareMetadata)
    }
  }
}

Manual Sync

@Dependency(\.defaultSyncEngine) var syncEngine

// Pull changes:
try await syncEngine.syncChanges()

// Delete local data:
try await syncEngine.deleteLocalData()

Table Definitions for CloudKit Sync

Use ON CONFLICT REPLACE DEFAULT for columns to handle sync conflicts:

try #sql("""
  CREATE TABLE "counters" (
    "id" TEXT PRIMARY KEY NOT NULL ON CONFLICT REPLACE DEFAULT (uuid()),
    "count" INT NOT NULL ON CONFLICT REPLACE DEFAULT 0
  ) STRICT
  """)
  .execute(db)

9. Testing & Previews

Xcode Previews

#Preview {
  let _ = prepareDependencies {
    $0.defaultDatabase = try! appDatabase()
  }
  NavigationStack {
    ContentView()
  }
}

With Seed Data

#if DEBUG
extension DatabaseWriter {
  func seedSampleData() throws {
    try write { db in
      try db.seed {
        Item(id: UUID(), title: "Sample 1", isInStock: true, notes: "")
        Item(id: UUID(), title: "Sample 2", isInStock: false, notes: "Note")
      }
    }
  }
}
#endif

#Preview {
  let _ = try! prepareDependencies {
    try $0.bootstrapDatabase()
    try? $0.defaultDatabase.seedSampleData()
  }
  NavigationStack {
    ContentView()
  }
}

In-Memory Database for Testing

extension DatabaseWriter where Self == DatabaseQueue {
  static var testDatabase: Self {
    let databaseQueue = try! DatabaseQueue()
    var migrator = DatabaseMigrator()
    migrator.registerMigration("Create tables") { db in
      try #sql("""
        CREATE TABLE "facts" (
          "id" INTEGER PRIMARY KEY AUTOINCREMENT,
          "body" TEXT NOT NULL
        ) STRICT
        """)
        .execute(db)
    }
    try! migrator.migrate(databaseQueue)
    return databaseQueue
  }
}

Unit Tests

@Test(.dependency(\.defaultDatabase, try appDatabase()))
func feature() {
  // ...
}

10. Complete Examples

Minimal SwiftUI App (SwiftData Template Replacement)

import SQLiteData
import SwiftUI

@main
struct MyApp: App {
  init() {
    prepareDependencies {
      $0.defaultDatabase = .appDatabase
    }
  }
  var body: some Scene {
    WindowGroup {
      ContentView()
    }
  }
}

struct ContentView: View {
  @FetchAll(Item.all, animation: .default) private var items
  @Dependency(\.defaultDatabase) private var database

  var body: some View {
    NavigationStack {
      List {
        ForEach(items) { item in
          Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
        }
        .onDelete(perform: deleteItems)
      }
      .toolbar {
        ToolbarItem(placement: .navigationBarTrailing) { EditButton() }
        ToolbarItem {
          Button(action: addItem) {
            Label("Add Item", systemImage: "plus")
          }
        }
      }
    }
  }

  private func addItem() {
    withErrorReporting {
      try database.write { db in
        try Item.insert().execute(db)
      }
    }
  }

  private func deleteItems(offsets: IndexSet) {
    withErrorReporting {
      try database.write { db in
        try Item.where { $0.id.in(offsets.map { items[$0].id }) }.delete().execute(db)
      }
    }
  }
}

@Table
nonisolated struct Item: Identifiable {
  let id: Int
  var timestamp: Date
}

extension DatabaseWriter where Self == DatabaseQueue {
  static var appDatabase: Self {
    let databaseQueue = try! DatabaseQueue()
    var migrator = DatabaseMigrator()
    migrator.registerMigration("Create items table") { db in
      try #sql("""
        CREATE TABLE "items" (
          "id" INTEGER PRIMARY KEY AUTOINCREMENT,
          "timestamp" TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
        ) STRICT
        """)
        .execute(db)
    }
    try! migrator.migrate(databaseQueue)
    return databaseQueue
  }
}

@Observable Model Pattern

@Observable
@MainActor
class FeatureModel {
  @ObservationIgnored
  @FetchAll(Item.order { $0.id.desc() }, animation: .default)
  var items

  @ObservationIgnored
  @FetchOne(Item.count(), animation: .default)
  var itemsCount = 0

  @ObservationIgnored
  @Dependency(\.defaultDatabase) var database

  func addItem(body: String) async {
    await withErrorReporting {
      try await database.write { db in
        try Item.insert { Item.Draft(body: body) }.execute(db)
      }
    }
  }

  func deleteItems(indices: IndexSet) {
    withErrorReporting {
      try database.write { db in
        let ids = indices.map { items[$0].id }
        try Item.where { $0.id.in(ids) }.delete().execute(db)
      }
    }
  }
}

UIKit View Controller Pattern

final class ItemsViewController: UICollectionViewController {
  @FetchAll(Item.order { $0.id.desc() }, animation: .default)
  private var items

  @Dependency(\.defaultDatabase) var database

  private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!

  override func viewDidLoad() {
    super.viewDidLoad()

    let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Item> {
      cell, indexPath, item in
      var configuration = cell.defaultContentConfiguration()
      configuration.text = item.title
      cell.contentConfiguration = configuration
    }

    dataSource = UICollectionViewDiffableDataSource<Section, Item>(
      collectionView: collectionView
    ) { collectionView, indexPath, item in
      collectionView.dequeueConfiguredReusableCell(
        using: cellRegistration, for: indexPath, item: item
      )
    }

    // Observe database changes and update UI
    observe { [weak self] in
      guard let self else { return }
      var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
      snapshot.appendSections([.items])
      snapshot.appendItems(items, toSection: .items)
      dataSource.apply(snapshot, animatingDifferences: true)
    }
  }

  enum Section: Hashable { case items }
}

Database Triggers Pattern

try database.write { db in
  // Auto-set position on insert
  try Item.createTemporaryTrigger(
    after: .insert { new in
      Item
        .find(new.id)
        .update { $0.position = Item.select { ($0.position.max() ?? -1) + 1 } }
    }
  )
  .execute(db)

  // Cascade FTS updates on insert
  try Reminder.createTemporaryTrigger(
    after: .insert { new in
      ReminderText.insert {
        ReminderText.Columns(
          rowid: new.rowid,
          title: new.title,
          notes: new.notes.replace("\\n", " "),
          tags: ""
        )
      }
    }
  )
  .execute(db)

  // Cascade FTS updates on column change
  try Reminder.createTemporaryTrigger(
    after: .update {
      ($0.title, $0.notes)
    } forEachRow: { _, new in
      ReminderText
        .where { $0.rowid.eq(new.rowid) }
        .update {
          $0.title = new.title
          $0.notes = new.notes.replace("\\n", " ")
        }
    }
  )
  .execute(db)

  // Cascade FTS delete
  try Reminder.createTemporaryTrigger(
    after: .delete { old in
      ReminderText
        .where { $0.rowid.eq(old.rowid) }
        .delete()
    }
  )
  .execute(db)
}

Quick Reference

SwiftDataSQLiteData
@Model class@Table struct
@Query var items: [Item]@FetchAll var items: [Item]
@Query(sort:)@FetchAll(Item.order(by:))
@Query(filter:)@FetchAll(Item.where(...))
N/A@FetchOne(Item.count())
N/A@Fetch(CustomRequest())
@Environment(\.modelContext)@Dependency(\.defaultDatabase)
modelContext.insert(item)try Item.insert {item}.execute(db)
try modelContext.save()(auto-saved within database.write)
modelContext.delete(item)try Item.delete(item).execute(db)
ModelContainer(...)prepareDependencies {$0.defaultDatabase =...}
iOS 17+ onlyiOS 13+ supported
Views only (@Query)Views, @Observable, UIKit, anywhere

Platform Support

SQLiteData supports: iOS 13+, macOS 10.15+, tvOS 13+, watchOS 6+ — much broader than SwiftData's iOS 17+ requirement.

References

适合场景

01

用户想查找某类 Agent Skill 时

02

需要根据任务场景推荐可安装能力包时

03

需要对比不同来源的安装命令和来源信息时

能力概览

能力 1

按任务关键词查找相关 Skills

能力 2

展示可复制的安装命令

能力 3

保留来源站点、仓库和原始说明,方便继续核验

能力 4

展示第三方安全扫描或审计结果

安装后应在对应宿主中按原始 README 的触发条件使用;具体调用方式请以来源页面和 README 为准。

平台分布

Codex

33.3%
按下载量换算1,361

Claude

32.68%
按下载量换算1,336

Cursor

19.89%
按下载量换算813

Gemini CLI

8.77%
按下载量换算358

安全审计

Gen Agent Trust Hub

通过

Socket

通过

Snyk

可疑

权限和风险

需要联网

该 Skill 可能需要联网访问来源站点、仓库或外部 API;具体网络访问范围需要结合源码和 README 复核。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。来源安全扫描存在 warning/failed 结果,不能写成本站确认安全。当前只有一个来源,正式发布前建议补源仓库或其他目录站核验。

来源信息

继续浏览同类 Skills