{"id":40970664,"url":"https://github.com/coenttb/swift-records","last_synced_at":"2026-01-22T06:43:07.568Z","repository":{"id":311199344,"uuid":"1038358217","full_name":"coenttb/swift-records","owner":"coenttb","description":"The Swift library for PostgreSQL database operations.","archived":false,"fork":false,"pushed_at":"2025-10-17T16:34:52.000Z","size":530,"stargazers_count":6,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-31T00:25:23.069Z","etag":null,"topics":["nio","postgres","postgresnio","records","sql","structured-queries","swift"],"latest_commit_sha":null,"homepage":"https://coenttb.com","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/coenttb.png","metadata":{"files":{"readme":"README.md","changelog":"HISTORY.md","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":"2025-08-15T03:48:46.000Z","updated_at":"2025-10-30T18:18:17.000Z","dependencies_parsed_at":"2025-08-22T19:56:32.066Z","dependency_job_id":"1cdecb93-f708-4abd-b38d-dcd97a5be4ef","html_url":"https://github.com/coenttb/swift-records","commit_stats":null,"previous_names":["coenttb/swift-structured-queries-postgres"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/coenttb/swift-records","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/coenttb%2Fswift-records","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/coenttb%2Fswift-records/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/coenttb%2Fswift-records/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/coenttb%2Fswift-records/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/coenttb","download_url":"https://codeload.github.com/coenttb/swift-records/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/coenttb%2Fswift-records/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28657110,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-22T01:17:37.254Z","status":"online","status_checked_at":"2026-01-22T02:00:07.137Z","response_time":144,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":["nio","postgres","postgresnio","records","sql","structured-queries","swift"],"created_at":"2026-01-22T06:43:07.426Z","updated_at":"2026-01-22T06:43:07.554Z","avatar_url":"https://github.com/coenttb.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Swift Records\n\n[![CI](https://github.com/coenttb/swift-records/workflows/CI/badge.svg)](https://github.com/coenttb/swift-records/actions/workflows/ci.yml)\n![Development Status](https://img.shields.io/badge/status-active--development-blue.svg)\n\nA high-level, type-safe database abstraction layer for PostgreSQL in Swift, built on [StructuredQueries](https://github.com/pointfreeco/swift-structured-queries) and [PostgresNIO](https://github.com/vapor/postgres-nio), inspired by GRDB.\n\n## Features\n\n- **Connection Pooling**: Automatic connection lifecycle management with configurable pool sizes\n- **Transactions**: Full transaction support with isolation levels and savepoints\n- **Migrations**: Version-tracked schema migrations with automatic execution\n- **Full-Text Search**: Type-safe PostgreSQL full-text search with highlighting and ranking\n- **Testing Utilities**: Schema isolation for parallel test execution\n- **Type Safety**: Leverages Swift's type system and StructuredQueries for compile-time guarantees\n- **Actor-Based Concurrency**: Safe multi-threaded database access with Swift 6.0 concurrency\n- **Dependency Injection**: Seamless integration with Point-Free's Dependencies library\n\n## Installation\n\nAdd to your `Package.swift`:\n\n```swift\ndependencies: [\n    .package(url: \"https://github.com/coenttb/swift-records\", exact: \"0.0.1\")\n]\n\ntargets: [\n    .target(\n        name: \"YourTarget\",\n        dependencies: [\n            .product(name: \"Records\", package: \"swift-records\")\n        ]\n    ),\n    .testTarget(\n        name: \"YourTargetTests\",\n        dependencies: [\n            .product(name: \"RecordsTestSupport\", package: \"swift-records\")\n        ]\n    )\n]\n```\n\n**Requirements:**\n- Swift 6.0+\n- PostgreSQL 12+\n- macOS 10.15+ / iOS 13+ / tvOS 13+ / watchOS 6+\n\n## Versioning\n\n### Current Version: 0.0.1 (Experimental)\n\n**⚠️ Important**: This is experimental software. Breaking changes may occur in any version update until we reach 1.0.0. We strongly recommend pinning to exact versions in your Package.swift:\n\n```swift\n.package(url: \"https://github.com/coenttb/swift-records\", exact: \"0.0.1\")\n```\n\n### Version History\n\n- **0.0.1** (2024): Initial experimental release\n  - Complete database abstraction layer for PostgreSQL\n  - Actor-based architecture for safe concurrent access\n  - Built on swift-structured-queries-postgres and PostgresNIO\n\n### Roadmap to 1.0.0\n\nWe will continue with 0.x.x versions while the API evolves:\n- 0.0.x - Bug fixes and critical issues\n- 0.x.0 - New features and potential breaking changes\n- 1.0.0 - API stability achieved, production-ready\n\n## Quick Start\n\n### Basic Setup\n\n```swift\nimport Records\n\n// Define your model using @Table macro\n@Table(\"users\")\nstruct User {\n    let id: Int\n    let name: String\n    let email: String\n    let createdAt: Date\n}\n\n// Configure database at app startup\nimport Dependencies\n\n@main\nstruct MyApp {\n    static func main() async throws {\n        \n        let database = try await Database.Pool(\n            configuration: .init(\n                host: \"localhost\",\n                port: 5432,\n                database: \"myapp\",\n                username: \"postgres\",\n                password: \"password\"\n            ),\n            minConnections: 5,\n            maxConnections: 20\n        )\n        \n        try await prepareDependencies {\n            $0.defaultDatabase = database\n        }\n        \n        // Or use environment variables\n        let database = try await Database.Pool(\n            configuration: .fromEnvironment(),\n            minConnections: 5,\n            maxConnections: 20\n        )\n        try await prepareDependencies {\n            $0.defaultDatabase = database\n        }\n        \n        // Your app code here...\n    }\n}\n```\n\n### Query Operations\n\n```swift\nimport Dependencies\n\n// Access database via dependency injection\nstruct UserService {\n    @Dependency(\\.defaultDatabase) var db\n    \n    // Fetch all users\n    func fetchUsers() async throws -\u003e [User] {\n        try await db.read { db in\n            try await User.fetchAll(db)\n        }\n    }\n    \n    // Fetch with conditions\n    func fetchActiveUsers() async throws -\u003e [User] {\n        try await db.read { db in\n            try await User\n                .filter { $0.isActive }\n                .order(by: .descending(\\.createdAt))\n                .limit(10)\n                .fetchAll(db)\n        }\n    }\n    \n    // Insert new user\n    func createUser(name: String, email: String) async throws {\n        try await db.write { db in\n            try await User.insert {\n                User.Draft(\n                    name: name,\n                    email: email,\n                    createdAt: Date()\n                )\n            }.execute(db)\n        }\n    }\n    \n    // Update user\n    func updateUserName(email: String, newName: String) async throws {\n        try await db.write { db in\n            try await User\n                .filter { $0.email == email }\n                .update { $0.name = newName }\n                .execute(db)\n        }\n    }\n    \n    // Delete old users\n    func deleteOldUsers(olderThan date: Date) async throws {\n        try await db.write { db in\n            try await User\n                .filter { $0.createdAt \u003c date }\n                .delete()\n                .execute(db)\n        }\n    }\n}\n```\n\n### Transactions\n\n```swift\nstruct TransferService {\n    @Dependency(\\.defaultDatabase) var db\n    \n    // Basic transaction\n    func createUserWithProfile(name: String, email: String) async throws {\n        try await db.withTransaction { db in\n            let userId = try await User.insert {\n                User.Draft(\n                    name: name,\n                    email: email,\n                    createdAt: Date()\n                )\n            }\n            .returning(\\.id)\n            .fetchOne(db)\n            \n            try await Profile.insert {\n                Profile.Draft(\n                    userId: userId!,\n                    bio: \"New user\"\n                )\n            }.execute(db)\n            // Both succeed or both are rolled back\n        }\n    }\n    \n    // Transaction with isolation level\n    func transferFunds(from: Int, to: Int, amount: Decimal) async throws {\n        try await db.withTransaction(isolation: .serializable) { db in\n            // Your transactional operations\n        }\n    }\n}\n\n// Savepoints for nested transactions\ntry await db.withTransaction { db in\n    try await User.insert { ... }.execute(db)\n    \n    do {\n        try await db.withSavepoint(\"risky_operation\") { db in\n            try await riskyOperation(db)\n        }\n    } catch {\n        // Only the savepoint is rolled back\n        print(\"Risky operation failed: \\\\(error)\")\n    }\n    \n    try await Post.insert { ... }.execute(db)\n}\n```\n\n### Migrations\n\nSwift Records uses a forward-only migration system - migrations can only be applied, not rolled back. This design choice prioritizes simplicity and safety over reversibility.\n\n```swift\nimport Records\n\n// Define your migrations\nvar migrator = Database.Migrator()\n\n// Register migrations in order\nmigrator.registerMigration(\"create_users\") { db in\n    try await db.execute(\"\"\"\n        CREATE TABLE users (\n            id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n            email TEXT UNIQUE NOT NULL,\n            name TEXT NOT NULL,\n            created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n        )\n    \"\"\")\n}\n\nmigrator.registerMigration(\"add_user_status\") { db in\n    try await db.execute(\"\"\"\n        ALTER TABLE users \n        ADD COLUMN status TEXT NOT NULL DEFAULT 'active'\n    \"\"\")\n}\n\nmigrator.registerMigration(\"create_posts\") { db in\n    try await db.execute(\"\"\"\n        CREATE TABLE posts (\n            id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n            user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n            title TEXT NOT NULL,\n            content TEXT,\n            published_at TIMESTAMPTZ\n        )\n    \"\"\")\n    \n    try await db.execute(\"\"\"\n        CREATE INDEX idx_posts_user_id ON posts(user_id)\n    \"\"\")\n}\n\n// Apply migrations at startup\n@main\nstruct MyApp {\n    static func main() async throws {\n        let db = try await Database.Pool(\n            configuration: .fromEnvironment(),\n            minConnections: 5,\n            maxConnections: 20\n        )\n        \n        // Run pending migrations\n        try await db.write { db in\n            try await migrator.migrate(db)\n        }\n        \n        prepareDependencies {\n            $0.defaultDatabase = db\n        }\n        \n        // Your app code...\n    }\n}\n```\n\n#### Why Forward-Only?\n\nSwift Records deliberately omits rollback functionality for migrations:\n\n1. **Production Safety**: Rollbacks risk data loss and are rarely safe in production\n2. **Simplicity**: Single migration path reduces complexity and potential for errors\n3. **Modern Practice**: Aligns with immutable infrastructure and forward-fix strategies\n4. **Real-World Usage**: Teams typically fix issues with new migrations, not rollbacks\n\n#### Why Pure SQL?\n\nMigrations use raw SQL strings rather than Swift model references because migrations must remain immutable historical records. Using type-safe references (like `User.table` or field names) would break when models evolve - if you rename `User` to `Account` or change field names, old migrations would fail. Pure SQL ensures migrations can always recreate the exact database schema progression, regardless of how your Swift code changes.\n\nFor development iteration, use `eraseDatabaseOnSchemaChange`:\n\n```swift\n// Development configuration\ntry await migrator.migrate(\n    db,\n    options: .init(eraseDatabaseOnSchemaChange: true)\n)\n```\n\nThis approach keeps production migrations safe and predictable while providing flexibility during development.\n\n## Testing\n\nThe `RecordsTestSupport` module provides utilities for testing with automatic schema isolation:\n\n```swift\nimport Testing\nimport Records\nimport RecordsTestSupport\nimport Dependencies\n\n@Suite(\"User Tests\", .dependency(\\.defaultDatabase, Database.TestDatabase.withSchema()))\nstruct UserTests {\n    @Dependency(\\.defaultDatabase) var db\n    \n    @Test func createUser() async throws {\n        // Each test runs in its own schema, enabling parallel execution\n        try await db.withRollback { db in\n            let user = try await User.insert {\n                User.Draft(\n                    name: \"Test User\",\n                    email: \"test@example.com\"\n                )\n            }\n            .returning(\\\\.self)\n            .execute(db)\n            \n            #expect(user.first?.name == \"Test User\")\n        }\n    }\n}\n```\n\n### Test Database Configuration\n\nTests use environment variables for database configuration:\n\n```bash\nexport DATABASE_HOST=localhost\nexport DATABASE_PORT=5432\nexport DATABASE_NAME=test_db\nexport DATABASE_USER=postgres\nexport DATABASE_PASSWORD=password\n```\n\nOr create a `.env` file in your test directory:\n\n```env\nDATABASE_HOST=localhost\nDATABASE_PORT=5432\nDATABASE_NAME=test_db\nDATABASE_USER=postgres\nDATABASE_PASSWORD=password\n```\n\n## Architecture\n\nSwift Records provides a layered architecture:\n\n1. **Database Layer**: Top-level coordinator with `Reader` and `Writer` actors\n2. **Connection Management**: Automatic pooling with configurable min/max connections\n3. **Query Execution**: Type-safe query building via StructuredQueries\n4. **PostgreSQL Bridge**: Low-level utilities from swift-structured-queries-postgres\n\n### Connection Pooling\n\nThe connection pool automatically manages connection lifecycle:\n\n- Maintains minimum connections for quick response\n- Scales up to maximum under load\n- Validates connections before reuse\n- Handles connection failures gracefully\n\n### Concurrency Safety\n\nUsing Swift 6.0's actor model ensures thread-safe database access:\n\n- `Database.Reader`: Read-only operations (can use multiple connections)\n- `Database.Writer`: Write operations (ensures serialization when needed)\n\n### Connection Lifecycle Management\n\nProperly manage database connections in your application lifecycle:\n\n```swift\n// For Vapor applications\nimport Vapor\nimport Records\n\nstruct DatabaseLifecycleHandler: LifecycleHandler {\n    let database: any Database.Reader\n    \n    func shutdown(_ app: Application) {\n        app.eventLoopGroup.next().execute {\n            Task {\n                try? await database.close()\n            }\n        }\n    }\n}\n\n// In your configure function\nfunc configure(_ app: Application) async throws {\n    let db = try await Database.Pool(\n        configuration: .fromEnvironment(),\n        minConnections: 5,\n        maxConnections: 20\n    )\n    \n    prepareDependencies {\n        $0.defaultDatabase = db\n    }\n    \n    app.lifecycle.use(DatabaseLifecycleHandler(database: db))\n}\n```\n\n### Error Recovery Strategies\n\n```swift\n// Retry logic for transient failures\nfunc withRetry\u003cT\u003e(\n    maxAttempts: Int = 3,\n    operation: () async throws -\u003e T\n) async throws -\u003e T {\n    var lastError: Error?\n    \n    for attempt in 1...maxAttempts {\n        do {\n            return try await operation()\n        } catch Database.Error.connectionTimeout {\n            lastError = error\n            if attempt \u003c maxAttempts {\n                // Exponential backoff\n                try await Task.sleep(nanoseconds: UInt64(attempt * 1_000_000_000))\n            }\n        } catch {\n            throw error\n        }\n    }\n    \n    throw lastError!\n}\n\n// Usage\nlet users = try await withRetry {\n    try await db.read { db in\n        try await User.fetchAll(db)\n    }\n}\n```\n\n## Advanced Usage\n\n### Custom Query Types\n\n```swift\n@Selection\nstruct UserWithPosts {\n    let userId: Int\n    let userName: String\n    let postCount: Int\n}\n\nlet results = try await db.reader.read { db in\n    try await User\n        .join(Post.all) { $0.id.eq($1.userId) }\n        .group(by: { user, _ in user.id })\n        .select { user, post in\n            UserWithPosts.Columns(\n                userId: user.id,\n                userName: user.name,\n                postCount: post.id.count()\n            )\n        }\n        .fetchAll(db)\n}\n```\n\n### Raw SQL\n\nWhen needed, you can execute raw SQL:\n\n```swift\nstruct MaintenanceService {\n    @Dependency(\\.defaultDatabase) var db\n    \n    func createEmailIndex() async throws {\n        try await db.write { db in\n            try await db.execute(\"\"\"\n                CREATE INDEX CONCURRENTLY idx_users_email \n                ON users(email)\n            \"\"\")\n        }\n    }\n    \n    func vacuumDatabase() async throws {\n        try await db.write { db in\n            try await db.execute(\"VACUUM ANALYZE\")\n        }\n    }\n}\n```\n\n## Full-Text Search\n\nSwift Records provides first-class support for PostgreSQL's powerful full-text search capabilities through an elegant type-safe DSL. Built on top of PostgreSQL's `tsvector` and `tsquery` types, you can add sophisticated search functionality to your application with just a few lines of code.\n\n\u003e **📐 Architecture Note**: PostgreSQL full-text search uses dedicated `tsvector` columns within regular tables, unlike SQLite's virtual table approach. This necessitates the `searchVectorColumn` protocol requirement to specify which column to search.\n\u003e\n\u003e **Default behavior**: Most tables can use the default `\"search_vector\"` column name without any configuration—just conform to `FullTextSearchable` and you're done. See the [Full-Text Search Guide](Sources/Records/Documentation.docc/FullTextSearch.md#Understanding-searchVectorColumn) for architectural details.\n\n### Quick Start\n\n```swift\nimport Records\nimport StructuredQueriesPostgres\n\n// 1. Make your model searchable\n@Table\nstruct Article: FullTextSearchable {\n    let id: Int\n    var title: String\n    var body: String\n    var author: String\n\n    // Specify the tsvector column name (defaults to \"search_vector\")\n    static var searchVectorColumn: String { \"search_vector\" }\n}\n\n// 2. Set up full-text search in a migration\nmigrator.registerMigration(\"add_articles_fts\") { db in\n    // Add tsvector column\n    try await db.execute(\"\"\"\n        ALTER TABLE articles\n        ADD COLUMN search_vector tsvector\n    \"\"\")\n\n    // Create GIN index for fast searches\n    try await db.execute(\"\"\"\n        CREATE INDEX articles_search_idx\n        ON articles\n        USING GIN (search_vector)\n    \"\"\")\n\n    // Create trigger to automatically update search vector\n    try await db.execute(\"\"\"\n        CREATE OR REPLACE FUNCTION articles_search_trigger() RETURNS trigger AS $$\n        BEGIN\n          NEW.search_vector :=\n            setweight(to_tsvector('english', coalesce(NEW.title, '')), 'A') ||\n            setweight(to_tsvector('english', coalesce(NEW.body, '')), 'B') ||\n            setweight(to_tsvector('english', coalesce(NEW.author, '')), 'C');\n          RETURN NEW;\n        END\n        $$ LANGUAGE plpgsql\n    \"\"\")\n\n    try await db.execute(\"\"\"\n        CREATE TRIGGER articles_search_update\n        BEFORE INSERT OR UPDATE ON articles\n        FOR EACH ROW EXECUTE FUNCTION articles_search_trigger()\n    \"\"\")\n\n    // Backfill existing data\n    try await db.execute(\"\"\"\n        UPDATE articles SET search_vector =\n          setweight(to_tsvector('english', coalesce(title, '')), 'A') ||\n          setweight(to_tsvector('english', coalesce(body, '')), 'B') ||\n          setweight(to_tsvector('english', coalesce(author, '')), 'C')\n    \"\"\")\n}\n\n// 3. Search your content\nstruct SearchService {\n    @Dependency(\\.defaultDatabase) var db\n\n    func searchArticles(query: String) async throws -\u003e [Article] {\n        try await db.read { db in\n            try await Article\n                .where { $0.match(query) }\n                .order { $0.rank(query) }\n                .fetchAll(db)\n        }\n    }\n}\n```\n\n\u003e **📚 For comprehensive documentation**, see the [Full-Text Search Guide](Sources/Records/Documentation.docc/FullTextSearch.md) including:\n\u003e - [Why searchVectorColumn is required](Sources/Records/Documentation.docc/FullTextSearch.md#Understanding-searchVectorColumn)\n\u003e - [PostgreSQL vs SQLite comparison](Sources/Records/Documentation.docc/FullTextSearch.md#Understanding-searchVectorColumn)\n\u003e - [Multi-language support](Sources/Records/Documentation.docc/FullTextSearch.md#Multi-Language-Support)\n\u003e - [Performance tuning](Sources/Records/Documentation.docc/FullTextSearch.md#Performance-Considerations)\n\u003e - [Complete examples](Sources/Records/Documentation.docc/FullTextSearch.md#Complete-Example)\n\n### Search Methods\n\nSwift Records provides multiple search methods for different use cases:\n\n#### Basic Search (`match`)\n\nUses PostgreSQL's `to_tsquery()` for powerful boolean searches:\n\n```swift\n// Single term\nArticle.where { $0.match(\"Swift\") }\n\n// Boolean AND - both terms must match\nArticle.where { $0.match(\"Swift \u0026 PostgreSQL\") }\n\n// Boolean OR - either term can match\nArticle.where { $0.match(\"Swift | Rust\") }\n\n// Negation - must NOT contain term\nArticle.where { $0.match(\"Swift \u0026 !Objective-C\") }\n\n// Phrase search with adjacency\nArticle.where { $0.match(\"quick \u003c-\u003e brown\") }  // Words must be adjacent\n```\n\n#### Plain Text Search (`plainMatch`)\n\nSafe for user input - treats all words as AND-connected terms:\n\n```swift\n// User enters: \"swift postgresql database\"\n// Automatically becomes: swift \u0026 postgresql \u0026 database\nArticle.where { $0.plainMatch(userInput) }\n```\n\n#### Web Search Syntax (`webMatch`)\n\nGoogle-like search syntax for end users:\n\n```swift\n// Quoted phrases\nArticle.where { $0.webMatch(#\"\"swift postgresql\" database\"#) }\n\n// Exclusions with minus\nArticle.where { $0.webMatch(\"swift -objective-c\") }\n\n// OR operator\nArticle.where { $0.webMatch(\"Swift OR Rust\") }\n```\n\n#### Phrase Search (`phraseMatch`)\n\nExact phrase matching where words must appear in order:\n\n```swift\n// Finds \"San Francisco\" but not \"Francisco's San Diego trip\"\nArticle.where { $0.phraseMatch(\"San Francisco\") }\n```\n\n### Ranking Results\n\nOrder search results by relevance:\n\n```swift\n// Basic relevance ranking\nArticle\n    .where { $0.match(\"Swift\") }\n    .order { $0.rank(\"Swift\") }\n    .fetchAll(db)\n\n// Weighted ranking - prioritize title matches over body\nArticle\n    .where { $0.match(\"Swift\") }\n    .order {\n        $0.rank(\n            \"Swift\",\n            weights: [0.1, 0.2, 0.4, 1.0]  // [D, C, B, A]\n        )\n    }\n    .fetchAll(db)\n\n// Coverage-based ranking (better for phrase searches)\nArticle\n    .where { $0.match(\"database indexing\") }\n    .order { $0.rankCoverage(\"database indexing\") }\n    .fetchAll(db)\n```\n\n**Weight Labels:**\n- `A` - Highest importance (typically titles)\n- `B` - High importance (typically subtitles, emphasized text)\n- `C` - Medium importance (typically metadata, tags)\n- `D` - Lowest importance (typically body text)\n\n### Highlighting Search Results\n\nShow users exactly where matches appear:\n\n```swift\n// Highlight matches in search results\nlet results = try await db.read { db in\n    try await Article\n        .where { $0.match(\"Swift\") }\n        .select {\n            (\n                $0.title,\n                $0.body.tsHeadline(\n                    \"Swift\",\n                    startSel: \"\u003cmark\u003e\",\n                    stopSel: \"\u003c/mark\u003e\",\n                    maxWords: 50\n                )\n            )\n        }\n        .fetchAll(db)\n}\n\n// Returns: (\"Swift Concurrency Guide\", \"Modern async/await patterns in \u003cmark\u003eSwift\u003c/mark\u003e programming...\")\n```\n\n### Column-Specific Search\n\nSearch within specific columns:\n\n```swift\n// Ad-hoc search without pre-computed tsvector\nArticle.where { $0.title.matchText(\"Swift\") }\n\n// Only searches the title column\n```\n\n### Search Configuration\n\nPostgreSQL supports multiple languages for stemming and stop words:\n\n```swift\n// English (default)\nArticle.where { $0.match(\"running\", language: \"english\") }\n// Matches: run, runs, running, ran\n\n// Simple (no stemming)\nArticle.where { $0.match(\"running\", language: \"simple\") }\n// Matches: only \"running\" exactly\n\n// Other languages\nArticle.where { $0.match(\"courir\", language: \"french\") }\nArticle.where { $0.match(\"laufen\", language: \"german\") }\n```\n\n### Multi-Column Weighting\n\nWeight different columns differently in your search vector:\n\n```swift\n// Title has highest weight (A), body medium (B), tags low (C)\nCREATE TRIGGER product_search_update\nBEFORE INSERT OR UPDATE ON products\nFOR EACH ROW EXECUTE FUNCTION products_search_trigger()\n\nCREATE OR REPLACE FUNCTION products_search_trigger() RETURNS trigger AS $$\nBEGIN\n  NEW.search_vector :=\n    setweight(to_tsvector('english', coalesce(NEW.name, '')), 'A') ||\n    setweight(to_tsvector('english', coalesce(NEW.description, '')), 'B') ||\n    setweight(to_tsvector('english', coalesce(NEW.tags, '')), 'C');\n  RETURN NEW;\nEND\n$$ LANGUAGE plpgsql\n```\n\n### Performance Considerations\n\n1. **Always use GIN indexes** for tsvector columns:\n   ```sql\n   CREATE INDEX articles_search_idx ON articles USING GIN (search_vector);\n   ```\n\n2. **Update search vectors automatically** with triggers to keep them in sync\n\n3. **Use appropriate search method**:\n   - `match()` - Most powerful but requires valid tsquery syntax\n   - `plainMatch()` - Safest for user input\n   - `webMatch()` - Best UX for end users\n\n4. **Consider normalization** for ranking:\n   ```swift\n   Article.order { $0.rank(\"query\", normalization: 1) }\n   // 1 = divide by (1 + log(length)) - favors longer documents less\n   ```\n\n### Complete Search Example\n\n```swift\nstruct ArticleSearchService {\n    @Dependency(\\.defaultDatabase) var db\n\n    struct SearchResult {\n        let article: Article\n        let headline: String\n        let rank: Double\n    }\n\n    func search(query: String, limit: Int = 20) async throws -\u003e [SearchResult] {\n        // Sanitize user input with plainMatch for safety\n        let results = try await db.read { db in\n            try await Article\n                .where { $0.plainMatch(query) }\n                .select {\n                    (\n                        $0,  // Full article\n                        $0.body.tsHeadline(\n                            query,\n                            startSel: \"\u003cmark\u003e\",\n                            stopSel: \"\u003c/mark\u003e\",\n                            maxWords: 50\n                        ),\n                        $0.rank(query, weights: [0.1, 0.2, 0.4, 1.0])\n                    )\n                }\n                .order { $0.rank(query, weights: [0.1, 0.2, 0.4, 1.0]) }\n                .limit(limit)\n                .fetchAll(db)\n        }\n\n        return results.map { article, headline, rank in\n            SearchResult(article: article, headline: headline, rank: rank)\n        }\n    }\n}\n```\n\n### PostgreSQL Full-Text Search Resources\n\n- [PostgreSQL Full-Text Search Documentation](https://www.postgresql.org/docs/current/textsearch.html)\n- [Text Search Functions](https://www.postgresql.org/docs/current/functions-textsearch.html)\n- [GIN Indexes](https://www.postgresql.org/docs/current/textsearch-indexes.html)\n\n## Development Documentation\n\nFor contributors and those interested in the package's development history:\n\n- **[Development History](docs/DEVELOPMENT_HISTORY.md)** - Journey from initial implementation to 94 passing tests\n  - Phase 1: Test cleanup and package boundary establishment\n  - Phase 2: Reminder schema implementation (upstream alignment)\n  - Phase 3: Package deduplication (removing ~750 lines of duplicate code)\n  - Phase 4: PostgreSQL-specific test fixes (sequences, DATE types)\n\n- **[Testing Architecture](docs/TESTING_ARCHITECTURE.md)** - Comprehensive testing patterns and solutions\n  - Upstream patterns analysis (sqlite-data, swift-structured-queries)\n  - PostgreSQL vs SQLite differences\n  - Evolution of testing approaches\n  - Final solution: Direct database creation for parallel test execution\n  - Best practices and troubleshooting\n\n## Related Packages\n\n### Dependencies\n\n- [swift-environment-variables](https://github.com/coenttb/swift-environment-variables): A Swift package for type-safe environment variable management.\n\n### Used By\n\n- [coenttb-newsletter](https://github.com/coenttb/coenttb-newsletter): A Swift package for newsletter subscription and email management.\n- [swift-identities](https://github.com/coenttb/swift-identities): The Swift library for identity authentication and management.\n\n### Third-Party Dependencies\n\n- [pointfreeco/swift-dependencies](https://github.com/pointfreeco/swift-dependencies): A dependency management library for controlling dependencies in Swift.\n- [pointfreeco/swift-snapshot-testing](https://github.com/pointfreeco/swift-snapshot-testing): Delightful snapshot testing for Swift.\n- [pointfreeco/xctest-dynamic-overlay](https://github.com/pointfreeco/xctest-dynamic-overlay): Define XCTest assertion helpers directly in production code.\n- [vapor/postgres-nio](https://github.com/vapor/postgres-nio): Non-blocking, event-driven Swift client for PostgreSQL.\n\n## Dependencies\n\nThis package builds on excellent work from:\n- [StructuredQueries](https://github.com/pointfreeco/swift-structured-queries) - Type-safe SQL generation\n- [PostgresNIO](https://github.com/vapor/postgres-nio) - PostgreSQL driver\n- [swift-dependencies](https://github.com/pointfreeco/swift-dependencies) - Dependency injection\n\n## License\n\nThis project is licensed under the Apache 2.0 License - see the [LICENSE](LICENSE) file for details.\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n## Acknowledgments\n\n- [Point-Free](https://www.pointfree.co) for StructuredQueries and Dependencies\n- The [Vapor](https://vapor.codes) team for PostgresNIO\n- [GRDB](https://github.com/groue/GRDB.swift) for API design inspiration \n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcoenttb%2Fswift-records","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcoenttb%2Fswift-records","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcoenttb%2Fswift-records/lists"}