https://github.com/yavorfingarov/embeddedsql
A clean and organized way to manage SQL statements and database migrations using .sql files.
https://github.com/yavorfingarov/embeddedsql
db-migrations dotnet sql
Last synced: 3 months ago
JSON representation
A clean and organized way to manage SQL statements and database migrations using .sql files.
- Host: GitHub
- URL: https://github.com/yavorfingarov/embeddedsql
- Owner: yavorfingarov
- License: mit
- Created: 2025-02-27T09:19:53.000Z (3 months ago)
- Default Branch: master
- Last Pushed: 2025-02-27T13:23:49.000Z (3 months ago)
- Last Synced: 2025-02-27T18:17:29.119Z (3 months ago)
- Topics: db-migrations, dotnet, sql
- Language: C#
- Homepage:
- Size: 56.6 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# EmbeddedSql
[](https://www.nuget.org/packages/EmbeddedSql)
[](https://www.nuget.org/stats/packages/EmbeddedSql?groupby=Version)
[](https://github.com/yavorfingarov/EmbeddedSql/actions/workflows/cd.yml?query=branch%3Amaster)
[](https://github.com/yavorfingarov/EmbeddedSql/actions/workflows/cd.yml?query=branch%3Amaster)
[](https://github.com/yavorfingarov/EmbeddedSql/actions/workflows/cd.yml?query=branch%3Amaster)
[](https://github.com/yavorfingarov/EmbeddedSql/actions/workflows/cd.yml?query=branch%3Amaster)
[](https://github.com/yavorfingarov/EmbeddedSql/actions/workflows/cd.yml?query=branch%3Amaster)
[](https://scorecard.dev/viewer/?uri=github.com/yavorfingarov/EmbeddedSql&sort_by=risk-level&sort_direction=desc)EmbeddedSql is a NuGet package that provides a clean and organized way to manage SQL statements and database migrations using `.sql` files. It works on top of `System.Data` abstractions, making it database provider and ORM agnostic.
## Rationale
Having SQL statements as plain strings scattered around your codebase is not only ugly but error-prone as well. When all of them are organized in `.sql` files, you can have a better overview, reduce the cognitive overhead of mixed languages, employ proper code style, and have nice syntax highlighting.
## Setup
* Install the [EmbeddedSql NuGet package](https://www.nuget.org/packages/EmbeddedSql).
* Make sure all `.sql` files are going to be embedded in the assembly:
```xml
```
* Add EmbeddedSql services:
```csharp
builder.Services.AddEmbeddedSql();
```* Alternatively, you can also point specific assemblies for scanning and apply filename-based filtering:
```csharp
builder.Services.AddEmbeddedSql(options =>
{
options.Assemblies = new[] { typeof(TestApi.Common.Entry).Assembly };
options.UseFilter(resourceName => !resourceName.Contains("Scripts"));
});
```## SQL statements
A special type of comment with three dashes `---` denotes a key for a SQL statement.
> [!NOTE]
> All code samples use [Dapper](https://github.com/DapperLib/Dapper) and [SQLite](https://www.sqlite.org/index.html).```sql
--- AppUser.GetSELECT Id, FirstName, LastName
FROM AppUser
WHERE Id = @Id
```On application startup, a singleton service `ISql` is registered in the DI container. All embedded `.sql` resources are scanned and stored internally in a [FrozenDictionary](https://learn.microsoft.com/en-us/dotnet/api/system.collections.frozen.frozendictionary-2?view=net-9.0) for better performance.
> [!NOTE]
> The name of the files containing SQL statements doesn't matter.Getting SQL statements is then straightforward:
```csharp
app.MapGet("/api/users/{id}", (string id, IDbConnection db, ISql sql) =>
{
var user = db.QuerySingleOrDefault(sql["AppUser.Get"], new { Id = id });
if (user == null)
{
return Results.NotFound();
}return Results.Ok(user);
});
```### Unsafe format
The `UnsafeFormat` overloads offer a great way to construct SQL statements dynamically.
Consider this statement:
```sql
--- AppUser.SearchSELECT Id, FirstName, LastName
FROM AppUser
WHERE {0}
```You can use it like that:
```csharp
app.MapGet("/api/users/search", (string? firstName, string? lastName, IDbConnection db, ISql sql) =>
{
var parameters = new Dictionary();
var predicates = new List();
if (firstName != null)
{
parameters.Add("FirstName", firstName);
predicates.Add("FirstName = @FirstName");
}if (lastName != null)
{
parameters.Add("LastName", lastName);
predicates.Add("LastName = @LastName");
}if (parameters.Count == 0)
{
return Results.BadRequest();
}var condition = string.Join(" AND ", predicates);
var query = sql.UnsafeFormat("AppUser.Search", condition);
var users = db.Query(query, parameters);return Results.Ok(users);
});
```> [!WARNING]
> Never pass non-validated user-provided values into this method. Doing so may expose your application to SQL injection attacks.## Migrations
EmbeddedSql offers a simple way to handle your database migrations.
```csharp
private static void MigrateDb(this WebApplication app)
{
using var scope = app.Services.CreateScope();
var migrator = scope.ServiceProvider.GetRequiredService();
migrator.Run();
}// OR
private static async Task MigrateDb(this WebApplication app)
{
await using var scope = app.Services.CreateAsyncScope();
var migrator = scope.ServiceProvider.GetRequiredService();
await migrator.RunAsync();
}```
A scoped `IMigrator` service is registered by default. Without further customization, it will create a table for tracking all applied migrations. You should provide its definition and the statements to work with it. Furthermore, the migrator will expect to find an `IDbConnection` in the DI container.
```sql
--- _Migration.EnsureTableCREATE TABLE IF NOT EXISTS _Migration (
Id TEXT NOT NULL,
CONSTRAINT PK__Migration PRIMARY KEY (Id)
)--- _Migration.GetAll
SELECT Id
FROM _Migration--- _Migration.Create
INSERT INTO _Migration (Id)
VALUES (@Id)
```> [!NOTE]
> The script for ensuring the migration tracking table is run every time, so it should be indempotent.By default, all statements that are prefixed with `Migration.` are considered migration scripts.
```sql
--- Migration.AppUser.0001_InitCREATE TABLE AppUser (
Id TEXT NOT NULL,
FirstName TEXT NOT NULL,
LastName TEXT NOT NULL,
CONSTRAINT PK_AppUser PRIMARY KEY (Id)
)
```Migration scripts are going to be applied in alphabetical order.
### Customization
You can customize many aspects of the behavior of the migrator.
```csharp
builder.Services.AddEmbeddedSql(options =>
{
options.ConfigureMigrator(migratorOptions =>
{
// ...
});
});
```#### Idempotent scripts
By default, the migrator will expect non-idempotent scripts. You can change this:
```csharp
migratorOptions.Idempotent = true;
```This would mean that no migration tracking table will be created.
#### Transaction behavior
By default, every script is wrapped in a transaction. Instead, you can also wrap all migrations in a single transaction:
```csharp
migratorOptions.TransactionBehavior = TransactionBehavior.Overarching;
```Alternatively, you can disable transactions altogether:
```csharp
migratorOptions.TransactionBehavior = TransactionBehavior.None;
```> [!WARNING]
> Please make sure you know how DDL statements are handled by your db.
>
> Currently, the only check that the migrator does is whether your provider is MySql/MariaDb and logs a warning if you are using transactions due to [implicit commit](https://dev.mysql.com/doc/refman/5.7/en/implicit-commit.html).#### Script naming
If the default naming conventions don't fit well in your codebase, you can change them:
```csharp
migratorOptions.EnsureMigrationTableCommand = "_MyMigration.EnsureTable";
migratorOptions.GetMigrationsQuery = "_MyMigration.GetAll";
migratorOptions.CreateMigrationCommand = "_MyMigration.Create";
migratorOptions.MigrationScriptPrefix = "MyMigration.";
```Also, since the migration tracking table is defined by you, you can pimp it up too:
```sql
--- _MyMigration.EnsureTableCREATE TABLE IF NOT EXISTS _Migration (
Id TEXT NOT NULL,
Timestamp TEXT NOT NULL,
CONSTRAINT PK__Migration PRIMARY KEY (Id)
)--- _MyMigration.Create
INSERT INTO _Migration (Id, Timestamp)
VALUES (@Id, @Timestamp)
```Then you should change the parameters for the create migration command accordingly.
```csharp
migratorOptions.ConfigureCreateMigrationCommandParameters((parameters, migration) =>
{
parameters["@Id"] = migration;
parameters["@Timestamp"] = DateTime.UtcNow;
});
```## Keyed services
You can set a service key for `ISql` and `IMigrator` when registering:
```csharp
builder.Services.AddEmbeddedSql(options =>
{
options.ServiceKey = "AppUsers";
});
```The migrator will first try to resolve the `IDbConnection` with the same key, and if such is not found, it will fall back to the keyless one. This way you can have many sets of SQL statements, migrators, and database connections.
## Additional resources
* [API reference](https://github.com/yavorfingarov/EmbeddedSql/blob/master/docs/EmbeddedSql.md)
* [Sample app](https://github.com/yavorfingarov/EmbeddedSql/tree/master/samples/EmbeddedSql.SampleApi)
* [Changelog](https://github.com/yavorfingarov/EmbeddedSql/blob/master/CHANGELOG.md)
* [License](https://github.com/yavorfingarov/EmbeddedSql/blob/master/LICENSE)
## Feedback
* [Issue tracker](https://github.com/yavorfingarov/EmbeddedSql/issues)
* [Discussions](https://github.com/yavorfingarov/EmbeddedSql/discussions)