{"id":15036327,"url":"https://github.com/bawkee/sqlbinder","last_synced_at":"2025-04-09T23:23:30.343Z","repository":{"id":144135841,"uuid":"129268113","full_name":"bawkee/SqlBinder","owner":"bawkee","description":":paperclip: Free and open-source library that helps you create complex and reusable SQL.","archived":false,"fork":false,"pushed_at":"2024-10-03T09:48:28.000Z","size":1347,"stargazers_count":24,"open_issues_count":0,"forks_count":3,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-07T15:51:22.468Z","etag":null,"topics":["ado-net","csharp-library","database","dotnet","separation-of-concerns","sql","templating"],"latest_commit_sha":null,"homepage":"","language":"C#","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/bawkee.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}},"created_at":"2018-04-12T14:56:36.000Z","updated_at":"2025-03-26T04:42:50.000Z","dependencies_parsed_at":"2024-09-24T20:30:53.743Z","dependency_job_id":null,"html_url":"https://github.com/bawkee/SqlBinder","commit_stats":null,"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bawkee%2FSqlBinder","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bawkee%2FSqlBinder/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bawkee%2FSqlBinder/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bawkee%2FSqlBinder/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bawkee","download_url":"https://codeload.github.com/bawkee/SqlBinder/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248126902,"owners_count":21052091,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["ado-net","csharp-library","database","dotnet","separation-of-concerns","sql","templating"],"created_at":"2024-09-24T20:30:48.385Z","updated_at":"2025-04-09T23:23:30.296Z","avatar_url":"https://github.com/bawkee.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# SqlBinder :paperclip:\n\n![License](https://img.shields.io/github/license/bawkee/SqlBinder) ![Stars](https://img.shields.io/github/stars/bawkee/SqlBinder?style=social) [![NuGet](https://img.shields.io/nuget/v/SqlBinder.svg)](https://www.nuget.org/packages/SqlBinder/)\n\nSqlBinder is a free, open-source library designed to effortlessly generate valid SQL statements by transforming SQL templates based on a set of conditions.\n\n- **Not an ORM**: SqlBinder isn't an Object-Relational Mapping tool. It's a DBMS-independent, SQL-centric templating engine that removes the overhead of SQL generation and bind variables.\n  \n- **Enhanced Composability**: Unlike typical SQL builders, SqlBinder is geared towards crafting complex queries, going beyond simple placeholder replacements.\n  \n- **Plays Well with Others**: Compatible with other ORM tools like Dapper, PetaPoco, and EntityFramework.\n  \n- **Security First**: Automatically handles bind variables, mitigating SQL injection risks.\n\n## Key Features\n\n- 🚀 **Scalable**: Tackles both basic and complex SQL generation needs. No performance penalties, no overhead.\n- 📚 **Examples**: Covering everything from beginner to expert use cases.\n- ⚡ **Performance**: Outstanding speed metrics, even when used alongside tools like Dapper, PetaPoco, and EntityFramework.\n- 🧪 **Well Tested**: A wide range of tests and great coverage makes it super stable.\n- 🏆 **Award-Winning Article**: Check out our [CodeProject article](https://www.codeproject.com/Articles/1246990/SqlBinder-Library) for an in-depth walkthrough.\n- 🌐 **Real-world Demo**: A WPF-based application demonstrating SqlBinder's capabilities with the Northwind database.\n\n## Current Status\n\nSqlBinder has been stable for several years. Even though it hasn't seen recent updates, the code base was used in real-world production environments. I believe a boost in its user base could rekindle community involvement, new features and contributions.\n\n## A Quick Demonstration\n\nConsider the following method signature:\n\n```C#\nIEnumerable\u003cCategorySale\u003e GetCategorySales(\n\tIDbConnection connection,\n\tIEnumerable\u003cint\u003e categoryIds = null,\n\tDateTime? fromShippingDate = null, DateTime? toShippingDate = null,\n\tDateTime? fromOrderDate = null, DateTime? toOrderDate = null,\n\tIEnumerable\u003cstring\u003e shippingCountries = null);\n```\n\nImplementation of this method should return a summary of sales grouped by categories and filtered by any combination of the following criteria: categories, shipping dates, order dates and shipping countries. \n\nInstead of manually building SQL using Fluent APIs or string concatenation, see how SqlBinder, combined with Dapper, simplifies the process:\n\n```C#\nIEnumerable\u003cCategorySale\u003e GetCategorySales(\n\tIDbConnection connection,\n\tIEnumerable\u003cint\u003e categoryIds = null,\n\tDateTime? fromShippingDate = null, DateTime? toShippingDate = null,\n\tDateTime? fromOrderDate = null, DateTime? toOrderDate = null,\n\tIEnumerable\u003cstring\u003e shippingCountries = null)\n{\n\tvar query = new Query(GetEmbeddedResource(\"CategorySales.sql\")); // SqlBinder!\n\n\tquery.SetCondition(\"categoryIds\", categoryIds);\n\tquery.SetConditionRange(\"shippingDates\", fromShippingDate, toShippingDate);\n\tquery.SetConditionRange(\"orderDates\", fromOrderDate, toOrderDate);\n\tquery.SetCondition(\"shippingCountries\", shippingCountries);\n\n\treturn connection.Query\u003cCategorySale\u003e(query.GetSql(), query.SqlParameters);\n}\n```\n\nThe SQL templates, like this one, offer native support for multiple `WHERE` clauses, varied `ORDER BY` statements, and numerous sub-queries.\n\nAnother script with shortcut aliases and an optional sub-query:\n\n```SQL\nSELECT\n\tCAT.CategoryID, \n\tCAT.CategoryName, \n\tSUM(CCUR(OD.UnitPrice * OD.Quantity * (1 - OD.Discount) / 100) * 100) AS TotalSales\nFROM ((Categories AS CAT\t\t\n\tINNER JOIN Products AS PRD ON PRD.CategoryID = CAT.CategoryID)\n\tINNER JOIN OrderDetails AS OD ON OD.ProductID = PRD.ProductID)\n{WHERE \t\n\t{OD.OrderID IN (SELECT OrderID FROM Orders AS ORD WHERE \n\t\t\t{ORD.ShippedDate :shippingDates} \n\t\t\t{ORD.OrderDate :orderDates}\n\t\t\t{ORD.ShipCountry :shippingCountries})} \n\t{CAT.CategoryID :categoryIds}}\nGROUP BY \n\tCAT.CategoryID, CAT.CategoryName\n```\n\nWhat's this *optional* sub-query? Well, since our `OD.OrderID IN` condition is enclosed within `{ }` braces it means that it won't be used if it's *not needed* - in other words, if it's not needed then output SQL won't contain it along with its sub-query `SELECT OrderID FROM Orders ...`. Again, the whole part enclosed in `{ }` would be removed if its conditions aren't used, specifically if none of the `:shippingDates`, `:orderDates` or `:shippingCountries` are used. The `:categoryIds` condition is separate from this and belongs to the parent query, SqlBinder will connect it with the above condition automatically (*if* it's used) with an `AND` operand.\n\nThis way, SqlBinder ensures SQL scripts are decoupled from your code, improving maintainability.\n\nThe next template script uses different aliases and would work just the same:\n\n```SQL\nSELECT\n\tCategories.CategoryID, \n\tCategories.CategoryName, \n\tSUM(CCUR(OrderDetails.UnitPrice * OrderDetails.Quantity * \n\t\t(1 - OrderDetails.Discount) / 100) * 100) AS TotalSales\nFROM ((Categories\t\t\n\tINNER JOIN Products ON Products.CategoryID = Categories.CategoryID)\n\tINNER JOIN OrderDetails ON OrderDetails.ProductID = Products.ProductID)\n{WHERE \t\n\t{OrderDetails.OrderID IN (SELECT OrderID FROM Orders WHERE \n\t\t\t{Orders.ShippedDate :shippingDates} \n\t\t\t{Orders.OrderDate :orderDates}\n\t\t\t{Orders.ShipCountry :shippingCountries})} \n\t{Categories.CategoryID :categoryIds}}\nGROUP BY \n\tCategories.CategoryID, Categories.CategoryName\n```\n\nYou don't need to modify your `GetCategorySales` method for this template to work, it'll work as long as the parameter names are the same.\n\nNext template uses a completely different join and has no sub-queries:\n\n```SQL\nSELECT\n\tCategories.CategoryID, \n\tCategories.CategoryName, \n\tSUM(CCUR(OrderDetails.UnitPrice * OrderDetails.Quantity * \n\t\t(1 - OrderDetails.Discount) / 100) * 100) AS TotalSales\nFROM (((Categories\t\t\n\tINNER JOIN Products ON Products.CategoryID = Categories.CategoryID)\n\tINNER JOIN OrderDetails ON OrderDetails.ProductID = Products.ProductID)\n\tINNER JOIN Orders ON Orders.OrderID = OrderDetails.OrderID)\n{WHERE\n\t{Orders.ShippedDate :shippingDates} \n\t{Orders.OrderDate :orderDates}\n\t{Orders.ShipCountry :shippingCountries} \n\t{Categories.CategoryID :categoryIds}}\nGROUP BY \n\tCategories.CategoryID, Categories.CategoryName\n```\n\nHere's another template which has two `WHERE` clauses, is using a different syntax to join and has no `GROUP BY`. This works out of the box and would produce the same data:\n\n```SQL\nSELECT \n\tCategories.CategoryID, \n\tCategories.CategoryName, \n\t(SELECT SUM(CCUR(UnitPrice * Quantity * (1 - Discount) / 100) * 100) \n\tFROM OrderDetails WHERE ProductID IN \n\t\t(SELECT ProductID FROM Products WHERE Products.CategoryID = Categories.CategoryID)\n\t\t{AND OrderID IN (SELECT OrderID FROM Orders WHERE \n\t\t\t{Orders.ShippedDate :shippingDates} \n\t\t\t{Orders.OrderDate :orderDates}\n\t\t\t{Orders.ShipCountry :shippingCountries})}) AS TotalSales\nFROM Categories {WHERE {Categories.CategoryID :categoryIds}}\n```\n\nWhat SqlBinder does is it binds `SqlBinder.Condition` objects to its template scripts returning a valid SQL which you can then pass to your ORM.\n\n## Tutorials, Examples and Demo App\n\n- **In-depth Guide**: Discover more examples and tutorials in the [SqlBinder's Code Project article](https://www.codeproject.com/Articles/1246990/SqlBinder-Library).\n\n- **Demo App**: The SqlBinder source code includes a hands-on demo application, offering a deeper dive into its capabilities. More details can be found in the [Code Project article](https://www.codeproject.com/Articles/1246990/SqlBinder-Library).\n\n⭐ Don't forget to rate the article if it helped!\n\n## The Syntax\n\nConsists of two basic types of elements: scopes and parameter placeholders. Scopes are defined by curly braces `{ ... }` and parameter placeholders can be defined by the typical SQL syntax (i.e. `:parameter` or `@parameter`) or by custom SqlBinder syntax (if configured so, i.e. `[parameter]`). \n\nExplained with regex:\n```SQL\n... [@\\+]{ ... [:?@]paramPlaceholder  ... } ...\n```\n\nOr, consider the following set of valid examples where `...` can be any SQL:\n```SQL\n... { ... :paramPlaceholder  ... } ...\n\n... { ... @paramPlaceholder  ... } ...\n\n... { ... { ... :paramPlaceholder  ... } ... } ...\n\n... @{ ... { ... :paramPlaceholder1  ... } ... { ... :paramPlaceholder2 ... } ... } ...\n\n... +{ ... { ... :paramPlaceholder1  ... } ... { ... :paramPlaceholder2 ... } ... } ...\n\n... { ... { ... [place holder 1]  ... } ... { ... [place holder 2] ... } ... } ...\n```\n\nFurther explanation of above examples:\n* Curly braces `{ ... }` define a scope. Scope can either contain child scopes or a single parameter placeholder. Scope that does not contain either of those will always be removed as that's considered pointless. Otherwise, the scope is removed only if all its child scopes are removed or its parameter placeholder is removed, *which* in turn is removed if no matching *condition* was found for it (conditions are explained further on). \n* `:paramPlaceholder` can be any alphanumeric name that will be matched against `Query.Conditions` collection. This is referred to as *parameter* in the SqlBinder objects. If a parameter doesn't match any condition it will be removed along with its entire parent scope. The output SQL bind variable will be formatted with the same prefix as the parameter (acceptable prefixes are `:` or `@` or `?`). These parameters are not bind-variables and you must respect the aforementioned syntax, i.e. the Oracle variable `:\"MyVariable\"` won't be recognized as a parameter - if you need custom formatting in your output variables which you can't accomplish with the SqlBinder syntax names there ways to do so via events and delegates (see the Query class). Note that there can only be one placeholder in a given scope. When you need multiple placeholders put each one in its own separate scope.\n* `[place holder xy]` works the same way as above except any character is allowed and you must provide the parameter prefix manually (in C#) by overriding the `Query` class, `DbQuery` class or setting the appropriate property. Also, this syntax doesn't work by default, you have to enable a special hint via `Query.ParserHints` property since `[]` characters are used by some SQL flavors. With that said, you can still escape these tags into the output SQL `[[like this]]`.\n* The `@` character before the scope (i.e. `@{`) tells the SqlBinder to connect scopes with an `OR` rather than default `AND` operator. \n* The `+` character before the scope (i.e. `+{`) instructs the SqlBinder to not automatically connect this specific scope with its previous sibling by an `AND` (or any  operator), to instead just leave the white space as it already is. \n* The `...` can be any SQL from any DBMS or just about any text. The string literals won't be processed by the SqlBinder which means they can safely contain SqlBinder syntax. The special flavors of literals such as PostgreSQL dollar literals or Oracle AQM literals are recognized as well and can safely contain any special character used by the SqlBinder. The same goes for SQL comments.\n\n**The comment syntax** looks like this:\n```SQL\n/*{ ...sql binder comment... }*/\n\n/*{\n\t...sql binder comment...\n}*/\n```\nSqlBinder comments will be removed entirely from the output SQL while the SQL comments will remain intact.\n\n**The SQL literals** are respected and may contain SqlBinder syntax which won't be processed:\n```SQL\n'Anything can be here: [] {} ...' \n\n\"Or here {} [] ...\"\n\n'Or in ''escaped literal {} [] ...'''\n\nq'{Or in this Oracle literal {} [] which can get very creative ...}'\n\n$myTag$Or in this PostgreSQL literal {} [] ...$myTag$\n```\nNone of these are processed against SqlBinder syntax. You may safely put parameter placeholder or scope syntax in here and it won't be altered in any way. SqlBinder does not do simple find-replace, it parses the script and re-builds the SQL based on it.\n\n## Performance Metrics\n\nSqlBinder's performance, when combined with ORMs like Dapper, is exceptional:\n\n**LocalDB (Sql Sever Express):**\n\n```\n    Dapper +SqlBinder\n---------------------\n     52.88      53.46\n     57.31      59.55\n     56.22      68.07\n     55.97      56.16\n     66.52      55.59\n     54.82      52.96\n     50.98      61.97\n     59.06      57.53\n     50.38      53.97\n    AVG 56     AVG 58\n\n ^ Dapper = Just Dapper.\n ^ +SqlBinder = Dapper with SqlBinder.\n```\n\n**OleDb (Access):**\n\n```\n    Dapper +SqlBinder\n---------------------\n    335.42     336.38\n    317.99     318.89\n    342.56     324.85\n    317.20     320.84\n    327.91     324.56\n    320.29     326.86\n    334.42     338.73\n    344.43     326.33\n    315.32     322.48\n   AVG 328    AVG 327\n\n ^ Dapper = Just Dapper.\n ^ +SqlBinder = Dapper with SqlBinder.\n```\n\nAs you can observe, on SqlServer we've had an additional overhead of 2ms which is the time it took SqlBinder to formulate a query based on different criteria. On the OleDb Access test this difference was so insignificant it was lost entirely in deviations (most likely in interaction with the DB).\n\nEach row in the test results was a result of 500 executions of the following queries:\n\n```SQL\nSELECT * FROM POSTS WHERE ID IN @id\n```\n\nAnd\n\n```SQL\nSELECT * FROM POSTS {WHERE {ID @id}}\n```\n\nWhere the latter was used in Dapper + SqlBinder combination. \n\nIt is important to note that SqlBinder has the ability to re-use compiled templates as it completely separates the parsing and templating concerns. You may create a SqlBinder query template once and then build all the subsequent SQL queries from the same pre-parsed template. One of the key functionalities of SqlBinder is that it doesn't parse or generate the whole SQL *every time*.\n\nPerformance tests are available in the source folder. Benchmark SqlBinder on your own!\n\n## Origin Story\n\nSqlBinder was born in 2009 out of a necessity to simplify front-end development against complex Oracle databases. With performance and bandwidth being critical, we needed a solution that could leverage powerful SQLs without compromising code maintainability. SqlBinder grew from a prototype to the robust tool it is today, often used alongside Dapper for optimal results.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbawkee%2Fsqlbinder","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbawkee%2Fsqlbinder","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbawkee%2Fsqlbinder/lists"}