Quarry.Generator 0.2.1

dotnet add package Quarry.Generator --version 0.2.1
                    
NuGet\Install-Package Quarry.Generator -Version 0.2.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Quarry.Generator" Version="0.2.1">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Quarry.Generator" Version="0.2.1" />
                    
Directory.Packages.props
<PackageReference Include="Quarry.Generator">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Quarry.Generator --version 0.2.1
                    
#r "nuget: Quarry.Generator, 0.2.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Quarry.Generator@0.2.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Quarry.Generator&version=0.2.1
                    
Install as a Cake Addin
#tool nuget:?package=Quarry.Generator&version=0.2.1
                    
Install as a Cake Tool

Quarry

Type-safe SQL builder for .NET 10. Source generators + C# 12 interceptors emit all SQL at compile time. AOT compatible. Structured logging via Logsmith.


Quarry.Generator

Roslyn incremental source generator that analyzes fluent query chains at compile time and emits interceptor methods containing pre-built SQL, ordinal-based readers, and zero-allocation carrier classes. No SQL is built at runtime — what you see in the generated code is exactly what executes.

Packages

Name NuGet Description
Quarry Quarry Runtime types: builders, schema DSL, dialects, executors.
Quarry.Generator Quarry.Generator Roslyn incremental source generator + interceptor emitter.
Quarry.Analyzers Quarry.Analyzers Compile-time SQL query analysis rules (QRA series) with code fixes.
Quarry.Analyzers.CodeFixes Quarry.Analyzers.CodeFixes Code fix providers for QRA diagnostics.
Quarry.Tool Quarry.Tool CLI tool for migrations and database scaffolding (quarry command).

Installation

<PackageReference Include="Quarry" Version="1.0.0" />
<PackageReference Include="Quarry.Generator" Version="1.0.0"
    OutputItemType="Analyzer"
    ReferenceOutputAssembly="false" />

Enable interceptors by adding your QuarryContext namespace to InterceptorsNamespaces in your .csproj. The generator emits interceptors into the same namespace as your context class:

<PropertyGroup>
  <InterceptorsNamespaces>$(InterceptorsNamespaces);MyApp.Data</InterceptorsNamespaces>
</PropertyGroup>

Replace MyApp.Data with the namespace containing your QuarryContext subclass. If your context has no namespace, use Quarry.Generated.

To inspect generated code, add:

<PropertyGroup>
  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
  <CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GeneratedFiles</CompilerGeneratedFilesOutputPath>
</PropertyGroup>

Quick Start

1. Define a schema

public class UserSchema : Schema
{
    public static string Table => "users";

    public Key<int> UserId => Identity();
    public Col<string> UserName => Length(100);
    public Col<string?> Email { get; }
    public Col<bool> IsActive => Default(true);
    public Col<DateTime> CreatedAt => Default(() => DateTime.UtcNow);
}

2. Define a context

[QuarryContext(Dialect = SqlDialect.SQLite)]
public partial class AppDb : QuarryContext
{
    public partial IEntityAccessor<User> Users();
}

Dialects: SQLite, PostgreSQL, MySQL, SqlServer.

3. Query

await using var db = new AppDb(connection);

var activeUsers = await db.Users()
    .Where(u => u.IsActive)
    .Select(u => new UserDto { Name = u.UserName, Email = u.Email })
    .OrderBy(u => u.UserName)
    .Limit(10)
    .ExecuteFetchAllAsync();

The generator emits an interceptor that replaces ExecuteFetchAllAsync with pre-built SQL and a typed reader. No runtime translation occurs.


How It Works

The generator runs a multi-stage pipeline during compilation:

  1. Discovery — Scans syntax trees for method calls on Quarry builder types (Where, Select, Join, Insert, etc.)
  2. Binding — Enriches each call site with semantic information: entity metadata, dialect, parameter types
  3. Translation — Resolves column references, parameters, and expression trees into SQL expression IR
  4. Chain Analysis — Groups call sites into fluent chains, identifies terminals, analyzes conditional branches
  5. SQL Assembly — Renders each chain into dialect-specific SQL string literals for every possible clause combination
  6. Carrier Analysis — Determines if the chain qualifies for zero-allocation carrier optimization
  7. Code Emission — Generates [InterceptsLocation] methods that replace the original calls at compile time

The result: fluent C# queries become pre-compiled SQL execution with full type safety.


Optimization Tiers

The generator classifies every query chain into an optimization tier:

Mode Name Description
PrebuiltDispatch Pre-built dispatch All clauses analyzed. SQL dispatch table emitted as constants. Zero runtime string work.
RuntimeBuild Compile error Chain not statically analyzable. Produces QRY032 compile error directing the user to restructure.

PrebuiltDispatch is the only output mode for well-formed chains. The generator emits QRY001 or QRY032 diagnostics when a chain cannot be analyzed.


Carrier Architecture

For all analyzed chains, the generator emits a carrier class — a lightweight sealed class that holds all parameters, conditions, and state for the query. Carriers eliminate all intermediate builder allocations on the execution path.

Each carrier:

  • Implements the builder interfaces (IQueryBuilder<T>, IDeleteBuilder<T>, etc.)
  • Stores parameters as typed fields (P0, P1, ...)
  • Tracks conditional clause activation via a bitmask field (Mask)
  • Contains the pre-built SQL dispatch table as const string fields
  • Uses ordinal-based Func<DbDataReader, T> delegates for materialization — no reflection

Use ToDiagnostics() to verify carrier optimization:

var diag = db.Users()
    .Where(u => u.IsActive)
    .Select(u => u)
    .ToDiagnostics();

Console.WriteLine(diag.Kind);              // Select
Console.WriteLine(diag.CarrierClassName);  // Chain_...

Conditional Branch Support

Queries built with if/else branching are fully supported. The generator assigns each conditional clause a bit index and enumerates all possible combinations as a bitmask dispatch table.

var query = db.Users().Select(u => u);

if (activeOnly)
    query = query.Where(u => u.IsActive);

if (sortByName)
    query = query.OrderBy(u => u.UserName);

// Generator emits up to 4 SQL variants (2 bits x 2 states)
// and dispatches to the correct one at runtime via bitmask
var results = await query.Limit(10).ExecuteFetchAllAsync();

Each clause reports its conditional state via ToDiagnostics():

foreach (var clause in diag.Clauses)
    Console.WriteLine($"{clause.ClauseType}: active={clause.IsActive}, conditional={clause.IsConditional}");

Prepared Queries

.Prepare() freezes a query chain and allows multiple terminal operations without rebuilding:

var prepared = db.Users()
    .Where(u => u.IsActive)
    .Select(u => u)
    .Prepare();

var all = await prepared.ExecuteFetchAllAsync();
var first = await prepared.ExecuteFetchFirstAsync();
var diag = prepared.ToDiagnostics();

.Prepare() is zero-cost — it performs an Unsafe.As cast with no allocation. The generator intercepts each terminal on the PreparedQuery<T> variable independently.

Constraints:

  • The PreparedQuery variable must not escape the declaring method (no returns, field assignments, or lambda captures — QRY035)
  • At least one terminal must be invoked on the prepared variable (QRY036)

Query Diagnostics

ToDiagnostics() returns compile-time analysis metadata without executing the query:

var diag = db.Users()
    .Where(u => u.IsActive)
    .OrderBy(u => u.UserName)
    .Select(u => u)
    .ToDiagnostics();

Console.WriteLine(diag.Sql);               // SELECT ... FROM "users" WHERE ...
Console.WriteLine(diag.Dialect);           // SQLite
Console.WriteLine(diag.Kind);             // Select

foreach (var p in diag.Parameters)
    Console.WriteLine($"{p.Name} = {p.Value} ({p.TypeName})");

foreach (var clause in diag.Clauses)
    Console.WriteLine($"{clause.ClauseType}: {clause.SqlFragment}");

Available on all builder types — SELECT, INSERT, UPDATE, DELETE, and batch insert chains.


Supported Query Patterns

Select

db.Users().Select(u => u);                                    // full entity
db.Users().Select(u => u.UserName);                           // single column
db.Users().Select(u => (u.UserId, u.UserName));               // tuple
db.Users().Select(u => new UserDto { Name = u.UserName });    // named DTO

Anonymous type projections are not supported (QRY014). Use named records, classes, or tuples.

Where

db.Users().Where(u => u.IsActive && u.UserId > minId);
db.Users().Where(u => u.Email != null);
db.Users().Where(u => u.UserName.Contains("smith"));        // LIKE '%smith%'
db.Users().Where(u => new[] { 1, 2, 3 }.Contains(u.UserId)); // IN clause
db.Users().Where(u => Sql.Raw<bool>("\"Age\" > @p0", 18));  // raw SQL

Supported operators: ==, !=, <, >, <=, >=, &&, ||, !. String methods: Contains, StartsWith, EndsWith, ToLower, ToUpper, Trim, Substring.

Joins

Up to 4 tables. Supports Join, LeftJoin, RightJoin, and navigation-based joins:

db.Users().Join<Order>((u, o) => u.UserId == o.UserId.Id)
    .Select((u, o) => (u.UserName, o.Total));

db.Users().Join(u => u.Orders)  // navigation-based
    .Select((u, o) => (u.UserName, o.Total));

Many<T> properties support Any(), All(), and Count() in WHERE clauses, translated to correlated EXISTS and COUNT subqueries:

db.Users().Where(u => u.Orders.Any());
db.Users().Where(u => u.Orders.Any(o => o.Total > 100));
db.Users().Where(u => u.Orders.Count() > 5);

Aggregates

db.Orders().GroupBy(o => o.Status)
    .Having(o => Sql.Count() > 5)
    .Select(o => (o.Status, Sql.Count(), Sql.Sum(o.Total)));

Markers: Sql.Count(), Sql.Sum(), Sql.Avg(), Sql.Min(), Sql.Max().

Insert

// Single — initializer-aware, only set properties generate columns
var id = await db.Users()
    .Insert(new User { UserName = "x", IsActive = true })
    .ExecuteScalarAsync<int>();

// Batch — column-selector + data-provider pattern
await db.Users()
    .InsertBatch(u => (u.UserName, u.IsActive))
    .Values(users)
    .ExecuteNonQueryAsync();

Update

await db.Users().Update()
    .Set(u => { u.UserName = "New"; u.IsActive = true; })
    .Where(u => u.UserId == 1)
    .ExecuteNonQueryAsync();

Delete

await db.Users().Delete()
    .Where(u => u.UserId == 1)
    .ExecuteNonQueryAsync();

Update and Delete require Where() or All() before execution (QRY012).

Execution Terminals

Method Returns
ExecuteFetchAllAsync() Task<List<T>>
ExecuteFetchFirstAsync() Task<T> (throws if empty)
ExecuteFetchFirstOrDefaultAsync() Task<T?>
ExecuteFetchSingleAsync() Task<T> (throws if not exactly one)
ExecuteScalarAsync<T>() Task<T>
ExecuteNonQueryAsync() Task<int>
ToAsyncEnumerable() IAsyncEnumerable<T>
ToDiagnostics() QueryDiagnostics
Prepare() PreparedQuery<T>

Inline Constants and Collection Parameters

The generator detects constant values and emits them as SQL literals (no parameter overhead):

db.Users().Where(u => u.Status == "Active")
// Generated: WHERE "Status" = 'Active'  — literal, no parameter

Collection parameters expand to IN clauses with per-element binding:

var ids = new[] { 1, 2, 3 };
db.Users().Where(u => ids.Contains(u.UserId))
// Generated: WHERE "UserId" IN (@p0, @p1, @p2)

Schema Features

Column Types

Type Purpose
Key<T> Primary key
Col<T> Standard column
Ref<TSchema, TKey> Foreign key with navigation
Many<T> One-to-many navigation (compile-time marker)

Column Modifiers

Identity(), ClientGenerated(), Computed(), Length(n), Precision(p, s), Default(v), Default(() => v), MapTo("name"), Mapped<TMapping>(), Sensitive(), Unique(), Collation("...").

Indexes

public Index IX_Email => Index(Email).Unique();
public Index IX_Created => Index(CreatedAt.Desc());
public Index IX_Active => Index(Email).Where(IsActive);
public Index IX_Covering => Index(Email).Include(UserName, CreatedAt);

Fluent modifiers: Unique(), Where(col), Where("raw SQL"), Include(columns...), Using(IndexType).

Custom Type Mappings

public class MoneyMapping : TypeMapping<Money, decimal>
{
    public override decimal ToDb(Money value) => value.Amount;
    public override Money FromDb(decimal value) => new Money(value);
}

// In schema:
public Col<Money> Price => Mapped<MoneyMapping>();

Generator Diagnostics (QRY Series)

Errors

ID Title
QRY002 Missing Table property on schema class
QRY003 Invalid column type with no TypeMapping
QRY004 Navigation references unknown entity
QRY006 Unsupported operation in Where expression
QRY007 Join references undefined relationship
QRY009 Aggregate without GroupBy
QRY010 Composite primary keys not supported
QRY011 Select() required before execution terminal
QRY012 Update/Delete requires Where() or All()
QRY013 GUID key requires ClientGenerated()
QRY014 Anonymous type projection not supported
QRY017 TypeMapping type mismatch
QRY018 Duplicate TypeMapping for same type
QRY020 All() requires a predicate
QRY021 Subquery entity not found in context
QRY022 Subquery FK column not found
QRY024 Subquery on non-navigation property
QRY025 Subquery on composite-PK entity
QRY027 Invalid EntityReader type
QRY029 Sql.Raw placeholder mismatch
QRY032 Query chain not analyzable
QRY033 Forked query chain (multiple terminals on same builder variable)
QRY035 PreparedQuery escapes method scope
QRY036 PreparedQuery has no terminals
QRY052 Migration version gap or duplicate

Warnings

ID Title
QRY001 Query not fully analyzable (runtime fallback)
QRY005 Unmapped property in Select projection
QRY008 Potential SQL injection in Sql.Raw
QRY015 Ambiguous context resolution for entity
QRY016 Unbound parameter placeholder in generated SQL
QRY019 Clause not translatable at compile time
QRY023 Subquery FK-to-PK correlation ambiguous
QRY028 Redundant unique constraint (column + index)
QRY034 .Trace() requires QUARRY_TRACE define
QRY050 Schema changed since last migration snapshot
QRY051 Migration references unknown table/column
QRY054 Destructive migration without backup
QRY055 Nullable to non-null without data migration

Info

ID Title
QRY026 Custom EntityReader active
QRY030 Query chain optimized
QRY053 Pending migrations detected

Multi-Dialect Support

Four SQL dialects with correct quoting, parameter formatting, pagination, and identity/returning syntax. Multiple contexts with different dialects can coexist in the same project — each generates its own interceptor file.

[QuarryContext(Dialect = SqlDialect.SQLite)]
public partial class LiteDb : QuarryContext { ... }

[QuarryContext(Dialect = SqlDialect.PostgreSQL, Schema = "public")]
public partial class PgDb : QuarryContext { ... }

Raw SQL

Source-generated typed readers — zero reflection:

await db.RawSqlAsync<User>("SELECT * FROM users WHERE id = @p0", userId);
await db.RawSqlScalarAsync<int>("SELECT COUNT(*) FROM users");
await db.RawSqlNonQueryAsync("DELETE FROM logs WHERE date < @p0", cutoff);

Migrations

The generator emits a MigrateAsync method on each context for runtime migration execution:

await db.MigrateAsync(connection);
await db.MigrateAsync(connection, new MigrationOptions
{
    TargetVersion = 5,
    DryRun = true,
    RunBackups = true
});

Migration scaffolding is handled by the quarry CLI tool — see Quarry.Tool for CLI documentation.


Logging

Quarry uses Logsmith for structured logging with categories: Quarry.Query, Quarry.Modify, Quarry.Execution, Quarry.Parameters, Quarry.Connection, Quarry.Migration, Quarry.RawSql.

Slow query detection is configurable per context:

db.SlowQueryThreshold = TimeSpan.FromSeconds(1);

Mark columns with Sensitive() in the schema to redact parameter values in all log output.

There are no supported framework assets in this package.

Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Quarry.Generator:

Package Downloads
Quarry

Type-safe SQL builder and query reader for .NET 10 with AOT support

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.1 90 3/29/2026
0.2.0 91 3/29/2026
0.1.0 105 3/13/2026