Datastorekit
A SwiftData custom data store that uses SQLite as its primary persistence layer.
Install / Use
/learn @asymbas/DatastorekitREADME
DataStoreKit
A SwiftData custom data store implementation that supports SQLite as its primary persistence layer.
This package is in active development.
Table of Contents
- Features
- Installation
- Quick Start
- Usage
- Preview
- Documentation
- Compatibility
- Limitations
- Known Issues
- Roadmap
- FAQ
- Changelog
- License
Features
- SwiftData integration.
- Provides
DatabaseStoreas SwiftData's SQLite storage backend, configured throughDatabaseConfigurationinModelContainer. - View all supported predicate expressions in
/Sources/DataStoreRuntime/SQLQuery/PredicateExpressions+SQLPredicateExpression.swift. - Extended SwiftData features and conveniences:
- Use
#Predicateto query attributes that are Swift collection types, such asDictionary,Set, andArray. - Automatic persistence handling for custom value types that conform to
RawRepresentableandOptionSet.- Conforming types will be stored as raw values.
- Allows you to use typed cases and constants in
#Predicate(you are still required to capture their value as expected by the macro). - Provide a URL to specify a custom directory for external storage.
- Use
- Caches references, snapshots, and queries:
- References between entities are managed by the
ReferenceGraphto reduce fetching overhead from the database. - Snapshots for the model's backing data are cached by one or more associated
ModelContextinstances. - Queries cache and rebuild results based on matching hash keys.
- Implicit prefetching for relationships (e.g., preferring to include already cached snapshots in the fetch result).
- References between entities are managed by the
- Allows SwiftData and custom fetch/save request/result types:
PreloadFetchRequestwarms up an upcoming fetch by offloading the work asynchronously.ModelContext.preload(_:for:)is used to manually fetch and process the result ahead of time on a background actor using theasync/awaitsyntax. Follow up by switching to the desired actor to fetch the prepared results.ModelContext.preloadedFetch(_:isolation:)is an instance method that conveniently wraps the actor switching for you using the@concurrentattribute.@Fetchis a new property wrapper that builds on preloaded fetching and behaves similarly to SwiftData’s built-in@Query, but moves the expensive work onto a background actor. The@MainActorthen only applies the prepared results, which significantly reduces UI stutters on large databases.
- Persistent history tracking:
- History is stored inline for the current year.
- Supports archiving older history into external databases.
- Archived history can be attached separately when fetching history.
- Combine ORM and SQL workflow/patterns.
- Represent your models as snapshots or rows (array or dictionary).
- Continue using your SwiftData
PersistentModeltypes as observable reference models withModelContext. - Use
DatabaseSnapshotas a DTO or as a value-type representation of your model. This object conforms toCodableandSendable.
- Continue using your SwiftData
- Create a snapshot from a model using
DatabaseSnapshot(_:). - Create a model from a snapshot using
PersistentModel(snapshot:modelContext:).
- Represent your models as snapshots or rows (array or dictionary).
- Use
[any Sendable]and[String: any Sendable]when fetching row data. - Provides access to
DatabaseStoreto manually make requests for fetching and saving. - Provides shared resource access to
DatabaseQueue, where you can lease a noncopyableDatabaseConnectioninstance.
Installation
Swift Package Manager
Add to a Swift package in Package.swift:
dependencies: [
.package(url: "https://github.com/asymbas/datastorekit.git", from: "0.0.1")
],
targets: [
.target(
name: "Target",
dependencies: [.product(name: "DataStoreKit", package: "datastorekit")]
)
]
Quick Start
The SwiftData experience is preserved, and in many cases adopting DataStoreKit can be as simple as replacing ModelConfiguration with DatabaseConfiguration in an existing schema setup.
import DataStoreKit
let schema = Schema(versionedSchema: DatabaseSchema.self)
let configuration = DatabaseConfiguration(name: "custom", schema: schema)
let modelContainer = try ModelContainer(for: schema, configurations: configuration)
Usage
Request a noncopyable DatabaseConnection from the DatabaseQueue.
let rows = try store.queue.withConnection { connection in
try connection.fetch("SELECT * FROM Entity")
}
Specify a connection type explicitly, or use the convenience methods.
By default, withConnection(_:for:_:) uses nil for the connection type, which prefers a reader and may fall back to a writer if no reader is available.
try store.queue.withConnection(nil) { connection in ... }
try store.queue.withConnection(.reader) { connection in ... }
try store.queue.withConnection(.writer) { connection in ... }
try store.queue.reader { connection in ... }
try store.queue.writer { connection in ... }
You can also work with conventional SQL-style rows by fetching them as Swift collections, such as [any Sendable] or [String: any Sendable].
See the documentation for details on mapping row data back to SwiftData models using the schema.
Preview
Explore the Editor repository. It is a companion Xcode project used to develop and demonstrate DataStoreKit.
It showcases SwiftData and DataStoreKit features and is intended to become a dedicated tool for the library as development continues.
Documentation
The documentation is currently being revised and is hosted separately from this repository.
Read the latest version here: DataStoreKit Documentation
For questions, feedback, or suggestions, please use GitHub Discussions.
Compatibility
- OS 26.1, OS 26.2, and OS 26.3 have an issue with
Schema, where Swift collections can be unintentionally defined as transformable attributes when their elements contain simple types. This causes ModelCoders to incorrectly handle the data, resulting in a fatal error.- A workaround fix has been applied to how snapshots are encoded/decoded.
- Apple responded to the report and mentioned that this should be resolved in OS 26.3.
- Update: This has not been resolved.
Limitations
APIs are not ready for mutating the database
Using DatabaseQueue and DatabaseConnection to mutate the database rather than saving changes with ModelContext can result in the following:
- Inconsistent persistent history tracking.
- Stale references for to-many or many-to-many relationships.
- Unhandled external storage.
In order to save changes manually while ensuring completeness, you can use the same method SwiftData calls when it makes a save request by supplying it with a DatabaseSaveChanges type. You must correctly assign which snapshots to insert, update, or delete. This should also include any affected relationships, which may need to be fetched beforehand.
Known Issues
- Required one-to-one relationships can form dependency cycles<br>
Cyclic non-optional to-one relationships are currently not supported during a single insert pass. Newly inserted models that reference each other through a bidirectional non-optional one-to-one relationship can fail to save in the same operation, because DataStoreKit resolves required to-one dependencies before inserting a snapshot. A required to-one dependency blocks insertion when its related identifier is still uncommitted. When both sides require the other side to already exist, no valid insertion order can be established. As a result, both inserts may be repeatedly deferred until the save operation reaches its maximum retry count.<br>
- Workaround: Make one side optional during insertion.
- Tombstones cannot be instantiated<br>
SwiftData does not provide a way to create
HistoryTombstonefor preserved values.<br>- Workaround: Use the subscript on the
DatabaseHistoryDeleteinstance rather than itstombstoneproperty.
- Workaround: Use the subscript on the
- Generic or protocol-constrained key paths can't be matched to schema metadata<br> When a model is accessed through a protocol or generic constraint rather than its concrete type, the key path identity changes enough that the key path dictionary lookup misses. The parse-based fallback helps in some cases, but isn't reliable across all shapes.
- Key paths that traverse an optional value cannot be resolved<br>
AnyKeyPath.appending(path:)returnsnilwhen the left-hand side produces an optional value type and the right-hand side expects the unwrapped type. It is currently unknown how to dynamically append through an optional boundary. Any predicate or sort descriptor that chains through an optional intermediate cannot be reconstructed into a key path for SQL generation. SortDescriptoron a relationship's attribute requires a predicate referencing that relationship<br> Sort descriptors that traverse a relationship path, such as\Model.relationship.name, require a predicate that also references the relationship. DataStoreKit derives relationship traversal information for SQL generation from#Predicate. Without a predicate touching that relationship, noJOINis generated, and the sort clause references a table that isn't in theFROMclause. The sort is silently omitted.SchemaMigrationPlancannot be officially supported<br>ModelContainerdoes not allow aDataStoreConfigurationto be provided with aSchemaMigrationPlan.DataStore.init(_:migrationPlan:)still exposes a migration plan parameter, but this cannot be passed throughModelContainer.
Roadmap
Expect significant changes to th
Related Skills
oracle
341.2kBest practices for using the oracle CLI (prompt + file bundling, engines, sessions, and file attachment patterns).
prose
341.2kOpenProse VM skill pack. Activate on any `prose` command, .prose files, or OpenProse mentions; orchestrates multi-agent workflows.
Command Development
84.5kThis skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
Plugin Structure
84.5kThis skill should be used when the user asks to "create a plugin", "scaffold a plugin", "understand plugin structure", "organize plugin components", "set up plugin.json", "use ${CLAUDE_PLUGIN_ROOT}", "add commands/agents/skills/hooks", "configure auto-discovery", or needs guidance on plugin directory layout, manifest configuration, component organization, file naming conventions, or Claude Code plugin architecture best practices.
