Switching Database Targets
One of Quarry's key advantages is that switching database targets requires no changes to your query code, schema definitions, or business logic. The SQL dialect is configured in a single place -- the [QuarryContext] attribute -- and the source generator handles all dialect-specific differences at compile time.
The One-Line Switch
Change the Dialect property on your context and rebuild:
// Before: targeting SQLite for local development
[QuarryContext(Dialect = SqlDialect.SQLite)]
public partial class AppDb : QuarryContext
{
public partial IEntityAccessor<User> Users();
public partial IEntityAccessor<Order> Orders();
}
// After: targeting PostgreSQL for production -- change one enum value
[QuarryContext(Dialect = SqlDialect.PostgreSQL, Schema = "public")]
public partial class AppDb : QuarryContext
{
public partial IEntityAccessor<User> Users();
public partial IEntityAccessor<Order> Orders();
}
On the next build, the generator re-emits every interceptor with PostgreSQL-correct SQL. Your queries, inserts, updates, and deletes all produce dialect-appropriate output without any code changes.
What Changes Automatically
The generator handles these dialect differences for you:
| Concern | SQLite | PostgreSQL | MySQL | SQL Server |
|---|---|---|---|---|
| Identifier quoting | "col" |
"col" |
`col` |
[col] |
| Parameter placeholders | @p0, @p1 |
$1, $2 |
?, ? |
@p0, @p1 |
| Boolean literals | 1 / 0 |
TRUE / FALSE |
1 / 0 |
1 / 0 |
| Pagination | LIMIT n OFFSET m |
LIMIT n OFFSET m |
LIMIT n OFFSET m |
OFFSET m ROWS FETCH NEXT n ROWS ONLY |
| Identity return | RETURNING "Id" |
RETURNING "Id" |
SELECT LAST_INSERT_ID() |
OUTPUT INSERTED.[Id] |
| Schema qualification | (ignored) | "public"."users" |
`mydb`.`users` |
[dbo].[users] |
| String concatenation | \|\| |
\|\| |
CONCAT() |
+ |
| LIKE escape | ESCAPE '\' |
ESCAPE '\' |
ESCAPE '\' |
ESCAPE '\' |
Every query in your project gets all of these adjustments automatically. There is no runtime dialect negotiation -- the SQL is baked into string literals at compile time.
What Stays the Same
Everything else is dialect-independent:
- Schema classes --
Key<T>,Col<T>,Ref<T, TKey>,Many<T>, modifiers, indexes, naming styles - Query expressions --
.Where(),.Select(),.OrderBy(),.Join(),.GroupBy(), aggregates - Modifications --
.Insert(),.Update(),.Delete(),.InsertBatch() - Diagnostics --
ToDiagnostics()returns the dialect-specific SQL - Migrations --
MigrationBuilderoperations are rendered with dialect-correct DDL at runtime
Running Multiple Dialects Side by Side
You can define multiple context classes that share the same schema definitions but target different dialects. Each context generates its own interceptor file with fully independent SQL.
// Local development / testing
[QuarryContext(Dialect = SqlDialect.SQLite)]
public partial class LocalDb : QuarryContext
{
public partial IEntityAccessor<User> Users();
public partial IEntityAccessor<Order> Orders();
}
// Production
[QuarryContext(Dialect = SqlDialect.PostgreSQL, Schema = "public")]
public partial class ProdDb : QuarryContext
{
public partial IEntityAccessor<User> Users();
public partial IEntityAccessor<Order> Orders();
}
This is useful for:
- Local development -- Use SQLite with an in-memory or file database for fast iteration, then deploy against PostgreSQL or SQL Server in production.
- Testing -- Run integration tests against SQLite for speed, with a separate test suite against the production dialect for verification.
- Migration -- Gradually switch from one database to another by running both contexts in parallel, comparing results.
Both contexts can be used in the same application simultaneously:
// Fast local queries against SQLite cache
await using var local = new LocalDb(sqliteConnection);
var cached = await local.Users().Where(u => u.IsActive).Select(u => u).ExecuteFetchAllAsync();
// Production queries against PostgreSQL
await using var prod = new ProdDb(npgsqlConnection);
var users = await prod.Users().Where(u => u.IsActive).Select(u => u).ExecuteFetchAllAsync();
The generated SQL is completely independent -- local emits SQLite SQL, prod emits PostgreSQL SQL, and neither affects the other.
Verifying Dialect Output
Use ToDiagnostics() to inspect the generated SQL for any query:
var diag = db.Users()
.Where(u => u.IsActive && u.Email != null)
.OrderBy(u => u.UserName)
.Select(u => u)
.Limit(10)
.ToDiagnostics();
Console.WriteLine(diag.Dialect); // PostgreSQL
Console.WriteLine(diag.Sql);
// SELECT "UserId", "UserName", "Email", "IsActive", "CreatedAt"
// FROM "public"."users"
// WHERE "IsActive" = TRUE AND "Email" IS NOT NULL
// ORDER BY "UserName"
// LIMIT 10
This is particularly useful when switching dialects to confirm the generated SQL matches your expectations before deploying.
Dialect-Aware Type Mappings
If you have custom type mappings that need dialect-specific behavior (e.g., jsonb on PostgreSQL vs TEXT on SQLite), implement IDialectAwareTypeMapping:
public class JsonDocMapping : TypeMapping<JsonDoc, string>, IDialectAwareTypeMapping
{
public override string ToDb(JsonDoc value) => JsonSerializer.Serialize(value);
public override JsonDoc FromDb(string value) => JsonSerializer.Deserialize<JsonDoc>(value)!;
public string GetSqlTypeName(SqlDialect dialect) => dialect switch
{
SqlDialect.PostgreSQL => "jsonb",
_ => "TEXT"
};
public void ConfigureParameter(SqlDialect dialect, DbParameter parameter)
{
if (dialect == SqlDialect.PostgreSQL && parameter is NpgsqlParameter npgsql)
npgsql.NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Jsonb;
}
}
The generator calls GetSqlTypeName() when emitting DDL and migration code, and the runtime calls ConfigureParameter() when binding query parameters. Both adapt automatically to whichever dialect the context is configured for.