Diagnostics
Quarry provides diagnostics at two levels. At compile time, the source generator and analyzers report errors and warnings (QRY and QRA codes) when a query chain cannot be statically analyzed or contains anti-patterns. At runtime, ToDiagnostics() returns a QueryDiagnostics object that exposes the generated SQL, bound parameters, clause breakdown, projection metadata, and conditional variant information for any query chain. Together these give you full visibility into what SQL Quarry will execute, before it executes.
Query Diagnostics
ToDiagnostics() returns a QueryDiagnostics object with comprehensive compile-time analysis. It is available on all builder types (select, join, insert, update, delete, batch insert) and on PreparedQuery<T>.
var diag = db.Users()
.Where(u => u.IsActive)
.OrderBy(u => u.UserName)
.Select(u => u)
.ToDiagnostics();
Console.WriteLine(diag.Sql); // SELECT "UserId", "UserName", ... FROM "users" WHERE ...
Console.WriteLine(diag.Dialect); // SQLite
Console.WriteLine(diag.Kind); // Select
Console.WriteLine(diag.TableName); // users
Available Properties
| Property | Description |
|---|---|
Sql |
The generated SQL string for the current variant |
Parameters |
Active bound parameters (filtered by mask for conditional chains) |
AllParameters |
All parameters including inactive conditional ones |
Kind |
Select, Delete, Update, or Insert |
Dialect |
The SQL dialect |
TableName |
Target table name |
Clauses |
Per-clause SQL fragments with metadata |
SqlVariants |
All pre-built SQL variants keyed by conditional bitmask |
ActiveMask |
Current runtime bitmask for conditional clauses |
ConditionalBitCount |
Number of conditional bits in the chain |
ProjectionColumns |
SELECT column breakdown with types and ordinals |
ProjectionKind |
Entity, Dto, Tuple, or SingleColumn |
Joins |
JOIN metadata (table, kind, alias, ON condition) |
CarrierClassName |
Name of the generated carrier class |
IsDistinct |
Whether DISTINCT is applied |
Limit |
LIMIT value |
Offset |
OFFSET value |
IdentityColumnName |
Identity column for INSERT chains |
TierReason |
Human-readable tier classification explanation |
DisqualifyReason |
Why the chain is not PrebuiltDispatch (null when it is) |
UnmatchedMethodNames |
Method names that could not be matched to known operations |
Parameter Inspection
Each DiagnosticParameter carries metadata beyond just name and value:
foreach (var p in diag.Parameters)
{
Console.WriteLine($"{p.Name} = {p.Value}");
Console.WriteLine($" Type: {p.TypeName}, Sensitive: {p.IsSensitive}");
Console.WriteLine($" IsEnum: {p.IsEnum}, IsCollection: {p.IsCollection}");
Console.WriteLine($" IsConditional: {p.IsConditional}, BitIndex: {p.ConditionalBitIndex}");
}
Parameters only includes parameters from active clauses. Use AllParameters to see every parameter in the chain regardless of which conditional branches were taken.
Clause Inspection
foreach (var clause in diag.Clauses)
{
Console.WriteLine($"{clause.ClauseType}: {clause.SqlFragment}");
Console.WriteLine($" Active={clause.IsActive}, Conditional={clause.IsConditional}");
if (clause.IsConditional)
Console.WriteLine($" BitIndex={clause.ConditionalBitIndex}, Branch={clause.BranchKind}");
if (clause.SourceLocation is { } loc)
Console.WriteLine($" Defined at {loc.FilePath}:{loc.Line}:{loc.Column}");
foreach (var p in clause.Parameters)
Console.WriteLine($" Param: {p.Name} = {p.Value}");
}
For conditional chains, each clause reports IsConditional and IsActive so you can inspect which branches were taken. BranchKind is Independent for if-without-else and MutuallyExclusive for if/else pairs.
SQL Variant Inspection
When a query chain contains conditional branches, the generator pre-builds one SQL variant per possible clause combination. SqlVariants maps each bitmask to its SQL string:
var query = db.Users().Select(u => u);
if (activeOnly)
query = query.Where(u => u.IsActive);
if (sortByName)
query = query.OrderBy(u => u.UserName);
var diag = query.Limit(10).ToDiagnostics();
// 2 conditional bits = up to 4 variants
Console.WriteLine($"ConditionalBitCount: {diag.ConditionalBitCount}"); // 2
Console.WriteLine($"ActiveMask: {diag.ActiveMask}"); // depends on runtime values
if (diag.SqlVariants is not null)
{
foreach (var (mask, variant) in diag.SqlVariants)
{
Console.WriteLine($"Mask {mask}: {variant.Sql} ({variant.ParameterCount} params)");
}
// Mask 0: SELECT ... FROM "users" LIMIT 10 (no Where, no OrderBy)
// Mask 1: SELECT ... FROM "users" WHERE "IsActive" = 1 LIMIT 10 (Where only)
// Mask 2: SELECT ... FROM "users" ORDER BY "UserName" LIMIT 10 (OrderBy only)
// Mask 3: SELECT ... FROM "users" WHERE "IsActive" = 1 ORDER BY ... LIMIT 10 (both)
}
Projection Columns and ProjectionKind
For SELECT queries, ProjectionColumns lists every column in the result set with type information and ordinal position. ProjectionKind tells you the shape of the projection.
ProjectionKind Values
| Kind | Select Expression | Description |
|---|---|---|
Entity |
Select(u => u) |
Full entity -- all columns emitted |
Dto |
Select(u => new UserDto { Name = u.UserName }) |
Named DTO with mapped properties |
Tuple |
Select(u => (u.UserId, u.UserName)) |
Value tuple of selected columns |
SingleColumn |
Select(u => u.UserName) |
Single scalar column |
Inspecting Projection Columns
var diag = db.Users()
.Select(u => new UserDto { Name = u.UserName, Active = u.IsActive })
.ToDiagnostics();
Console.WriteLine($"ProjectionKind: {diag.ProjectionKind}"); // Dto
foreach (var col in diag.ProjectionColumns!)
{
Console.WriteLine($" [{col.Ordinal}] {col.PropertyName} -> {col.ColumnName} ({col.ClrType})");
Console.WriteLine($" Nullable={col.IsNullable}, FK={col.IsForeignKey}, Enum={col.IsEnum}");
if (col.IsForeignKey)
Console.WriteLine($" References: {col.ForeignKeyEntityName}");
if (col.TypeMappingClass is not null)
Console.WriteLine($" TypeMapping: {col.TypeMappingClass}");
}
This is useful for verifying that your projection maps to the columns you expect, and for confirming that custom type mappings and foreign key wrappers are applied correctly.
Trace
Trace is a compile-time debugging feature that causes the generator to emit // [Trace] comment lines inside the generated interceptor source code. These comments show what the generator did at each pipeline stage for a given chain, helping you diagnose unexpected SQL output or generator behavior.
Enabling Trace
Two things are required:
- Add
QUARRY_TRACEto your project'sDefineConstants:
<PropertyGroup>
<DefineConstants>$(DefineConstants);QUARRY_TRACE</DefineConstants>
</PropertyGroup>
- Add
.Trace()to the query chain you want to inspect:
var results = await db.Users()
.Where(u => u.IsActive)
.Select(u => u)
.Trace()
.ExecuteFetchAllAsync();
.Trace() is a compile-time-only signal. At runtime it is a no-op that returns the same builder instance. It can be placed anywhere in the chain.
Trace Categories
The generator emits trace information at each pipeline stage:
| Category | Scope | What It Shows |
|---|---|---|
| Discovery | Per call site | Detected method kind, entity type, chain ID, conditional flags |
| Binding | Per call site | Resolved entity, context, dialect, join relationships |
| Translation | Per call site | SQL expression binding, parameter extraction, rendered SQL fragment |
| ChainAnalysis | Per chain | Grouped sites, terminal detection, tier classification, bitmask allocation |
| Assembly | Per chain | Final SQL per mask variant, parameter counts |
| Carrier | Per chain | Carrier class name, fields, mask type, implemented interfaces |
After building, open the generated .g.cs interceptor file (found in the obj/GeneratedFiles directory when EmitCompilerGeneratedFiles is enabled) and search for // [Trace] to see the output.
QRY034 Warning
If you add .Trace() to a chain but QUARRY_TRACE is not defined in DefineConstants, the generator emits a QRY034 warning:
.Trace() found on chain but QUARRY_TRACE is not defined. Add
<DefineConstants>QUARRY_TRACE</DefineConstants>to enable trace output.
This prevents accidentally leaving .Trace() calls in code that will never produce output. Remove the .Trace() call or add the symbol to suppress the warning.
Using ToDiagnostics() for Testing
ToDiagnostics() is the primary tool for verifying that Quarry generates the SQL you expect. Since all SQL is pre-built at compile time, you can assert against it in unit tests without executing any database queries.
Basic SQL Assertion
var diag = db.Users()
.Where(u => u.IsActive)
.Select(u => (u.UserId, u.UserName))
.ToDiagnostics();
Assert.That(diag.Sql, Is.EqualTo(
"SELECT \"UserId\", \"UserName\" FROM \"users\" WHERE \"IsActive\" = 1"));
Assert.That(diag.Kind, Is.EqualTo(DiagnosticQueryKind.Select));
Assert.That(diag.Dialect, Is.EqualTo(SqlDialect.SQLite));
Cross-Dialect Verification
When you have multiple contexts with different dialects, you can verify the SQL for each:
// Build the same query against each dialect context
var lite = dbSqlite.Users().Where(u => u.IsActive).Select(u => (u.UserId, u.UserName)).Prepare();
var pg = dbPostgres.Users().Where(u => u.IsActive).Select(u => (u.UserId, u.UserName)).Prepare();
Assert.That(lite.ToDiagnostics().Sql, Is.EqualTo(
"SELECT \"UserId\", \"UserName\" FROM \"users\" WHERE \"IsActive\" = 1"));
Assert.That(pg.ToDiagnostics().Sql, Is.EqualTo(
"SELECT \"UserId\", \"UserName\" FROM \"users\" WHERE \"IsActive\" = TRUE"));
Verifying Conditional Queries
For conditional chains, verify that each variant produces the expected SQL:
string GetSql(bool activeOnly, bool sortByName)
{
var q = db.Users().Select(u => (u.UserId, u.UserName));
if (activeOnly)
q = q.Where(u => u.IsActive);
if (sortByName)
q = q.OrderBy(u => u.UserName);
return q.ToDiagnostics().Sql;
}
Assert.That(GetSql(false, false), Does.Not.Contain("WHERE"));
Assert.That(GetSql(true, false), Does.Contain("WHERE"));
Assert.That(GetSql(false, true), Does.Contain("ORDER BY"));
Assert.That(GetSql(true, true), Does.Contain("WHERE").And.Contain("ORDER BY"));
Verifying Modifications
ToDiagnostics() works on insert, update, and delete chains too:
var diag = db.Users()
.Update()
.Set(u => u.UserName = "Updated")
.Where(u => u.UserId == 1)
.ToDiagnostics();
Assert.That(diag.Kind, Is.EqualTo(DiagnosticQueryKind.Update));
Assert.That(diag.Sql, Does.Contain("SET \"UserName\""));
Assert.That(diag.Sql, Does.Contain("WHERE \"UserId\""));
Using Prepare for Multi-Terminal Testing
.Prepare() compiles the chain once and lets you call both ToDiagnostics() and an execution terminal on the same chain. This is the recommended pattern for tests that need to verify SQL and then execute:
var prepared = db.Users()
.Where(u => u.IsActive)
.Select(u => (u.UserId, u.UserName))
.Prepare();
// Verify SQL
var diag = prepared.ToDiagnostics();
Assert.That(diag.Sql, Does.Contain("WHERE"));
Assert.That(diag.ProjectionKind, Is.EqualTo("Tuple"));
// Then execute
var results = await prepared.ExecuteFetchAllAsync();
Assert.That(results, Has.Count.GreaterThan(0));
Compile-Time Diagnostics
See Analyzer Rules for the full list of QRY and QRA diagnostic codes.