{"id":15060960,"url":"https://github.com/jordanmarr/sqlhydra","last_synced_at":"2026-02-23T19:16:19.552Z","repository":{"id":45646827,"uuid":"357745680","full_name":"JordanMarr/SqlHydra","owner":"JordanMarr","description":"SqlHydra is a suite of NuGet packages for working with databases in F# including code generation tools and query expressions.","archived":false,"fork":false,"pushed_at":"2026-01-30T17:01:03.000Z","size":28141,"stargazers_count":256,"open_issues_count":6,"forks_count":28,"subscribers_count":6,"default_branch":"main","last_synced_at":"2026-01-30T17:50:46.471Z","etag":null,"topics":["fsharp","orm","typeprovider"],"latest_commit_sha":null,"homepage":"","language":"F#","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/JordanMarr.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","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},"funding":{"github":"JordanMarr"}},"created_at":"2021-04-14T02:12:17.000Z","updated_at":"2026-01-24T14:32:21.000Z","dependencies_parsed_at":"2026-01-31T02:00:35.950Z","dependency_job_id":null,"html_url":"https://github.com/JordanMarr/SqlHydra","commit_stats":{"total_commits":857,"total_committers":13,"mean_commits":65.92307692307692,"dds":"0.40606767794632437","last_synced_commit":"c3a4f2934c5c53b3ecae29225ccad4f6b1c4cbb2"},"previous_names":[],"tags_count":49,"template":false,"template_full_name":null,"purl":"pkg:github/JordanMarr/SqlHydra","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JordanMarr%2FSqlHydra","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JordanMarr%2FSqlHydra/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JordanMarr%2FSqlHydra/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JordanMarr%2FSqlHydra/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/JordanMarr","download_url":"https://codeload.github.com/JordanMarr/SqlHydra/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JordanMarr%2FSqlHydra/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28926630,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-30T22:32:35.345Z","status":"online","status_checked_at":"2026-01-31T02:00:09.179Z","response_time":128,"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":["fsharp","orm","typeprovider"],"created_at":"2024-09-24T23:07:23.152Z","updated_at":"2026-02-23T19:16:19.544Z","avatar_url":"https://github.com/JordanMarr.png","language":"F#","funding_links":["https://github.com/sponsors/JordanMarr"],"categories":[],"sub_categories":[],"readme":"# SqlHydra\n\nType-safe SQL generation for F#. Generate types from your database, query with strongly-typed computation expressions.\n\n[![SqlHydra.Cli NuGet](https://img.shields.io/nuget/v/SqlHydra.Cli.svg?style=flat-square\u0026label=SqlHydra.Cli)](https://www.nuget.org/packages/SqlHydra.Cli/)\n[![SqlHydra.Query NuGet](https://img.shields.io/nuget/v/SqlHydra.Query.svg?style=flat-square\u0026label=SqlHydra.Query)](https://www.nuget.org/packages/SqlHydra.Query/)\n\n**Supported Databases:** SQL Server | PostgreSQL | SQLite | Oracle | MySQL\n\n---\n\n## Quick Start\n\n**1. Install the CLI tool locally:**\n```bash\ndotnet new tool-manifest\ndotnet tool install SqlHydra.Cli\n```\n\n**2. Generate types from your database:**\n```bash\ndotnet sqlhydra mssql    # or: npgsql, sqlite, oracle, mysql\n```\nThe wizard will prompt you for **connection string**, **output file**, and **namespace**.\n\n**3. Install the query library:**\n```bash\ndotnet add package SqlHydra.Query\n```\n\n**4. Configure Query Context:**\n\nSqlHydra.Cli now generates a DB‑specific `QueryContextFactory` for each generated database (perfect for DI injection). \n\nUse it to create a strongly‑typed query context:\n```fsharp\nlet db = AdventureWorks.QueryContextFactory.Create(connStr, printfn \"SQL: %O\") // Optional SQL output logging\n```\n\n**5. Write your first query:**\n\n```fsharp\nopen SqlHydra.Query\nopen AdventureWorks\n\n// Query with full type safety\nlet getProducts minPrice =\n    selectTask db {\n        for p in SalesLT.Product do\n        where (p.ListPrice \u003e minPrice)\n        orderBy p.Name\n        select p\n    }\n```\n\n\u003e **Note:** All query builders have both `Task` and `Async` variants: `selectTask`/`selectAsync`, `insertTask`/`insertAsync`, `updateTask`/`updateAsync`, `deleteTask`/`deleteAsync`.\n\nThat's it! Your queries are now type-checked at compile time.\n\n---\n\n## What Gets Generated?\n\nSqlHydra.Cli reads your database schema and generates:\n\n- **F# record types** for each table (with Option types for nullable columns)\n- **Table declarations** for use in queries\n- **HydraReader** for efficiently reading query results\n\n```fsharp\n// Generated from your database schema:\nmodule SalesLT =\n    type Product =\n        { ProductID: int\n          Name: string\n          ListPrice: decimal\n          Color: string option }  // nullable columns become Option\n\n    let Product = table\u003cProduct\u003e  // table declaration for queries\n```\n\n---\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ch2\u003eSqlHydra.Cli Reference\u003c/h2\u003e\u003c/summary\u003e\n\n### Installation\n\n**Local Install (recommended):**\n```bash\ndotnet new tool-manifest\ndotnet tool install SqlHydra.Cli\n```\n\n### Running the CLI\n\n```bash\ndotnet sqlhydra mssql     # SQL Server\ndotnet sqlhydra npgsql    # PostgreSQL\ndotnet sqlhydra sqlite    # SQLite\ndotnet sqlhydra oracle    # Oracle\ndotnet sqlhydra mysql     # MySQL\n```\n\n- If no `.toml` config exists, a wizard will guide you through setup\n- If a `.toml` config exists, it regenerates code using that config\n- Generated `.fs` files are automatically added to your `.fsproj` as `Visible=\"false\"`\n\n### Configuration Wizard\n\nThe wizard prompts for:\n\n1. **Connection String** - Used to query your database schema\n2. **Output Filename** - e.g., `AdventureWorks.fs`\n3. **Namespace** - e.g., `MyApp.AdventureWorks`\n4. **Use Case:**\n   - **SqlHydra.Query integration** (default) - Generates everything needed for SqlHydra.Query\n   - **Other data library** - Just the record types (for Dapper.FSharp, Donald, etc.)\n   - **Standalone** - Record types + HydraReader (no SqlHydra.Query metadata)\n\nFor advanced configuration, see the [TOML Configuration Reference](https://github.com/JordanMarr/SqlHydra/wiki/TOML-Configuration).\n\n### Auto-Regeneration (Build Event)\n\nTo regenerate on Rebuild in Debug mode:\n\n```xml\n\u003cTarget Name=\"SqlHydra\" BeforeTargets=\"Clean\" Condition=\"'$(Configuration)' == 'Debug'\"\u003e\n  \u003cExec Command=\"dotnet sqlhydra mssql\" /\u003e\n\u003c/Target\u003e\n```\n\n### Multiple TOML Files\n\nYou can have multiple `.toml` files for different scenarios:\n\n```bash\ndotnet sqlhydra sqlite -t \"shared.toml\"\ndotnet sqlhydra mssql -t \"reporting.toml\"\n```\n\nUseful for data migrations or generating types with different filters.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ch2\u003eSelect Queries\u003c/h2\u003e\u003c/summary\u003e\n\n### Basic Select\n\n```fsharp\nlet getProducts (db: QueryContextFactory)  =\n    selectTask db {\n        for p in SalesLT.Product do\n        select p\n    }\n```\n\n### Where Clauses\n\n```fsharp\nlet getExpensiveProducts (db: QueryContextFactory) minPrice =\n    selectTask db {\n        for p in SalesLT.Product do\n        where (p.ListPrice \u003e minPrice)\n        select p\n    }\n```\n\n**Where operators:**\n| Operator | Function | Description |\n|----------|----------|-------------|\n| `\\|=\\|` | `isIn` | Column IN list |\n| `\\|\u003c\u003e\\|` | `isNotIn` | Column NOT IN list |\n| `=%` | `like` | LIKE pattern |\n| `\u003c\u003e%` | `notLike` | NOT LIKE pattern |\n| `= None` | `isNullValue` | IS NULL |\n| `\u003c\u003e None` | `isNotNullValue` | IS NOT NULL |\n\n```fsharp\n// Filter where City starts with 'S'\nlet getCitiesStartingWithS (db: QueryContextFactory)  =\n    selectTask db {\n        for a in SalesLT.Address do\n        where (a.City =% \"S%\")\n        select a\n    }\n```\n\n### Conditional Where (v3.0+)\n\nUse `\u0026\u0026` to conditionally include/exclude where clauses:\n\n```fsharp\nlet getAddresses (db: QueryContextFactory) (cityFilter: string option) (zipFilter: string option) =\n    selectTask db {\n        for a in Person.Address do\n        where (\n            (cityFilter.IsSome \u0026\u0026 a.City = cityFilter.Value) \u0026\u0026\n            (zipFilter.IsSome \u0026\u0026 a.PostalCode = zipFilter.Value)\n        )\n    }\n```\n\nIf `cityFilter.IsSome` is `false`, that clause is excluded from the query.\n\n### Joins\n\n```fsharp\n// Inner join\nlet getProductsWithCategory (db: QueryContextFactory)  =\n    selectTask db {\n        for p in SalesLT.Product do\n        join c in SalesLT.ProductCategory on (p.ProductCategoryID.Value = c.ProductCategoryID)\n        select (p, c.Name)\n        take 10\n    }\n\n// Left join (joined table becomes Option).\n// You can use `|\u003e Option.map` to select specifc left joined columns.\nlet getCustomerAddresses (db: QueryContextFactory)  =\n    selectTask db {\n        for c in SalesLT.Customer do\n        leftJoin a in SalesLT.Address on (c.AddressID = a.Value.AddressID)\n        select (\n            c.Email, \n            a |\u003e Option.map _.State\n        ) into selected\n        mapList (\n            let email, stateMaybe = selected\n            let state = stateMaybe |\u003e Option.defaultValue \"N/A\"\n            $\"Customer: {email}, State: {state}\"\n        )\n    }\n\n\n// Improved join syntax with `join'` and `leftJoin'` lets you use full predicates in `on'` clauses.\n// * Makes multi-column joins much cleaner (no need for tuple comparison).\n// * Allows full predicates (e.g., AND/OR) in join conditions.\n// * Optional cheeky usage of `;` if you want `on'` on the same line!\nselectTask db {\n    for o in Sales.SalesOrderHeader do\n    join' d in Sales.SalesOrderDetail; on' (o.ID = d.OrderID \u0026\u0026 o.Status = \"Completed\")\n    select o\n}\n\n```\n\n\u003e **Note:** In join `on` clauses, put the known (left) table on the left side of the `=`.\n\n### Selecting Columns\n\n```fsharp\n// Select specific columns\nlet getCityStates (db: QueryContextFactory)  =\n    selectTask db {\n        for a in SalesLT.Address do\n        select (a.City, a.StateProvince)\n    }\n\n// Transform results with mapList\nlet getCityLabels (db: QueryContextFactory)  =\n    selectTask db {\n        for a in SalesLT.Address do\n        select (a.City, a.StateProvince) into (city, state)\n        mapList $\"City: {city}, State: {state}\"\n    }\n```\n\n### Aggregates\n\n```fsharp\nlet getCategoriesWithHighPrices (db: QueryContextFactory)  =\n    selectTask db {\n        for p in SalesLT.Product do\n        where (p.ProductCategoryID \u003c\u003e None)\n        groupBy p.ProductCategoryID\n        having (avgBy p.ListPrice \u003e 500M)\n        select (p.ProductCategoryID, avgBy p.ListPrice)\n    }\n\n// Count\nlet getCustomerCount (db: QueryContextFactory)  =\n    selectTask db {\n        for c in SalesLT.Customer do\n        count\n    }\n```\n\n**Aggregate functions:** `countBy`, `sumBy`, `minBy`, `maxBy`, `avgBy`\n\n\u003e **Warning:** If an aggregate might return NULL (e.g., `minBy` on an empty result set), wrap in `Some`:\n\u003e ```fsharp\n\u003e select (minBy (Some p.ListPrice))  // Returns Option\n\u003e ```\n\n### SQL Functions\n\nSqlHydra.Query includes built-in SQL functions for each supported database provider. These can be used in both `select` and `where` clauses.\n\n**Setup:**\n```fsharp\n// Import the extension module for your database provider:\nopen SqlHydra.Query.SqlServerExtensions  // SQL Server\nopen SqlHydra.Query.NpgsqlExtensions     // PostgreSQL\nopen SqlHydra.Query.SqliteExtensions     // SQLite\nopen SqlHydra.Query.OracleExtensions     // Oracle\nopen SqlHydra.Query.MySqlExtensions      // MySQL\n\nopen type SqlFn  // Optional: allows unqualified access, e.g. LEN vs SqlFn.LEN\n```\n\n**Use in select and where clauses:**\n```fsharp\n// String functions\nselectTask db {\n    for p in Person.Person do\n    where (LEN(p.FirstName) \u003e 3)\n    select (p.FirstName, LEN(p.FirstName), UPPER(p.FirstName))\n}\n// Generates: SELECT ... WHERE LEN([p].[FirstName]) \u003e 3\n\n// Null handling - ISNULL accepts Option\u003c'T\u003e and returns unwrapped 'T\nselectTask db {\n    for p in Person.Person do\n    select (ISNULL(p.MiddleName, \"N/A\"))  // Option\u003cstring\u003e -\u003e string\n}\n\n// Date functions\nselectTask db {\n    for o in Sales.SalesOrderHeader do\n    where (YEAR(o.OrderDate) = 2024)\n    select (o.OrderDate, YEAR(o.OrderDate), MONTH(o.OrderDate))\n}\n\n// Compare two functions\nselectTask db {\n    for p in Person.Person do\n    where (LEN(p.FirstName) \u003c LEN(p.LastName))\n    select (p.FirstName, p.LastName)\n}\n```\n\n**Built-in functions** include string functions (`LEN`, `UPPER`, `SUBSTRING`, etc.), null handling (`ISNULL`/`COALESCE` with overloads for `Option\u003c'T\u003e` and `Nullable\u003c'T\u003e`), numeric functions (`ABS`, `ROUND`, etc.), and date/time functions (`GETDATE`, `YEAR`, `MONTH`, etc.).\n\nSee the full list for each provider:\n- [SQL Server](src/SqlHydra.Query/SqlServerExtensions.fs)\n- [PostgreSQL](src/SqlHydra.Query/NpgsqlExtensions.fs)\n- [SQLite](src/SqlHydra.Query/SqliteExtensions.fs)\n- [Oracle](src/SqlHydra.Query/OracleExtensions.fs)\n- [MySQL](src/SqlHydra.Query/MySqlExtensions.fs)\n\n**Define custom functions:**\n\nYou can easily define your own SQL function wrappers using the `sqlFn` helper:\n```fsharp\n// Define a wrapper - the function name becomes the SQL function name\nlet SOUNDEX (s: string) : string = sqlFn\nlet DIFFERENCE (s1: string, s2: string) : int = sqlFn\n\n// Use in queries\nselectTask db {\n    for p in Person.Person do\n    where (SOUNDEX(p.LastName) = SOUNDEX(\"Smith\"))\n    select p.LastName\n}\n```\n\n\u003e **Note:** The `sqlFn` helper returns `Unchecked.defaultof\u003c'Return\u003e` - the function is never executed at runtime. The expression visitor translates the function name and arguments to SQL. If you use an invalid function name, you'll get a database error at runtime.\n\n### Subqueries\n\n```fsharp\n// Subquery returning multiple values\nlet top5Categories =\n    select {\n        for p in SalesLT.Product do\n        groupBy p.ProductCategoryID\n        orderByDescending (avgBy p.ListPrice)\n        select p.ProductCategoryID\n        take 5\n    }\n\nlet getTopCategoryNames (db: QueryContextFactory)  =\n    selectTask db {\n        for c in SalesLT.ProductCategory do\n        where (Some c.ProductCategoryID |=| subqueryMany top5Categories)\n        select c.Name\n    }\n\n// Subquery returning single value\nlet avgPrice =\n    select {\n        for p in SalesLT.Product do\n        select (avgBy p.ListPrice)\n    }\n\nlet getAboveAverageProducts (db: QueryContextFactory)  =\n    selectTask db {\n        for p in SalesLT.Product do\n        where (p.ListPrice \u003e subqueryOne avgPrice)\n        select p\n    }\n```\n\n### Other Operations\n\n```fsharp\n// Ordering\nselectTask db {\n    for p in SalesLT.Product do\n    orderBy p.Name\n    thenByDescending p.ListPrice\n    select p\n}\n\n// Conditional ordering with ^^\nlet getAddresses (db: QueryContextFactory) (sortByCity: bool) =\n    selectTask db {\n        for a in Person.Address do\n        orderBy (sortByCity ^^ a.City)\n        select a\n    }\n\n// Pagination\nselectTask db {\n    for p in SalesLT.Product do\n    skip 10\n    take 20\n    select p\n}\n\n// Distinct\nselectTask db {\n    for c in SalesLT.Customer do\n    select (c.FirstName, c.LastName)\n    distinct\n}\n\n// Get single/optional result\nselectTask db {\n    for p in SalesLT.Product do\n    where (p.ProductID = 123)\n    select p\n    tryHead  // Returns Option\n}\n```\n\n### Transforming Results (Important!)\n\nThe `select` clause only supports selecting columns/tables - **not** transformations like `.ToString()` or string interpolation.\n\n**Correct:** Transform in `mapList`/`mapArray`/`mapSeq`:\n```fsharp\nselectTask db {\n    for a in SalesLT.Address do\n    select (a.City, a.StateProvince) into (city, state)\n    mapList $\"City: {city}, State: {state}\"\n}\n```\n\n**Incorrect:** Transforming in `select` throws at runtime:\n```fsharp\n// DON'T DO THIS - will throw!\nselectTask db {\n    for a in SalesLT.Address do\n    select ($\"City: {a.City}\")\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ch2\u003eInsert, Update, Delete\u003c/h2\u003e\u003c/summary\u003e\n\n### Insert\n\n```fsharp\n// Simple insert\nlet! rowsInserted =\n    insertTask db {\n        into dbo.Person\n        entity { ID = Guid.NewGuid(); FirstName = \"John\"; LastName = \"Doe\" }\n    }\n\n// Insert with identity column\nlet! newId =\n    insertTask db {\n        for e in dbo.ErrorLog do\n        entity { ErrorLogID = 0; ErrorMessage = \"Test\"; (* ... *) }\n        getId e.ErrorLogID  // Returns the generated ID\n    }\n\n// Multiple inserts\nmatch items |\u003e AtLeastOne.tryCreate with\n| Some items -\u003e\n    insertTask db {\n        into dbo.Product\n        entities items\n    }\n| None -\u003e\n    printfn \"Nothing to insert\"\n```\n\n### Update\n\n```fsharp\n// Update specific fields\nupdateTask db {\n    for e in dbo.ErrorLog do\n    set e.ErrorMessage \"Updated message\"\n    set e.ErrorNumber 500\n    where (e.ErrorLogID = 1)\n}\n\n// Update entire entity\nupdateTask db {\n    for e in dbo.ErrorLog do\n    entity errorLog\n    excludeColumn e.ErrorLogID  // Don't update the ID\n    where (e.ErrorLogID = errorLog.ErrorLogID)\n}\n\n// Update all rows (requires explicit opt-in)\nupdateTask db {\n    for c in Sales.Customer do\n    set c.AccountNumber \"123\"\n    updateAll\n}\n```\n\n### Upsert - SQL Server (`insertOrUpdateOnUnique`)\n\nSqlHydra.Query v3.5+ supports **insert-or-update (upsert)** for SQL Server via the new `insertOrUpdateOnUnique` custom operation. This allows you to atomically insert a row or update it if a row with the same unique key already exists.\n\nThe goal was to provide a built-in upsert capability for SQL Server that is analogous to the `onConflictDoUpdate` style upsert extensions already available for SQLite and PostgreSQL queries. A key design decision was to avoid using SQL Server's `MERGE` statement in order to sidestep its [well-known footguns ](https://www.mssqltips.com/sqlservertip/3074/use-caution-with-sql-servers-merge-statement/).\n\n#### How It Works\n\nThe generated SQL uses a `TRY/CATCH` pattern that:\n1. Attempts the `INSERT`\n2. If it fails with a duplicate key violation (error 2627 or 2601), falls back to an `UPDATE`\n3. If the `UPDATE` affects 0 rows (due to a concurrent delete), retries the `INSERT`\n\n```fsharp\nopen SqlHydra.Query.SqlServerExtensions\n\nlet saveUser (user: Domain.User) =\n    let utcNow = System.DateTime.UtcNow\n    \n    insertTask db {\n        for u in dbo.Users do\n        entity {\n            Id = user.Id\n            Username = user.Username\n            Email = user.Email\n            CreatedDate = utcNow\n            UpdatedDate = utcNow\n        }\n        insertOrUpdateOnUnique\n            u.Id // If key is matched, update columns in the tuple below:\n            (\n                u.Username, \n                u.Email, \n                u.UpdatedDate\n            )\n    }\n```\n\n### Upsert - PostgreSQL and SQLite (`onConflictDoUpdate`)\n\n```fsharp\nopen SqlHydra.Query.NpgsqlExtensions\n// open SqlHydra.Query.SqliteExtensions\n\nlet saveUser (user: Domain.User) =\n    let utcNow = System.DateTime.UtcNow\n    \n    insertTask db {\n        for u in dbo.Users do\n        entity {\n            Id = user.Id\n            Username = user.Username\n            Email = user.Email\n            CreatedDate = utcNow\n            UpdatedDate = utcNow\n        }\n        onConflictDoUpdate\n            u.Id // If key is matched, update columns in the tuple below:\n            (\n                u.Username, \n                u.Email, \n                u.UpdatedDate\n            )\n    }\n```\n\n### Delete\n\n```fsharp\ndeleteTask db {\n    for e in dbo.ErrorLog do\n    where (e.ErrorLogID = 5)\n}\n\n// Delete all rows (requires explicit opt-in)\ndeleteTask db {\n    for c in Sales.Customer do\n    deleteAll\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ch2\u003eAdvanced Topics\u003c/h2\u003e\u003c/summary\u003e\n\n### Sharing a QueryContext Transaction Across Multiple Operations\n\n```fsharp\nlet completeOrder (db: QueryContextFactory) orderId = task {\n    use! shared = db.CreateContextAsync()\n    shared.BeginTransaction()        \n\n    // Update status for order\n    do! updateTask shared {\n            for o in dbo.Orders do\n            set o.Status \"Complete\"\n            where (o.Id = orderId)\n        } : Task\n\n    // Write to audit log\n    do! insertTask shared {\n            into dbo.AuditLog\n            entity { Message = $\"Completed order {orderId}\"; Timestamp = DateTime.UtcNow }\n        } : Task\n\n    shared.CommitTransaction()\n}\n```\n\n### Custom SqlKata Operations\n\nFor operations not directly supported, use the `kata` operation:\n\n```fsharp\nselect {\n    for c in main.Customer do\n    where (c.FirstName = \"John\")\n    kata (fun query -\u003e\n        query.OrderByRaw(\"LastName COLLATE NOCASE\")\n    )\n}\n```\n\n### Custom SQL with HydraReader\n\n```fsharp\nlet getTop10Products (db: QueryContextFactory) (conn: SqlConnection) = task {\n    let sql = \"SELECT TOP 10 * FROM Product\"\n    use cmd = new SqlCommand(sql, conn)\n    use! reader = cmd.ExecuteReaderAsync()\n    let hydra = HydraReader(reader)\n\n    return [\n        while reader.Read() do\n            hydra.``dbo.Product``.Read()\n    ]\n}\n```\n\n### SQL Server OUTPUT Clause\n\n```fsharp\nopen SqlHydra.Query.SqlServerExtensions\n\nlet! (created, updated) =\n    insertTask db {\n        for p in dbo.Person do\n        entity person\n        output (p.CreateDate, p.UpdateDate)\n    }\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ch2\u003eDatabase-Specific Notes\u003c/h2\u003e\u003c/summary\u003e\n\n### PostgreSQL\n\n**Enum Types:** Postgres enums are generated as CLR enums. Register them with Npgsql:\n\n```fsharp\nlet dataSource =\n    let builder = NpgsqlDataSourceBuilder(\"connection string\")\n    builder.MapEnum\u003cext.mood\u003e(\"ext.mood\") |\u003e ignore\n    builder.Build()\n```\n\n**Arrays:** `text[]` and `integer[]` column types are supported.\n\n### SQLite\n\nSQLite uses type affinity. Use standard type aliases in your schema for proper .NET type mapping.\nSee: [SQLite Type Affinity](https://www.sqlite.org/datatype3.html#affinity_name_examples)\n\n### SQL Server\n\nIf you get SSL certificate errors, append `;TrustServerCertificate=True` to your connection string.\n(Fixed in `Microsoft.Data.SqlClient` v4.1.1+)\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ch2\u003eSupported Frameworks\u003c/h2\u003e\u003c/summary\u003e\n\n- .NET 8, .NET 9, and .NET 10 are supported\n- For .NET 5 support, use the older provider-specific tools (`SqlHydra.SqlServer`, etc.)\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ch2\u003eContributing\u003c/h2\u003e\u003c/summary\u003e\n\n- Uses VS Code Remote Containers for dev environment with test databases\n- Or run `docker-compose` manually with your IDE\n- See [Contributing Wiki](https://github.com/JordanMarr/SqlHydra/wiki/Contributing)\n\n### Contributors\n\n\u003c!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --\u003e\n\u003c!-- prettier-ignore-start --\u003e\n\u003c!-- markdownlint-disable --\u003e\n\u003ctable\u003e\n  \u003ctr\u003e\n    \u003ctd align=\"center\"\u003e\n        \u003ca href=\"https://github.com/MargaretKrutikova\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/5932274?v=4?s=100\" style=\"width: 100px\" alt=\"\"/\u003e\n        \u003cbr /\u003e\u003ca href=\"https://github.com/JordanMarr/SqlHydra/pull/10\" title=\"Code\"\u003e💻\u003c/a\u003e\n    \u003c/td\u003e\n    \u003ctd align=\"center\"\u003e\n        \u003ca href=\"https://github.com/Jmaharman\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/215359?v=4\u0026s=100\" style=\"width: 100px\" alt=\"\"/\u003e\n        \u003cbr /\u003e\u003ca href=\"https://github.com/JordanMarr/SqlHydra/commits?author=Jmaharman\" title=\"Code\"\u003e💻\u003c/a\u003e\n    \u003c/td\u003e\n    \u003ctd align=\"center\"\u003e\n        \u003ca href=\"https://github.com/ntwilson\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/15835006?v=4\u0026s=100\" style=\"width: 100px\" alt=\"\"/\u003e\n        \u003cbr /\u003e\u003ca href=\"https://github.com/JordanMarr/SqlHydra/commits?author=ntwilson\" title=\"Code\"\u003e💻\u003c/a\u003e\n    \u003c/td\u003e\n    \u003ctd align=\"center\"\u003e\n        \u003ca href=\"https://github.com/MangelMaxime\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/4760796?v=4\u0026s=100\" style=\"width: 100px\" alt=\"\"/\u003e\n        \u003cbr /\u003e\u003ca href=\"https://github.com/JordanMarr/SqlHydra/commits?author=MangelMaxime\" title=\"Code\"\u003e💻\u003c/a\u003e\n    \u003c/td\u003e\n    \u003ctd align=\"center\"\u003e\n        \u003ca href=\"https://github.com/aciq\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/36763595?v=4\u0026s=100\" style=\"width: 100px\" alt=\"\"/\u003e\n        \u003cbr /\u003e\u003ca href=\"https://github.com/JordanMarr/SqlHydra/commits?author=aciq\" title=\"Code\"\u003e💻\u003c/a\u003e\n    \u003c/td\u003e\n  \u003ctr\u003e\n  \u003c/tr\u003e\n    \u003ctd align=\"center\"\u003e\n        \u003ca href=\"https://github.com/jwosty\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/4031185?v=4\u0026s=100\" style=\"width: 100px\" alt=\"\"/\u003e\n        \u003cbr /\u003e\u003ca href=\"https://github.com/JordanMarr/SqlHydra/commits?author=jwosty\" title=\"Code\"\u003e💻\u003c/a\u003e\n    \u003c/td\u003e\n    \u003ctd align=\"center\"\u003e\n        \u003ca href=\"https://github.com/devinlyons\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/8211199?v=4\u0026s=100\" style=\"width: 100px\" alt=\"\"/\u003e\n        \u003cbr /\u003e\u003ca href=\"https://github.com/JordanMarr/SqlHydra/commits?author=devinlyons\" title=\"Code\"\u003e💻\u003c/a\u003e\n    \u003c/td\u003e\n    \u003ctd align=\"center\"\u003e\n        \u003ca href=\"https://github.com/EverybodyKurts\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/879734?v=4\u0026s=100\" style=\"width: 100px\" alt=\"\"/\u003e\n        \u003cbr /\u003e\u003ca href=\"https://github.com/JordanMarr/SqlHydra/commits?author=EverybodyKurts\" title=\"Code\"\u003e💻\u003c/a\u003e\n    \u003c/td\u003e\n   \u003ctd align=\"center\"\u003e\n        \u003ca href=\"https://github.com/RJSonnenberg\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/24612120?v=4\" style=\"width: 100px\" alt=\"\"/\u003e\n        \u003cbr /\u003e\u003ca href=\"https://github.com/JordanMarr/SqlHydra/commits?author=RJSonnenberg\" title=\"Code\"\u003e💻\u003c/a\u003e\n    \u003c/td\u003e\n   \u003ctd align=\"center\"\u003e\n        \u003ca href=\"https://github.com/michelbieleveld\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/4332783?v=4\" style=\"width: 100px\" alt=\"\"/\u003e\n        \u003cbr /\u003e\u003ca href=\"https://github.com/JordanMarr/SqlHydra/commits?author=michelbieleveld\" title=\"Code\"\u003e💻\u003c/a\u003e\n    \u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\u003c!-- markdownlint-restore --\u003e\n\u003c!-- prettier-ignore-end --\u003e\n\u003c!-- ALL-CONTRIBUTORS-LIST:END --\u003e\n\n\u003c/details\u003e\n\n---\n\n## Links\n\n- [TOML Configuration Reference](https://github.com/JordanMarr/SqlHydra/wiki/TOML-Configuration)\n- [Using HydraReader with other libraries](https://github.com/JordanMarr/SqlHydra/wiki/DataReaders)\n- [SqlKata Documentation](https://sqlkata.com/)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjordanmarr%2Fsqlhydra","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjordanmarr%2Fsqlhydra","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjordanmarr%2Fsqlhydra/lists"}