{"id":24528077,"url":"https://github.com/kazaimazai/swiftletmodel","last_synced_at":"2026-04-07T04:01:09.872Z","repository":{"id":226948347,"uuid":"770015964","full_name":"KazaiMazai/SwiftletModel","owner":"KazaiMazai","description":"Lightweight Core Data alternative ","archived":false,"fork":false,"pushed_at":"2026-04-07T02:03:55.000Z","size":504,"stargazers_count":73,"open_issues_count":1,"forks_count":2,"subscribers_count":2,"default_branch":"main","last_synced_at":"2026-04-07T03:26:15.834Z","etag":null,"topics":["codable","coredata","data","ddd","ddd-architecture","domain-driven-design","graph","graphql","in-memory","in-memory-database","model","swift","swiftdata","swiftui"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/KazaiMazai.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-03-10T17:32:13.000Z","updated_at":"2026-04-07T02:03:08.000Z","dependencies_parsed_at":"2024-09-11T03:30:00.713Z","dependency_job_id":"ac171847-70b4-4fca-a5ed-c4f53eeda54c","html_url":"https://github.com/KazaiMazai/SwiftletModel","commit_stats":null,"previous_names":["kazaimazai/swiftymodel","kazaimazai/swifletdata","kazaimazai/swiftletmodel"],"tags_count":37,"template":false,"template_full_name":null,"purl":"pkg:github/KazaiMazai/SwiftletModel","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KazaiMazai%2FSwiftletModel","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KazaiMazai%2FSwiftletModel/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KazaiMazai%2FSwiftletModel/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KazaiMazai%2FSwiftletModel/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/KazaiMazai","download_url":"https://codeload.github.com/KazaiMazai/SwiftletModel/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KazaiMazai%2FSwiftletModel/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31499193,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-07T03:10:19.677Z","status":"ssl_error","status_checked_at":"2026-04-07T03:10:13.982Z","response_time":105,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["codable","coredata","data","ddd","ddd-architecture","domain-driven-design","graph","graphql","in-memory","in-memory-database","model","swift","swiftdata","swiftui"],"created_at":"2025-01-22T06:33:52.696Z","updated_at":"2026-04-07T04:01:09.863Z","avatar_url":"https://github.com/KazaiMazai.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cpicture\u003e\n  \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"https://github.com/KazaiMazai/SwiftletModel/blob/main/Docs/Resources/Logo-dark.svg\"\u003e\n  \u003csource media=\"(prefers-color-scheme: light)\" srcset=\"https://github.com/KazaiMazai/SwiftletModel/blob/main/Docs/Resources/Logo.svg\"\u003e\n  \u003cimg src=\"https://github.com/KazaiMazai/SwiftletModel/blob/main/Docs/Resources/Logo.svg\"\u003e\n\u003c/picture\u003e\n\n[![CI](https://github.com/KazaiMazai/SwiftletModel/workflows/Tests/badge.svg)](https://github.com/KazaiMazai/SwiftletModel/actions?query=workflow%3ATests)\n[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FKazaiMazai%2FSwiftletModel%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/KazaiMazai/SwiftletModel)\n[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FKazaiMazai%2FSwiftletModel%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/KazaiMazai/SwiftletModel)\n\n## What is SwiftletModel?\n\n*If CoreData broke up with legacy, embraced modern Swift, and married GraphQL — you'd get SwiftletModel.*\n\n\u003e *SwiftletModel is what you wished SwiftData was — if it were reinvented from scratch.*  \nIt gives you CoreData-level graph management power with plain Swift structs, in-memory speed, and zero boilerplate.\n\n**Is it an ORM?** Not exactly. SwiftletModel isn’t a traditional ORM or database layer. It doesn’t abstract SQL or manage disk persistence.  \n\nInstead, it’s a type-safe, normalized, in-memory graph model engine — a place to merge, shape, and manage business entity data from multiple sources, effortlessly.\n\n## Features\n\n- **Entities as Plain Structs**: Define your entities using simple Swift structs.\n- **Bidirectional Relations**: Manage relationships between entities effortlessly with type safety.\n- **Normalized In-Memory Storage**: Store your data in a normalized form to maintain consistency and efficiency.\n- **On-the-Fly Denormalization**: Transform your data into any required shape instantly.\n- **Incomplete Data Handling**: Seamlessly handle scenarios involving partial or missing data.\n- **Indexing**: Sort and filter data efficiently, enforce unique constraints, and perform full-text search. B-tree, Unique, and Full-Text BM25-ranked indexes help boost performance.\n- **Codable Out of the Box**: Easily encode and decode your entities for persistence, response mapping, or other purposes.\n\n## Use Cases\n\nSwiftletModel excels in the following scenarios:\n\n- **Complex Domain Models**: Ideal for apps with intricate domain models featuring multiple interconnected entity types.\n- **Lightweight Local Storage**: Suitable when you want to avoid the development overhead of persistent storage solutions like CoreData, SwiftData, Realm, or SQLite.\n- **Backend-Centric Applications**: Perfect for applications where the backend is the primary source of truth and a fully fledged local database is not needed.\n- **Multiple Data Sources**: A true painkiller for apps that manage and merge data from multiple origins — backend APIs, local files, cloud services, HealthKit, Location, etc. — into a unified, type-safe in-memory graph.\n\n**Persistence is optional.**  \nAlthough primarily in-memory, SwiftletModel’s data models are plain Codable structs, allowing straightforward integration with any storage solution as a sidecar: flat files, CRDTs, GRDB, CoreData/SwiftData, SQLite, iCloudKit, Firebase, backend APIs, etc.\n\n\n---\n \n## 🚀 Quick Start\n\nHere's how to get started fast.\n\n\n\n### 1. Installation\n\nUsing [Swift Package Manager](https://swift.org/package-manager/):\n\n```swift\n.package(url: \"https://github.com/KazaiMazai/SwiftletModel.git\", from: \"0.0.1\")\n``` \n\nOr via Xcode:\n\n- File → Add Packages\n- Enter the URL:\n```\nhttps://github.com/KazaiMazai/SwiftletModel.git\n```\n\n### 2. Define a Model\nUse `@EntityModel` to define structs with rich relationships:\n\n```swift\n@EntityModel\nstruct Message {\n    let id: String\n    let text: String\n\n    @Relationship(.required) var author: User?\n    @Relationship(inverse: \\.messages) var chat: Chat?\n}\n```\nThat’s all. The macro handles conformance to `EntityModelProtocol`, merging, relation handling, and storage access.\n\n\n### 3. Create a Context\n\n```swift\nvar context = Context()\n```\nThis acts as your normalized, in-memory store and handles all entity relations.\n\n### 4. Save Entities\n\n```swift\nlet user = User(id: \"1\", name: \"Alice\")\nlet chat = Chat(id: \"1\", users: .relation([user]))\n\ntry chat.save(to: \u0026context)\n```\n\nOnly include full objects when needed. Else just use `.id(...)` to refer to existing entities.\n\n```swift\nlet message = Message(id: \"1\", text: \"Hello\", author: .id(\"1\"), chat: .id(\"1\"))\n```\n\n### 5. Query \u0026 Resolve\n\n```swift\nlet chats = Chat\n    .query()\n    .filter(\\.hasNewMessages == true)\n    .sorted(by: \\.updatedAt.desc)\n    .with(\\.$users)\n    .with(\\.$messages) {\n        $0.with(\\.$author)\n    }\n    .resolve(in: context)\n```\nThis pulls the chat and its users and messages from the context with proper denormalization.\n\n\n### 6. Handle Partial Data Safely\n\n```swift\nlet partialUser = User(id: \"1\", name: \"Bob\") // No avatar, no profile\ntry partialUser.save(to: \u0026context, options: .fragment)\n```\n\nFragment merge strategy means: update only the non-nil parts. Nothing gets wiped accidentally.\n\n\n### 7. Codable Ready\n\nAll entities can be marked as Codable. \nModels and Relations would serialize, making it trivial to persist, transmit or integrate.\n\n```swift\nextension User: Codable { }\n\nlet json = try? JSONEncoder().encode(user)\n```\n\nThat’s it. You now have a type-safe, bidirectionally-linked, normalized in-memory model graph.\n\n## 🧠 Ideas Behind SwiftletModel\n\n**SwiftletModel intentionally does not bundle persistence, observation, or reactive capabilities.**\n\nAdding any form of persistence under the model’s core would inevitably influence its design and expose implementation details, introducing unwanted side effects — something I deliberately avoided.\n\nInstead, the SwiftletModel core is crafted as a pure, synchronous in-memory graph with no side effects or asynchronous behavior.\n\n- **Context** is simply a plain dictionary — with a superpower.\n- **Queries** are instant.\n- **Models** are plain `structs`, `Codable` if necessary.\n- **State** is deterministic.\n- **Testing** is effortless.\n    \nThis minimalistic design makes SwiftletModel an ideal foundational block, allowing developers to integrate it seamlessly with anything:\n- Combine\n- ObservableObjects or Observation\n- SwiftUI plain states \n- Even complex architectures like TCA (The Composable Architecture)\n    \nEntities are plain `Codable` structs, easily composable with any backend, caching layer, sync mechanism, or persistent storage.\nThis approach is a clear embodiment of **Functional Core, Imperative Shell**.\n\n## Table of Contents\n\n- [Model Definitions](#model-definitions)\n- [How to Save Entities](#how-to-save-entities)\n- [How to Delete Entities](#how-to-delete-entities)\n  * [Relationship DeleteRule](#relationship-deleterule)\n- [How to Query Entities](#how-to-query-entities)\n  * [Query with nested models](#query-with-nested-models)\n  * [Bulk nested models query](#bulk-nested-models-query)\n  * [Combining bulk nested models with nested models query](#combining-bulk-nested-models-with-nested-models-query)\n  * [Related models query](#related-models-query)\n  * [Limiting and Paginating Results](#limiting-and-paginating-results)\n- [How to use Sort Queries](#how-to-use-sort-queries)\n  * [Basic Sorting](#basic-sorting)\n    + [Single Property Sorting](#single-property-sorting)\n    + [Multi-Property Sorting](#multi-property-sorting)\n  * [Using Indexes for Sorting](#using-indexes-for-sorting)\n    + [Single Property Index](#single-property-index)\n    + [Compound Index](#compound-index)\n    + [Combining Sort and Filter](#combining-sort-and-filter)\n    + [Best Practises and Performance Considerations](#best-practises-and-performance-considerations)\n- [How to use Filter Queries](#how-to-use-filter-queries)\n  * [Basic Filtering](#basic-filtering)\n    + [Equality Filters](#equality-filters)\n    + [Comparison Filters](#comparison-filters)\n  * [Complex Filters](#complex-filters)\n    + [Logical Operators](#logical-operators)\n  * [Text Filtering](#text-filtering)\n    + [String Operations](#string-operations)\n    + [Full-Text Search](#full-text-search)\n  * [Performance Optimization](#performance-optimization)\n    + [Index Usage](#index-usage)\n  * [Filters Best Practices](#filters-best-practices)\n  * [Filter Method Reference](#filter-method-reference)\n- [Codable Conformance](#codable-conformance)\n- [Relationship Types](#relationship-types)\n  * [Optional to-one Relationship](#optional-to-one-relationship)\n  * [Required to-one Relationship](#required-to-one-relationship)\n  * [To-many Relationship](#to-many-relationship)\n- [Establishing Relations](#establishing-relations)\n  * [Setting to-one relations](#setting-to-one-relations)\n  * [Setting to-many relations](#setting-to-many-relations)\n  * [Saving Relations](#saving-relations)\n  * [Removing Relations](#removing-relations)\n- [Incomplete Data Handling](#incomplete-data-handling)\n  * [Handling incomplete Entity Models](#handling-incomplete-entity-models)\n    + [Default Merge Strategy](#default-merge-strategy)\n    + [Fragment Merge Strategy](#fragment-merge-strategy)\n    + [Last Write Wins Merge Strategy](#last-write-wins-merge-strategy)\n    + [Advanced Merge Strategies](#advanced-merge-strategies)\n  * [Handling incomplete Related Entity Models](#handling-incomplete-related-entity-models)\n  * [Handling incomplete data for to-many Relations](#handling-incomplete-data-for-to-many-relations)\n  * [Handling missing data for to-one Relations](#handling-missing-data-for-to-one-relations)\n  * [Incomplete Data Handling Summary](#incomplete-data-handling-summary)\n- [Indexing](#indexing)\n  * [Index](#index)\n  * [Unique](#unique)\n  * [FullTextIndex](#fulltextindex)\n  * [Index Performance Considerations](#index-performance-considerations)\n- [Schema](#schema)\n  * [Schema Versioning](#schema-versioning)\n  * [Schema Bulk Queries](#schema-bulk-queries)\n  * [Metadata](#metadata)\n- [Type Safety](#type-safety)\n- [Documentation](#documentation)\n- [Licensing](#licensing)\n\n\n## Model Definitions\n\nWhen we define the model with all kinds of relations:\n\n```swift\n\n@EntityModel\nstruct Message {\n    let id: String\n    let text: String\n    \n    @Relationship(.required)\n    var author: User?\n    \n    @Relationship(.required, inverse: \\.messages)\n    var chat: Chat?\n    \n    @Relationship(inverse: \\.message)\n    var attachment: Attachment?\n    \n    @Relationship(inverse: \\.replyTo)\n    var replies: [Message]?\n    \n    @Relationship(inverse: \\.replies)\n    var replyTo: Message?\n    \n    @Relationship\n    var viewedBy: [User]? = nil\n}\n\n```\n\nEntityModel macro will generate all the necessary things to\nmake our model conform to `EntityModelProtocol` requirements.\n\n\u003cdetails\u003e\u003csummary\u003eEntityModelProtocol definitions\u003c/summary\u003e\n\u003cp\u003e\n\n```swift\npublic protocol EntityModelProtocol {\n    associatedtype ID: Hashable, LosslessStringConvertible\n\n    var id: ID { get }\n   \n    mutating func normalize()\n    \n    mutating func willSave(to context: inout Context) throws\n\n    func didSave(to context: inout Context) throws\n    \n    func save(to context: inout Context, options: MergeStrategy\u003cSelf\u003e) throws\n    \n    func willDelete(from context: inout Context) throws\n\n    func didDelete(from context: inout Context) throws\n  \n    func delete(from context: inout Context) throws\n    \n    func asDeleted(in context: Context) -\u003e Deleted\u003cSelf\u003e?\n    \n    func saveMetadata(to context: inout Context) throws\n    \n    func deleteMetadata(from context: inout Context) throws\n\n    static var defaultMergeStrategy: MergeStrategy\u003cSelf\u003e { get }\n\n    static var fragmentMergeStrategy: MergeStrategy\u003cSelf\u003e { get }\n\n    static var patch: MergeStrategy\u003cSelf\u003e { get }\n    \n    static func queryAll(with nested: Nested..., in context: Context) -\u003e QueryList\u003cSelf\u003e\n         \n    static func nestedQueryModifier(_ query: Query\u003cSelf\u003e, in context: Context, nested: [Nested]) -\u003e Query\u003cSelf\u003e\n}\n}\n```\n\n\u003c/p\u003e\n\u003c/details\u003e\n\n\n## How to Save Entities\n\nNow let's create a chat instance and put some messages into it.\nTo do it we need to create a context first:\n\n```swift\nvar context = Context()\n```\n\n***What is a context?***\n\n\u003eContext is a place where all entities live.\n\u003eActually it's just a wrapper around a plain swift dictionary that is used to store entities and relations.\n\nNow let's create a chat with some messages.\n\n```swift\nlet chat = Chat(\n    id: \"1\",\n    users: .relation([\n        User(id: \"1\", name: \"Bob\"),\n        User(id: \"2\", name: \"Alice\")\n    ]),\n    messages: .relation([\n        Message(\n            id: \"1\",\n            text: \"Any thoughts on SwiftletModel?\",\n            author: .id( \"1\")\n        ),\n        \n        Message(\n            id: \"1\",\n            text: \"Yes.\",\n            author: .id( \"2\")\n        )\n    ]),\n    admins: .ids([\"1\"])\n)\n```\n\nNow let's save chat to the context.\n\n\n```swift\n\ntry chat.save(to: \u0026context)\n\n```\n\n\nJust look at this. \n\nInstead of providing the full entities everywhere...We need to provide them at least somewhere!\nIn other cases, we can just put ids and it will be enough to establish proper relations.\n\nAt this point, our chat and the related entities will be saved to the context.\n\n- All entities will be normalized so we don't have to care about duplication.\n- Bidirectional links will be managed.\n\n\nIf your model has optional fields and contains incomplete data, you can save it as a fragment:\n\n```swift\n\ntry chat.save(to: \u0026context, options: .fragment)\n```\n\nIt will patch the existing model. Read more about fragment data handling: [Handling incomplete Entity data](#handling-incomplete-entity-data)\n\n\n## How to Delete Entities \n\nThe delete method is generated via EntityModel macro making deletion as simple as:\n\n```swift\n\nlet chat = Chat(\"1\")\ntry chat.delete(from: \u0026context)\n\n```\n\nCalling `delete(...)` will:\n- Remove the current instance from the context and store it as `Deleted\u003cEntity\u003e` wrapper\n- Nullify all relations or cascade delete depending on `DeleteRule` attribute\n- Support restoration of the deleted entity via the `Deleted\u003cEntity\u003e` wrapper\n\n### Relationship DeleteRule\n\nDeleteRule allows to specify how the related entities would be treated when current entity is deleted:\n- nullify (the default option)\n- cascade \n\n```swift\n\n@Relationship(deleteRule: .cascade, inverse: \\.message)\nvar attachment: Attachment?\n\n```\n\n\n## How to Query Entities\n\n### Query with nested models\n\nLet's query something. For example, a User with the following nested models:\n\nIt can be done with the following syntax:\n\n\n```swift\n\nlet user = User\n    .query(\"1\")\n    .with(\\.$chats) { chat in\n        chat.with(\\.$messages) { message in\n            message.with(\\.$replies) { reply in\n                reply.with(\\.$author)\n                    .id(\\.$replyTo)\n            }\n            .id(\\.$author)\n            .id(\\.$chat)\n        }\n        .with(\\.$users)\n        .id(\\.$admins)\n    }\n    .resolve(in: context)\n```\n\n*Wait but we've just saved a chat with users and messages.\nNow we are querying things from another end, WTF?*\n\n*Exactly. That's the point of bidirectional links and normalization.*\n\nWhen `resolve(in: context)` is called all entities are pulled from the context storage \nand put in its place according to the nested shape in denormalized form.\n\n### Bulk nested models query\n\nBulk nested models query is a quick way to fetch related models graph up to a certain depth.\nIt's possible to query entity with all nested related models at once in a single line: \n\n```swift\nlet user = User\n    .query(\"1\")\n    .with(.entities)\n    .resolve(in: context)\n```\n\nIt's also possible to query all nested models of the graph recursively up to a certain depth and specify, how do we want to resolve them at a certain depth: as a complete entity, as a fragment or only ids:\n\n```swift\nlet user = User\n    .query(\"1\")\n    .with(.entities, .fragments, .ids)\n    .resolve(in: context)\n```\n\n### Combining bulk nested models with nested models query\n\nBulk nested queries can be combined with other queries to include all related models only for certain parts of the model graph\n\n*In the example below, user would be resovled with all chats and while each chat would include all related models.*\n\n```swift\nlet user = User\n    .query(\"1\")\n    .with(\\.$chats) { chat in\n        chat.with(.entities)\n    }\n    .resolve(in: context)\n```\n\n### Related models query\n\nWe can also query related items directly:\n\n```swift\n\nlet userChats: [Chat] = User\n    .query(\"1\")\n    .related(\\.$chats)\n    .resolve(in: context)\n\n```\n\n### Limiting and Paginating Results\n\nYou can limit the number of results and skip a certain number of entities using the `limit(_:offset:)` method:\n\n```swift\n// Get first 10 users\nlet firstPage = User.query()\n    .limit(10)\n    .resolve(in: context)\n\n// Get next 10 users (pagination)\nlet secondPage = User.query()\n    .limit(10, offset: 10)\n    .resolve(in: context)\n\n// Combined with sorting and filtering\nlet topActiveUsers = User.query()\n    .filter(\\.status == .active)\n    .sorted(by: \\.age.desc)\n    .limit(5)\n    .resolve(in: context)\n```\n\nThe `limit(_:offset:)` method is particularly useful for:\n- Implementing pagination in your UI\n- Reducing memory usage by loading only what's needed\n- Getting top N results from sorted queries\n- Loading data in batches for performance\n\n## How to use Sort Queries\n\nSwiftletModel provides a flexible sorting system that can leverage indexes for improved performance. \nThe sorting API supports both single and multi-property sorting, with options for ascending and descending order.\n\n### Basic Sorting\n#### Single Property Sorting\n\n```swift\n// Ascending sort (default)\nlet users = User.query()\n    .sorted(by: \\.age)\n    .resolve(in: context)\n\n// Descending sort\nlet users = User.query()\n    .sorted(by: \\.age.desc)\n    .resolve(in: context)\n```\n#### Multi-Property Sorting\n\n```swift\n// Sort by multiple properties\nlet users = User.query()\n    .sorted(by: \\.lastName, \\.firstName)\n    .resolve(in: context)\n\n// Mixed ascending/descending\nlet users = User.query()\n    .sorted(by: \\.age.desc, \\.lastName)\n    .resolve(in: context)\n```\n\n### Using Indexes for Sorting\n#### Single Property Index\n\n```swift\n@EntityModel\nstruct User {\n    @Index\u003cSelf\u003e(\\.age) private var ageIndex\n    \n    let id: String\n    let age: Int\n    let name: String\n}\n\n// This sort will use the index\nlet sortedUsers = User.query()\n    .sorted(by: \\.age)\n    .resolve(in: context)\n    \n// Not indexed property sort\nlet sortedUsers = User.query()\n    .sorted(by: \\.name)\n    .resolve(in: context)\n```\n#### Compound Index\n\n```swift\n@EntityModel\nstruct User {\n    @Index\u003cSelf\u003e(\\.lastName, \\.firstName) private var nameIndex\n    \n    let id: String\n    let firstName: String\n    let lastName: String\n    let age: Int\n}\n\n// This sort will use the compound index\nlet sortedUsers = User.query()\n    .sorted(by: \\.lastName, \\.firstName)\n    .resolve(in: context)\n    \n// Not indexed property sort. Compound index won't be used:\nlet sortedUsers = User.query()\n    .sorted(by: \\.lastName)\n    .resolve(in: context)\n    \n// Not indexed property sort. Compound index won't be used:\nlet sortedUsers = User.query()\n    .sorted(by: \\.lastName, \\.age)\n    .resolve(in: context)    \n    \n```\n\n#### Combining Sort and Filter\n```swift\n// Efficient when using indexes\nlet results = User.query()\n    .filter(\\.age \u003e 18)\n    .sorted(by: \\.lastName, \\.firstName)\n    .resolve(in: context)\n\n// Complex sorting with filters\nlet results = User.query()\n    .filter(\\.status == .active)\n    .sorted(by: \\.age.desc, \\.lastName)\n    .resolve(in: context)\n\n```\n\n#### Best Practises and Performance Considerations\n\n| Operation | Indexed | Not Indexed | Notes |\n|-----------|---------|-------------|--------|\n| Single Property Sort | O(n) | O(n log n) | Indexed uses pre-sorted data |\n| Multi-Property Sort | O(n) | O(n log n) | With compound index |\n| Sort + Filter | O(m) | O(n log n) | m = filtered result size |\n| Descending Sort | O(n) | O(n log n) | Same complexity as ascending |\n\nImportant to note: `Desc` sort indexing performance is lower than plain ascending. \n\n\n1. Index Selection:\n- Add indexes for frequently sorted properties\n- Use compound indexes for common sort combinations\n- Consider memory usage, index build performance vs. performance trade-offs\n2. Sort Order:\n- Choose appropriate sort direction (ascending/descending)\n- Consider default sorting needs\n- Use compound sorts when necessary\n3. Performance Optimization:\n- Leverage indexes for better performance\n- Filter before sorting may be beneficial\nConsider result set size\n4. Memory Considerations:\n- Indexes increase memory usage\n- Each compound index requires additional storage\n- Balance between query performance and resource usage\n\n\n## How to use Filter Queries\n\nSwiftletModel provides a powerful and flexible filtering system that supports both indexed and non-indexed queries. \nThe filtering API offers various comparison methods and can leverage indexes for improved performance.\n\n### Basic Filtering\n\n#### Equality Filters\n\n```swift\n// Single property equality\nlet users = User\n        .filter(\\.age == 25)\n        .resolve(in: context)\n        \n// Multiple property equality chain.\nlet results = User\n    .filter(\\.age == 25)\n    .filter(\\.status == .active)\n    .resolve(in: context)\n```\n\n#### Comparison Filters\n\n```swift\n// Greater than\nlet adults = User\n    .filter(\\.age \u003e 18)\n    .resolve(in: context)  \n\n// Less than or equal\nlet juniors = User\n        .filter(\\.age \u003c= 21)\n        .resolve(in: context)\n\n// Range combination\nlet youngAdults = User\n    .filter(\\.age \u003e= 18)\n    .filter(\\.age \u003c 30)\n    .resolve(in: context)\n```\n\n### Complex Filters\n\n#### Logical Operators\n\n```swift\n// OR operation\nlet results = User.filter(\\.age == 25)\n    .or(.filter(\\.age == 30))\n    .resolve(in: context)\n\n// AND operation with predicate\nlet results = User.filter(\\.age \u003e 18)\n    .and(\\.status == .active)\n    .resolve(in: context)\n\n// AND operation with another query\n// Useful for combining complex filter conditions\nlet premiumUsers = User.filter(\\.subscription == .premium)\nlet activeAdults = User.filter(\\.age \u003e 18)\n    .and(\\.status == .active)\n    .and(premiumUsers)\n    .resolve(in: context)\n\n// Complex combinations\nlet results = User.filter(\\.age == 25)\n    .or(.filter(\\.status == .active))\n    .or(.filter(\\.age \u003e 30).and(\\.level \u003c= 4))\n    .resolve(in: context)\n```\n### Text Filtering\n#### String Operations\n\n```swift\n// Contains\nlet results = Message\n    .filter(.string(\\.text, contains: \"hello\"))\n    .resolve(in: context)\n    \n// Prefix/Suffix\nlet results = Message\n    .filter(.string(\\.text, hasPrefix: \"Re:\"))\n    .resolve(in: context)\n    \nlet results = Message\n    .filter(.string(\\.text, hasSuffix: \"regards\"))\n    .resolve(in: context)\n\n// Case sensitivity\nlet results = Message\n    .filter(.string(\\.text, contains: \"Hello\", caseSensitive: true))\n    .resolve(in: context)\n\n\n```\n\n#### Full-Text Search\nWhen using FullTextIndex, you can perform more sophisticated fuzzy mathc text searches:\n\n```swift\n// Fuzzy matching\nlet results = Article.filter(.string(\\.content, matches: \"search terms\"))\n    .resolve(in: context)\n// Multiple field search\nlet results = Article\n    .filter(.string(\\.title, \\.content, matches: \"search terms\"))\n    .resolve(in: context)\n\n```\n\n### Performance Optimization\n\n#### Index Usage\nThe filtering system automatically utilizes available indexes when possible:\n\n```swift\n@EntityModel\nstruct User {\n    @Index\u003cSelf\u003e(\\.age) private var ageIndex          // B-tree index for range queries\n    @HashIndex\u003cSelf\u003e(\\.status) private var statusIndex // Hash index for equality lookups\n\n    let id: String\n    let age: Int\n    let status: UserStatus\n    let level: Int\n}\n\n// This query will use the age index (O(log n))\nlet results = User\n    .filter(\\.age \u003e 18)\n    .resolve(in: context)\n\n// This query will use the status hash index (O(1))\nlet results = User\n    .filter(\\.status == .active)\n    .resolve(in: context)\n```\n\nNon Indexed queries are significantly slower because they require full collection scan.\nIndexed property queries are insanely fast.\n\n| Operation Type | Index Type | Value Type | Indexed | Not Indexed | Notes |\n|---------------|------------|------------|----------|-------------|--------|\n| Equality (==) | `HashIndex` | Hashable | O(1) | O(n) | Hash-based lookup |\n| Equality (==) | `Index` | Comparable | O(log n) | O(n) | B-tree lookup |\n| Comparison (\u003e, \u003c, \u003e=, \u003c=) | `HashIndex` | Hashable | O(n) | O(n) | Hash indexes don't support range queries |\n| Comparison (\u003e, \u003c, \u003e=, \u003c=) | `Index` | Comparable | O(log n) | O(n) | B-tree enables efficient range queries |\n| Sorting | `Index` | Comparable | O(n) | O(n log n) | B-tree maintains sorted order |\n\n### Filters Best Practices\n1. Index Selection:\n- Use `HashIndex` for equality-only queries (O(1) lookups)\n- Use `Index` for range queries and sorting (O(log n) lookups)\n- Add indexes for frequently filtered properties\n- Balance between query performance and memory usage\n- Balance between read query and index update performance\n2. Query Optimization:\n- Place indexed property or most selective filters first\n3. Text Search:\n- Use FullTextIndex for better text search performance\n- Consider case sensitivity requirements\n- Test search relevance with representative data\n\n\n### Filter Method Reference\n```swift\n// Comparison Methods\n// == : Equal to\n// != : Not equal to\n// \u003e : Greater than\n// \u003e= : Greater than or equal to\n// \u003c : Less than\n// \u003c= : Less than or equal to\n\n// String Methods\n// contains: Substring matching\n// hasPrefix: Starts with\n// hasSuffix: Ends with\n// matches: Full-text fuzzy search matching\n\n// Logical Operators\n// and: Combines filters with AND logic\n// or: Combines filters with OR logic \n \n// Complex filter combining multiple conditions\nlet results = User\n    .filter(\\.age \u003e= 18)\n    .and(\\.status == .active)\n    .or(.filter(\\.role == .admin))\n    .and(\\.lastLogin \u003e oneWeekAgo)\n    .resolve(in: context)\n\n// Text search with multiple fields\nlet articles = Article\n    .filter(.string(\\.title, \\.content, matches: \"swift database\"))\n    .filter(\\.status == .published)\n    .resolve(in: context)\n```\n\n## Codable Conformance\n\nSince models are implemented as plain structs we can get `Codable` out of the box:\n\n```swift\n\nextension User: Codable { }\n\nextension Chat: Codable { }\n\nextension Message: Codable { }\n\n/** \nAnd then use it for our codable purposes:\n*/\n\nlet encoder = JSONEncoder.prettyPrinting\nencoder.relationEncodingStrategy = .plain\nlet userJSON = user.prettyDescription(with: encoder) ?? \"\"\nprint(userJSON)\n\n```\n\n\u003cdetails\u003e\u003csummary\u003eHere is the JSON string that we will get\u003c/summary\u003e\n\u003cp\u003e\n\n```\n{\n  \"adminOf\" : null,\n  \"chats\" : [\n    {\n      \"admins\" : [\n        {\n          \"id\" : \"1\"\n        }\n      ],\n      \"id\" : \"1\",\n      \"messages\" : [\n        {\n          \"attachment\" : null,\n          \"author\" : {\n            \"id\" : \"1\"\n          },\n          \"chat\" : {\n            \"id\" : \"1\"\n          },\n          \"id\" : \"1\",\n          \"replies\" : [\n            {\n              \"attachment\" : null,\n              \"author\" : {\n                \"adminOf\" : null,\n                \"chats\" : null,\n                \"id\" : \"2\",\n                \"name\" : \"Alice\"\n              },\n              \"chat\" : null,\n              \"id\" : \"2\",\n              \"replies\" : null,\n              \"replyTo\" : {\n                \"id\" : \"1\"\n              },\n              \"text\" : \"Yes.\",\n              \"viewedBy\" : null\n            }\n          ],\n          \"replyTo\" : null,\n          \"text\" : \"Any thoughts on SwiftletModel?\",\n          \"viewedBy\" : null\n        },\n        {\n          \"attachment\" : null,\n          \"author\" : {\n            \"id\" : \"2\"\n          },\n          \"chat\" : {\n            \"id\" : \"1\"\n          },\n          \"id\" : \"2\",\n          \"replies\" : [\n\n          ],\n          \"replyTo\" : null,\n          \"text\" : \"Yes.\",\n          \"viewedBy\" : null\n        }\n      ],\n      \"users\" : [\n        {\n          \"adminOf\" : null,\n          \"chats\" : null,\n          \"id\" : \"1\",\n          \"name\" : \"Bob\"\n        },\n        {\n          \"adminOf\" : null,\n          \"chats\" : null,\n          \"id\" : \"2\",\n          \"name\" : \"Alice\"\n        }\n      ]\n    }\n  ],\n  \"id\" : \"1\",\n  \"name\" : \"Bob\"\n}\n```\n\n\n\u003c/p\u003e\n\u003c/details\u003e\n\n\n## Relationship Types\n\n\nSwiftletModel supports the following types of relations:\n- One way \u0026 Mutual\n- To One \u0026 To Many\n- Optional \u0026 Required\n\nAll of them are represented by a single property wrapper: `@Relationship`\n \n### Optional to-one Relationship\n\nHere is a way to define an optional relation. \n\n```swift\n/**\nIt can be either one way. \n*/\n\n@Relationship\nvar user: User? = nil\n\n\n/**\nOr can be mutual. Mutual relation requires providing an inverse key pathsa as a witness to ensure \nthat it is indeed mutual\n*/\n\n@Relationship(inverse: \\.message)\nvar attachment: Attachment?\n\n```\n\nThe optionality of the Relation means that it can be explicitly nullified. \n(See: [Handling missing Data for to-one Relations](#handling-missing-data-for-to-one-relations))\n\n```swift\n/**\nWhen this message is saved, it **will nullify**\nthe existing message-attachment relation in the context.\n*/\n\nlet message = Message(\n    id: \"1\",\n    text: \"Any thoughts on SwiftletModel?\",\n    author: .id( \"1\"),\n    attachment: .null\n)\n\n\ntry message.save(to: \u0026context)\n\n```\n\n### Required to-one Relationship\n\nThere is a way to define an required relation. \n\n\n```swift\n/**\nIt can be either one way: \n*/\n\n@Relationship(.required)\nvar author: User?\n    \n\n/**\nIt can be mutual. Mutual relation requires providing a witness to ensure that it is indeed mutual: direct and inverse key paths.\nInverse relations can be either to-one or to-many and must be mutual.\n*/\n\n@Relationship(.required, inverse: \\.messages)\nvar chat: Chat?\n\n```\n\n\nIf it is a required relation, why is the property still optional? \nRelation properties are always optional because it's the way how SwiftletModel handles incomplete data. \n\nRequired relation only means that it cannot be explicitly nullified.\n\n\n### To-many Relationship\n\nTo-many relationships can be defined the following way:\n\n```swift\n/**\nIt can be either one way:\n*/\n\n@Relationship\nvar viewedBy: [User]? = nil\n\n \n/**\nIt can also be mutual. Mutual relation requires to provide an inverse key path as a witness \nto ensure that it is really mutual.\n*/\n\n@Relationship(inverse: \\.replyTo)\nvar replies: [Message]?\n\n```\n \nBasically, it's required because there is no reason for to-many relations to have an explicit nil. \n\n## Establishing Relations\n\nThe properties themselves are read-only. \nHowever, relations can be set up through the property wrapper's projected values.\n\n### Setting to-one relations\n\n```swift\n\nvar message = Message(\n    id: \"1\",\n    text: \"Howdy!\"\n)\n\n/**\nFor to-one relations, we can attach by directly setting the relation:\n*/\nmessage.$author = .relation(user)\ntry message.save(to: \u0026context)\n\n/**\nWe can also attach by id. In that case \nwe need to make sure that both entities exist in the context\n*/\n \nmessage.$author = .id( user.id)\ntry message.save(to: \u0026context)\ntry user.save(to: \u0026context)\n\n/**\nIf the relation is optional we can nullify it by setting it to explicit nil. \nIn that case, the existing relation will be destroyed on save. \nHowever, if there was some related entity it will not be deleted. \n*/\n \nmessage.$attachment = .null\ntry message.save(to: \u0026context)\n\n\n/**\nSetting the relation to `none` will not have any effect on the stored data. \nThis happens automatically during normalization when you save an entity:\n*/\n \nmessage.$attachment = .none\ntry message.save(to: \u0026context)\n\n\n```\n\n### Setting to-many relations\n\nTo-many relations can be set up exactly the same way:\n\n```swift\n\n/**\nTo-many relations can be set directly by providing an array of entities.\n*/\nchat.$messages = .relation([message])\ntry chat.save(to: \u0026context)\n\n/**\nAn array of ids will also work, but all entities should be additionally saved to the context. \n*/\nchat.$messages = .ids([message.id])\ntry chat.save(to: \u0026context)\ntry message.save(to: \u0026context)     \n```\n\nTo-many relations support not only setting up new relations, \nbut also appending new relations to the existing ones. It can be done via `appending(...)`\n\n(See: [Handling incomplete data for to-many Relations](#Handling-incomplete-data-for-to-many-relations))\n \n \n```swift\n\n/**\nNew to-many relations can be appended \nto the existing ones when set as an appending slice:\n*/\nchat.$messages = .appending(relation: [message])\ntry chat.save(to: \u0026context)\n\n/**\nAn array of ids will also work, \nbut all entities should be additionally saved to the context. \n*/\nchat.$messages = .appending(ids: [message.id])\ntry chat.save(to: \u0026context)\ntry message.save(to: \u0026context)     \n```\n\n### Saving Relations\n\nSaving an entity with all related ones is possible thanks to the Entity save method and is done automatically.\n\n\n### Removing Relations\n\nThere are several options to remove relations.\n\n```swift\n/**\nWe can detach entities. This will only destroy the relation between them while keeping entities in storage.\n*/\ndetach(\\.$chats, inverse: \\.$users, in: \u0026context)\n\n\n/**\nWe can delete related entities. It only destroys the relationship between them.  The related entities will be also removed from storage with their `delete(...)` method.\n*/\ntry delete(\\.$attachment, inverse: \\.$message, from: \u0026context)\n\n\n/**\nWe can explicitly nullify the relation. This is an equivalent of `detach(...)`\n*/\nmessage.$attachment = .null\ntry message.save(to: \u0026context)\n\n```\n\n\n## Incomplete Data Handling\n\nSwiftletModel provides a few strategies to handle incomplete data for the cases:\n\n- Incomplete Entity Models\n- Incomplete Related Entity Models\n- Incomplete collections of to-many Relations\n- Missing Data for to-one Relations\n\n\n### Handling incomplete Entity Models\n\nWhen the service gets more mature, models often become bulky.\nWe sometimes have to fetch them partially from different sources or deal with partial model data. \n\nLet's define a user model with an optional Profile. \n\n```swift\n\nextension User {\n    /**\n    Something heavy here is that the backend does not serve for all requests.\n    */\n    struct Profile: Codable { ... }\n}\n \n@EntityModel \nstruct User: Codable {\n    let id: String\n    private(set) var name: String\n    private(set) var avatar: Avatar\n    private(set) var profile: Profile?\n    \n    @Relationship(inverse: \\.users)\n    var chats: [Chat]?\n    \n    @Relationship(inverse: \\.admins)\n    var adminOf: [Chat]?\n}\n \n\n```\n\nIn SwiftletModel partial entity models are called fragments. SwiftletModel provides a reliable way \nto deal with fragments via `MergeStrategy` without corrupting existing data.\n\nMergeStrategy defines how new entities are merged with existing ones that we already have in the Context.\n\n\n```swift\n\n/**\nTo handle that `EntityModelProtocol` has a default and fragment merge strategies:\n*/\n\npublic extension EntityModelProtocol {\n    static var defaultMergeStrategy: MergeStrategy\u003cSelf\u003e { .replace }\n    \n    static var fragmentMergeStrategy: MergeStrategy\u003cSelf\u003e { Self.patch }\n}\n```\n\n#### Default Merge Strategy\n\nWhen saving entities to context, you can omit the `options`\nsince the defaultMergeStrategy is used. \n\nThe default merge Strategy replaces existing models in the context upon saving:\n\n```swift\nvar context = Context()\n\n/**\nThis is a complete user entity having all properties set:\n*/\nlet user = User(\n    id: \"1\", \n    name: \"Bob\", \n    avatar: Avatar(...), \n    profile: User.Profile(...)\n)\n\ntry user.save(to: \u0026context)\n\n```\n\n\n#### Fragment Merge Strategy\n\nFragment merge strategy patches existing models in the context. \nIn other words, it updates only non-nil values. \nIt's automatically generated for all mutable nullable properties via macro so you don't have to do anything.\n\n\n```swift\nvar context = Context()\n\n/**\nThis is a fragment. It doesn't a profile. \nProbably for a reason, we don't know, but we have to deal with it.\n*/\nlet user = User(\n    id: \"1\", \n    name: \"Bob\", \n    avatar: Avatar(...)\n)\n\ntry user.save(to: \u0026context, options: .fragment)\n\n\n``` \n\n\n#### Last Write Wins Merge Strategy\n\nThe last write wins merge strategy compares timestamps to determine which entity is newer, then applies merge strategies accordingly:\n\n```swift\n@EntityModel\nstruct User {\n    let id: String\n    var name: String?\n    var profile: Profile?\n    var lastModified: Date\n}\n\nextension User {\n    // New entity is considered to be the source of truth \n    // and only missing properties are patched with the old ones.\n    static var lastWritePatch: MergeStrategy\u003cSelf\u003e {\n        .lastWriteWins(User.patch, comparedBy: \\.lastModified)\n    }\n\n    // New entity is considered to be the source of truth and replaces the old one.\n    static var lastWriteWins: MergeStrategy\u003cSelf\u003e {\n        .lastWriteWins(.replace, comparedBy: \\.lastModified)\n    }\n}\n\n// Usage example:\nlet oldUser = User(id: \"1\", name: \"Bob\", profile: nil, lastModified: Date.distantPast)\nlet newUser = User(id: \"1\", name: nil, profile: profile, lastModified: Date())\n\ntry oldUser.save(to: \u0026context, options: User.lastWritePatch)\ntry newUser.save(to: \u0026context, options: User.lastWritePatch)\n\n// Result: name=\"Bob\" (preserved from old), profile=profile (from new)\n// since new.lastModified \u003e old.lastModified\n\n\n```\n\nFor entities that implement `Comparable`, the `.lastWriteWins` strategy can be used without explicitly specifying the comparison keyPath:\n\n```swift\nextension User: Comparable {\n    static func \u003c (lhs: User, rhs: User) -\u003e Bool {\n        lhs.lastModified \u003c rhs.lastModified\n    }\n}\n\nstatic var lastWriteWins: MergeStrategy\u003cSelf\u003e {\n    .lastWriteWins(User.patch)\n}\n```\n\n#### Customizing Merge Strategies\n\n\nBoth default and fragment merge strategies can be overridden for any entity:\n \n```swift\nextension User {\n    /**\n    This will make patching as the default behavior:\n    */\n    static var defaultMergeStrategy: MergeStrategy\u003cSelf\u003e {\n        User.patch\n    }\n    \n    /**\n    This is what the `User.patch` strategy for the user \n    with an optional `profile` actually looks like: \n    */\n    static var fragmentMergeStrategy: MergeStrategy\u003cSelf\u003e { \n        MergeStrategy(\n            .patch(\\.profile)\n        )\n     }\n}\n\n```\n\nMerge strategy may include several properties.\n\n```swift\n\nMergeStrategy(\n    .patch(\\.name),\n    .patch(\\.profile),\n    .patch(\\.avatar)\n)\n\n```\nMerge strategy can be applied to arrays. \n\n```swift\n\nMergeStrategy(\n    .append(\\.arrayOfSomething)\n)\n```\n\nYou can write your own merge strategy for any type:\n\n```swift\nextension MergeStrategy {\n    /**\n    This is what the property patch MergeStrategy looks like.\n    */\n    static func patch\u003cEntity, Value\u003e(_ keyPath: WritableKeyPath\u003cEntity, Optional\u003cValue\u003e\u003e) -\u003e MergeStrategy\u003cEntity\u003e   {\n        MergeStrategy\u003cEntity\u003e { old, new in\n            var new = new\n            new[keyPath: keyPath] = new[keyPath: keyPath] ?? old[keyPath: keyPath]\n            return new\n        }\n    }\n}\n\n```\n\n### Handling incomplete Related Entity Models\n\nWhen assigning related nested entities, we can mark them as fragments to utilise fragment merging strategy:\n\n```swift\n\nvar chat = Chat(id: \"1\")\nchat.$users = .fragment([.bob, .alice, .john])\ntry chat.save(to: \u0026context)\n\n```\n\n\n### Handling incomplete data for to-many Relations\n\nWe often have to deal with portions of data. \nIf we have a collection of anything on the backend it will almost certainly be paginated.\n\nSwiftletModel provides a convenient way to deal with incomplete collections for to-many relations.\n\nWhen setting to-many relation it's possible to mark the collection as a appending slice. \nIn that case, all the related entities will be appended to the existing ones.\n \n```swift\n\n/**\nNew to-many relations can be appended \nto the existing ones when we set them as a appending entities:\n*/\nchat.$messages = .appending(relation: [message])\ntry chat.save(to: \u0026context)\n\n/**\nor appending ids:\n*/\nchat.$messages = .appending(ids: [message])\ntry chat.save(to: \u0026context)\n\n/**\nor appending fragments to ulitise fragment merging strategy:\n*/ \nchat.$messages = .appending(fragment: [message])\ntry chat.save(to: \u0026context)\n\n```\n\n\n### Handling missing data for to-one Relations\n\nTo-one relation can be either optional or required.\n\nBasically, data can be missing for at least 3 reasons:\n\n1. The business logic of the app allows the related entity to be missing. For example: a message may not have an attachment.\n\n2. Data is missing because we haven't loaded it yet. If the source is a backend or even a local storage there is almost certainly a case when the app hasn't received the data yet. \n\n3. The logic of obtaining the data implies that some of the data will be missing. For example: a typical app flow where we obtain a list of chats from the backend. Then we get a list of messages for the chat. Even though a message cannot exist without a chat, a message model coming from the backend will hardly ever contain a chat model because it will make the shape of the data weird with a lot of duplication.\n\n\nWhen we deal with missing data it's hard to figure out the reason why it's missing. \nIt can always be an explicit nil or maybe not.\n\nThat's why SwiftletModel's relations properties are always optional. \nIt allows to implement a patching update policy for relations by default: when entities with missing relations are saved to the storage they don't overwrite or nullify existing relations.\n\n\nThis allows to safely update models and merge them with the exising data:\n\n\n```swift\n\n/**\nWhen this message is saved it **WILL NOT OVERWRITE** \nexisting relations to attachments if there are any:\n*/\nlet message = Message(\n    id: \"1\",\n    text: \"Any thoughts on SwiftletModel?\",\n    author: .id( \"1\"),\n)\n\ntry message.save(to: \u0026context)\n\n```\n\nOptional relation allows to set the relation to an explicit nil:  \n\n```swift\n\n/**\nWhen a message with an explicit nil \nis saved it **WILL OVERWRITE** existing relations to the attachment by nullifying them:\n*/\nlet message = Message(\n    id: \"1\",\n    text: \"Any thoughts on SwiftletModel?\",\n    author: .id( \"1\"),\n    attachment: .null\n)\n\n\ntry message.save(to: \u0026context)\n\n```\n\n### Incomplete Data Handling Summary \n\nUse `default` merge strategy to replace entity with new full entity:\n```swift\ntry message.save(to: \u0026context)\n```\n\nUse fragment merge strategy to patch entity with non-`nil` fields only:\n```swift\ntry message.save(to: \u0026context, options: .fragment)\n```\n\nSet relation accordingly to the case to carry out a proper relation update when saving an entity:\n\n| Slice                | Description                                           |\n|----------------------|-------------------------------------------------------|\n| `.relation(x)`       | Overwrite with new full related entity                |\n| `.fragment(x)`       | Patch related entity with non-`nil` fields only     |\n| `.id(\"x\")`           | Set to-one relation by ID                             |\n| `.ids([\"x\", \"y\"])`   | Set to-many relation by IDs                           |\n| `.relation([x])`     | Overwrite with new full related entities              |\n| `.fragment([x])`     | Patch related entities with non-`nil` fields only     |\n| `.appending(...)`    | Append to an existing to-many relation                |\n| `.null`              | Explicitly nullify a relation (only if optional)      |\n| `.none`              | No-op; leaves existing relation unchanged             |\n\n\n## Indexing\nSwiftletModel provides four types of indexes to optimize data access and enforce uniqueness constraints: `Index`, `HashIndex`, `Unique`, and `FullTextIndex`. Each serves a different purpose and offers specific functionality.\n\n### Index\nThe Index property wrapper enables efficient range querying and sorting of entity properties using a B-tree data structure.\n\n```swift\n@Index\u003cEntity\u003e(\\.propertyName)\nprivate var propertyIndex\n```\n\nFeatures:\n- Requires `Comparable` property types\n- Allows compound indexes up to 4 properties\n- Maintains sorted order for efficient range queries\n- Supports comparison operators: `==`, `\u003c`, `\u003c=`, `\u003e`, `\u003e=`, `!=`\n- O(log n) lookup performance\n- Declared as instance property (required for generic types, recommended for consistency)\n\nExample:\n\n```swift\n@EntityModel\nstruct User {\n    @Index\u003cSelf\u003e(\\.age) private var ageIndex\n    @Index\u003cSelf\u003e(\\.lastName, \\.firstName) private var nameIndex\n\n    let id: String\n    let firstName: String\n    let lastName: String\n    let age: Int\n}\n\n// Range queries\nlet adults = User.filter(\\.age \u003e= 18).resolve(in: context)\nlet seniors = User.filter(\\.age \u003e 65).resolve(in: context)\nlet teens = User.filter(\\.age \u003e= 13 \u0026\u0026 \\.age \u003c 20).resolve(in: context)\n\n// Sorting\nlet sortedByAge = User.query().sorted(by: \\.age).resolve(in: context)\n```\n\n### HashIndex\nThe HashIndex property wrapper enables O(1) equality lookups using a hash-based data structure.\n\n```swift\n@HashIndex\u003cEntity\u003e(\\.propertyName)\nprivate var propertyIndex\n```\n\nFeatures:\n- Requires `Hashable` property types\n- Allows compound indexes up to 4 properties\n- O(1) constant-time equality lookups\n- Only supports equality (`==`) queries\n- More efficient than Index for equality-only queries\n- Declared as instance property (required for generic types, recommended for consistency)\n\nExample:\n\n```swift\n@EntityModel\nstruct User {\n    @HashIndex\u003cSelf\u003e(\\.status) private var statusIndex\n    @HashIndex\u003cSelf\u003e(\\.region, \\.department) private var regionDeptIndex\n\n    let id: String\n    let status: String\n    let region: String\n    let department: String\n}\n\n// Equality lookups\nlet activeUsers = User.filter(\\.status == \"active\").resolve(in: context)\nlet salesTeam = User\n    .filter(\\.region == \"US\" \u0026\u0026 \\.department == \"Sales\")\n    .resolve(in: context)\n```\n\n**When to use Index vs HashIndex:**\n| Use Case | Recommended Index |\n|----------|-------------------|\n| Equality queries only | `HashIndex` (O(1)) |\n| Range queries (`\u003c`, `\u003e`, `\u003c=`, `\u003e=`) | `Index` (O(log n)) |\n| Sorting | `Index` |\n| Both equality and range queries | `Index` |\n\n#### Generic Types with Indexes\n\nFor generic entity models, indexes must be declared as instance properties since Swift doesn't allow static properties with generic constraints:\n\n```swift\n@EntityModel\nstruct GenericAttachment\u003cT: Codable \u0026 Sendable \u0026 Hashable\u003e: Codable, Sendable {\n    @HashIndex\u003cSelf\u003e(\\.kind) private var kindIndex\n\n    let id: String\n    var kind: T\n\n    @Relationship\n    var message: Message? = .none\n}\n```\n### Unique\nThe Unique property wrapper enforces uniqueness constraints on entity properties.\n\n```swift\n@Unique\u003cEntity\u003e(\\.propertyName, collisions: .throw)\nprivate var uniqueIndex\n```\n\nFeatures:\n- Enforces uniqueness constraints\n- Supports compound unique constraints up to 4 properties\n- Configurable collision handling:\n    - throw: Throws error on violation\n    - upsert: Replaces existing entity\n    - custom collision handling\n- Works with both Comparable and Hashable types\n- Declared as instance property (required for generic types, recommended for consistency)\n\n\nExample:\n\n```swift\n@EntityModel\nstruct User {\n    // Unique username with upsert on collision\n    @Unique\u003cSelf\u003e(\\.username, collisions: .upsert) \n    private var uniqueUsername\n    \n    // Unique email that throws on collision\n    @Unique\u003cSelf\u003e(\\.email, collisions: .throw) \n    private var uniqueEmail\n    \n    // Custom collision handling for current user\n    @Unique\u003cSelf\u003e(\\.isCurrent, collisions: .updateCurrentUser) \n    private var currentUserIndex\n    \n    let id: String\n    let username: String\n    let email: String\n    var isCurrent: Bool\n}\n\n// Custom collision resolver implementation\nextension CollisionResolver where Entity == User {\n    static var updateCurrentUser: Self {\n        CollisionResolver { existingId, _, _, context in\n            guard var user = Query\u003cEntity\u003e(context: context, id: existingId).resolve(in: context),\n                user.isCurrent\n            else {\n                return\n            }\n               \n            user.isCurrent = false\n            try user.save(to: \u0026context)\n        }\n    }\n}\n```\n\nThis example demonstrates three different collision handling strategies:\n1. `.upsert` - Automatically replaces existing entity when username conflicts\n2. `.throw` - Throws an error when email conflicts\n3. `.updateCurrentUser` - Custom logic to handle \"current user\" status:\n   - When a new user is marked as current, automatically updates the existing current user not being current anymore\n   - Ensures only one user can be marked as current at a time\n\n#### Unique Constraints for Optional Values\n\nSometimes you need uniqueness constraints that only apply when a value is present — for example, an optional email field where multiple `nil` values are allowed, but non-nil values must be unique.\n\nYou can achieve this by implementing a custom `CollisionResolver` that ignores collisions when the value is `nil`:\n\n```swift\n@EntityModel\nstruct User {\n    @Unique\u003cSelf\u003e(\\.email, collisions: .ignoreNil)\n    private var uniqueEmail\n\n    let id: String\n    var email: String?\n}\n\nextension CollisionResolver {\n    static var ignoreNil: Self {\n        CollisionResolver { existingId, newEntity, keyPath, context in\n            // Only throw if the new value is non-nil\n            guard newEntity[keyPath: keyPath] != nil else {\n                return\n            }\n            throw UniqueConstraintViolation(existingId: existingId, keyPath: keyPath)\n        }\n    }\n}\n\n// Usage:\nvar context = Context()\n\n// Multiple users with nil email are allowed\nlet user1 = User(id: \"1\", email: nil)\nlet user2 = User(id: \"2\", email: nil)\ntry user1.save(to: \u0026context) // OK\ntry user2.save(to: \u0026context) // OK\n\n// Non-nil emails must be unique\nlet user3 = User(id: \"3\", email: \"alice@example.com\")\nlet user4 = User(id: \"4\", email: \"alice@example.com\")\ntry user3.save(to: \u0026context) // OK\ntry user4.save(to: \u0026context) // Throws UniqueConstraintViolation\n```\n\nThis pattern keeps the API minimal and composable while supporting common real-world use cases like optional unique identifiers.\n\n### FullTextIndex\n\nThe FullTextIndex property wrapper implements full-text search capabilities using the BM25 ranking algorithm.\n\n```swift\n@FullTextIndex\u003cEntity\u003e(\\.propertyName)\nprivate var searchIndex\n```\n\nFeatures:\n- Full-text search with relevance ranking\n- BM25 scoring algorithm for better search results\n- Token frequency tracking \u0026 Document length normalization\n- Supports multiple text fields\n- Automatic tokenization and indexing\n- Optimized for search performance\n- Used for `match`, `contains`, `prefix`, `suffix` text search queries\n- Declared as instance property (required for generic types, recommended for consistency)\n\nExample:\n\n```swift\n@EntityModel\nstruct Article {\n    @FullTextIndex\u003cSelf\u003e(\\.title, \\.content) private var contentIndex\n    \n    let id: String\n    let title: String\n    let content: String\n}\n\n// Usage\nlet articles = Article\n    .query()\n    .filter(.string(\\.title, \\.content, matches: \"search terms\"))\n    .resolve(in: context)\n```\n\n### Index Performance Considerations\n1. Index Selection:\n    - Use `HashIndex` for equality-only queries (O(1) lookup)\n    - Use `Index` for range queries and sorting (O(log n) lookup)\n    - Use `Unique` when uniqueness must be enforced\n    - Use `FullTextIndex` for text search functionality\n2. Compound Indexes:\n    - Limited to 4 properties for performance reasons\n    - Consider the order of properties in compound indexes\n    - More indexes increase write overhead\n3. Memory Usage:\n    - Each index type maintains its own data structures\n    - `Index` uses B-tree structures for ordered storage\n    - `HashIndex` uses hash tables for direct lookups\n    - Full-text indexes require more memory for token storage\n    - Consider the trade-off between query performance and memory usage\n4. Performance\n    - Each index requires time to build and update that is executed when model is saved\n    - `HashIndex` has O(1) read and write performance\n    - `Index` has O(log n) read and write performance\n    - Consider the trade-off between query read performance and index update performance\n\nBest Practices\n1. Index Sparingly:\n    - Only index properties that need to be queried or sorted\n    - Avoid redundant indexes\n    - Consider query patterns when designing indexes\n2. Choose the Right Index Type:\n    - Prefer `HashIndex` when you only need equality checks\n    - Use `Index` when you need range queries or sorting\n    - Don't use both `Index` and `HashIndex` on the same property\n3. Instance Properties for Indexes:\n    - Always use instance properties for index declarations\n    - Required for generic types, recommended for all types for consistency\n    - Index properties are omitted from Codable encoding/decoding automatically\n4. Collision Handling:\n    - Use .throw for strict uniqueness enforcement\n    - Use .upsert when replacing existing records is acceptable\n    - Use collision resolver for custom replacement logic\n5. Full-Text Search:\n    - Index only text fields that need to be searched\n    - Consider the length of indexed content\n    - Test search relevance with representative data\n\n## Schema \n\nSchema is implicitly defined by your model types. However, in some cases, it's beneficial to define the entire schema explicitly in one place, enabling bulk data queries. This approach proves especially useful for schema versioning, persistent storage, and synchronization with external data sources.\n\n### Schema Versioning\n\nYou can define a schema that includes all related entities and version your data model. Here's an example:\n\n```swift    \n@EntityModel\nstruct Schema: Codable {\n    enum Version: String { case v1 }\n    \n    var id: String { \"\\(Schema.self)\"}\n    \n    @Relationship\n    var v1: V1? = .relation(V1())\n}\n\ntypealias User = Schema.V1.User\ntypealias Chat = Schema.V1.Chat\ntypealias Message = Schema.V1.Message\ntypealias Attachment = Schema.V1.Attachment\n \nextension Schema {\n    \n    @EntityModel\n    struct V1: Codable {\n        var version: Version { .v1 }\n        \n        var id: String { version.rawValue }\n        \n        @Relationship var attachments: [Attachment]? = .none\n        @Relationship var chats: [Chat]? = .none\n        @Relationship var messages: [Message]? = .none\n        @Relationship var users: [User]? = .none\n        \n        @Relationship var deletedAttachments: [Deleted\u003cAttachment\u003e]? = .none\n        @Relationship var deletedChats: [Deleted\u003cChat\u003e]? = .none\n        @Relationship var deletedMessages: [Deleted\u003cMessage\u003e]? = .none\n        @Relationship var deletedUsers: [Deleted\u003cUser\u003e]? = .none\n    }\n}\n\n```\n\nSince schema is a plain struct like any other entity, migration between versions is straightforward:\n1. Add a new version of the schema\n2. Update the typealiases to point to the latest version \n3. Define how data should be mapped to the latest version\n\n### Schema Bulk Queries\n\nSwiftletModel provides powerful bulk query capabilities for your schema, which are particularly useful for:\n- Data synchronization with backends or local databases\n- Creating full backups\n- Implementing undo/redo functionality\n- Debugging and development tools\n\nHere's how to define and use schema queries:\n\n```swift\nextension Schema {\n    /**\n        - Query all available schemas\n        - For each schema query all related versions\n        - For each version query all available entities\n        - For each entity query all related entities' IDs\n        - is enough to restore the entire schema and all relations.\n    */\n    static func fullSchemaQuery() -\u003e QueryList\u003cSelf\u003e {\n        Schema\n            .queryAll(\n                with: .entities, .schemaEntities, .ids,\n            )\n    }\n\n    /**\n        - Query all available schemas\n        - For each schema query all related versions\n        - For each version query all available entities with `updatedAt` within a specific time range\n        - For each entity query all related entities' IDs\n        - is enough to restore the entire schema and all relations.\n    */\n\n    static func fullSchemaQuery(updated: ClosedRange\u003cDate\u003e) -\u003e QueryList\u003cSelf\u003e {\n        Schema\n            .queryAll(\n                with: .entities, .schemaEntities, .ids,\n            )\n            .filter(\n                .updated(within: updated),\n            )\n    }\n\n\n    /**\n        - Query all available schemas\n        - For each schema query all related versions\n        - For each version query all available entities as `fragments` with `updatedAt` within a specific time range\n        - For each entity query all related entities' IDs\n        - is enough to restore the entire schema and all relations.\n    */\n    static func fullSchemaQueryFragments() -\u003e QueryList\u003cSelf\u003e {\n        Schema\n            .queryAll(\n                with: .entities, .schemaFragments, .ids,\n            )\n    }\n}\n\n// Usage example:\nvar context = Context()\n\n// Initialize and save schema\nlet schema = Schema()\ntry schema.save(to: \u0026context)\n\n// Save some entities\nlet user = User(id: \"1\", name: \"Bob\")\nlet chat = Chat(id: \"1\", users: .relation([user]))\ntry user.save(to: \u0026context)\ntry chat.save(to: \u0026context)\n\n// Query entire schema with all entities and relationships\nlet schemaData = Schema.fullSchemaQuery().resolve(in: context)\n\n// Query schema changes since last sync\nvar lastSyncDate = Date.distantPast\nlet syncChanges = Schema.fullSchemaQuery(updated: lastSyncDate...Date()).resolve(in: context)\nlastSyncDate = Date()\n```\n\nKey benefits of this schema approach:\n- **Version Control**: Manage schema migrations and backwards compatibility\n- **Complete Data Access**: Query all entities and relationships in one operation\n- **Deletion Tracking**: Track deleted entities for sync operations\n- **Time-Based Filtering**: Query entities updated within specific time ranges\n- **Flexible Resolution**: Choose between fetching complete entities, fragments, or just IDs\n\nYou can use schema queries for:\n- Data synchronization with a backend or local db\n- Creating local backups\n- Implementing undo/redo functionality\n- Debugging and development tools\n\n### Metadata\n\nSwiftletModel provides a metadata sidecar that allows storing and indexing additional information about entities. This is particularly useful for tracking entity state changes, implementing sync mechanisms, and filtering entities based on metadata values.\n\nBy default, SwiftletModel automatically tracks the `updatedAt` metadata for all entities, updating it whenever an entity is saved. This enables efficient querying of recently changed entities.\n\nExample usage:\n\n```swift\n// Query entities updated within a time range\nlet recentChanges = User\n    .filter(.updated(within: lastSync...Date()))\n    .resolve(in: context)\n\n```\n\nThe metadata system supports both Comparable and Hashable values, allowing you to:\n- Track timestamps for entity changes\n- Implement efficient sync mechanisms\n- Filter entities based on metadata values\n\nKey features:\n- Automatic `updatedAt` tracking\n- Efficient querying using metadata indexes\n\nThis is particularly useful when implementing:\n- Data synchronization\n- Change tracking\n\nFor example, you can use metadata to implement efficient incremental sync with a backend:\n\n```swift\n// Track last sync time\nvar lastSyncDate = Date.distantPast\n\n// Query only entities that changed since last sync\nlet updatedEntities = User\n    .filter(.metadata(\\.updatedAt, within: lastSyncDate...Date()))\n    .resolve(in: context)\n\n// Update sync timestamp\nlastSyncDate = Date()\n```\n\nThe metadata system is built on top of SwiftletModel's indexing capabilities, ensuring efficient querying and filtering operations.\n\n## Type Safety\n\nRelations rely heavily on principles of Type-Driven design under the hood.\nThey are implemented so that there is very little chance of misuse. \n\n```swift\nstruct Relation\u003cEntity, Directionality, Cardinality, Constraints\u003e { ... }\n```\n\nAll you can do with relation is defined by its Directionality, Cardinality, and Constraint types.\n\nAny mistake will be spotted at compile time:\n\n- You cannot accidentally set an explicit nil to the required relation\n- You cannot establish a wrong relation by making it mutual on one side and one-way on another\n- You can't save relation in a wrong way\n- You cannot ever confuse to-one and to-many relations \n\n\nThis also means that you cannot accidentally break it.\n \n\n## Documentation\n\nFull project documentation can be found [here](https://swiftpackageindex.com/KazaiMazai/SwiftletModel/main/documentation/swiftletmodel)\n\n\n## Licensing\n\nSwiftletModel is licensed under MIT license.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkazaimazai%2Fswiftletmodel","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkazaimazai%2Fswiftletmodel","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkazaimazai%2Fswiftletmodel/lists"}