Analyzer Rules
Quarry ships two independent diagnostic systems that report issues at compile time:
- QRY (Generator Diagnostics) -- Emitted by
Quarry.Generator, the Roslyn source generator that builds SQL. These diagnostics cover schema errors, query translation failures, chain analyzability, migration integrity, and internal generator faults. They are always active when the generator package is referenced. - QRA (Analyzer Rules) -- Emitted by
Quarry.Analyzers, a separate Roslyn analyzer package. These are optional, advisory rules that inspect query patterns for simplification opportunities, wasteful constructs, performance pitfalls, anti-patterns, and dialect-specific issues. Three rules (QRA101, QRA102, QRA201) include automatic code fixes.
Both systems surface diagnostics in the Error List, dotnet build output, and IDE squiggles like any other Roslyn diagnostic.
Installing Quarry.Analyzers
Add the analyzer and (optionally) its code fix companion to your .csproj:
<PackageReference Include="Quarry.Analyzers" Version="1.0.0"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
<!-- Optional: enables lightbulb code fixes in Visual Studio / Rider -->
<PackageReference Include="Quarry.Analyzers.CodeFixes" Version="1.0.0"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
No additional configuration is required. All QRA rules are enabled by default and can be tuned via EditorConfig or #pragma directives (see Suppressing Diagnostics below).
Generator Diagnostics (QRY)
Query (QRY001--QRY019)
| Code | Severity | Description |
|---|---|---|
| QRY001 | Warning | Query chain not fully analyzable |
| QRY002 | Error | Missing Table property on schema |
| QRY003 | Error | Invalid column type |
| QRY004 | Error | Unknown navigation entity |
| QRY005 | Error | Unmapped projection property |
| QRY006 | Error | Unsupported Where operator |
| QRY007 | Error | Undefined join relationship |
| QRY008 | Warning | Sql.Raw usage risk |
| QRY009 | Error | Aggregate without GroupBy |
| QRY010 | Error | Composite key unsupported in this context |
| QRY011 | Error | Select clause required |
| QRY012 | Error | Where or All required for modifications |
| QRY013 | Error | GUID column needs ClientGenerated modifier |
| QRY014 | Error | Anonymous type unsupported in this context |
| QRY015 | Warning | Ambiguous context resolution |
| QRY016 | Error | Unbound parameter |
| QRY019 | Warning | Clause not translatable |
Subquery (QRY020--QRY025)
| Code | Severity | Description |
|---|---|---|
| QRY020 | Error | All() requires a predicate |
| QRY021 | Error | Subquery entity not found |
| QRY022 | Error | Foreign key not found for correlation |
| QRY023 | Error | Correlation ambiguous |
| QRY024 | Error | Subquery on non-Many property |
| QRY025 | Error | Composite PK not supported for subqueries |
Entity Reader (QRY026--QRY027)
| Code | Severity | Description |
|---|---|---|
| QRY026 | Info | Custom entity reader active |
| QRY027 | Error | Invalid entity reader type |
Chain (QRY028--QRY036)
| Code | Severity | Description |
|---|---|---|
| QRY028 | Warning | Redundant unique constraint on index |
| QRY029 | Warning | Sql.Raw placeholder mismatch |
| QRY030 | Info | Prebuilt dispatch optimization applied |
| QRY032 | Error | Chain not analyzable at compile time |
| QRY033 | Error | Forked chain (multiple terminals) |
| QRY034 | Warning | .Trace() without QUARRY_TRACE symbol |
| QRY035 | Error | PreparedQuery escapes method scope |
| QRY036 | Error | .Prepare() with no terminal calls |
Migration (QRY050--QRY055)
| Code | Severity | Description |
|---|---|---|
| QRY050 | Warning | Schema drift detected |
| QRY051 | Error | Unknown table or column reference |
| QRY052 | Error | Version gap or duplicate |
| QRY053 | Warning | Pending migrations not applied |
| QRY054 | Warning | Destructive operation without backup |
| QRY055 | Warning | Nullable-to-non-null column change |
Internal
| Code | Severity | Description |
|---|---|---|
| QRY900 | Error | Internal generator error |
Common QRY Diagnostics
These are the generator diagnostics you are most likely to encounter during normal development.
QRY032 -- Chain not analyzable at compile time
Quarry requires every query chain to be fully analyzable by the source generator. If the generator cannot trace the chain from entry point to terminal, it emits QRY032. Common causes:
- Storing a builder in a field, property, or collection instead of a local variable.
- Passing a builder across method boundaries (as a parameter or return value).
- Building a chain inside a loop where the iteration variable influences the chain structure.
// QRY032 -- builder escapes method scope
IQueryBuilder<User> BuildQuery() =>
db.Users().Where(u => u.IsActive);
// Fix: keep the entire chain in one method
var users = await db.Users()
.Where(u => u.IsActive)
.Select(u => u)
.ExecuteFetchAllAsync();
Use conditional branches (if/else) instead of dynamic chain construction -- the generator handles those natively via bitmask dispatch.
QRY011 -- Select clause required
Every query chain must include a .Select() call before a terminal. The generator needs it to know which columns to emit.
// QRY011 -- missing Select
var users = await db.Users()
.Where(u => u.IsActive)
.ExecuteFetchAllAsync();
// Fix: add Select
var users = await db.Users()
.Where(u => u.IsActive)
.Select(u => u)
.ExecuteFetchAllAsync();
QRY012 -- Where or All required for modifications
Update and Delete operations require an explicit Where() or All() call to prevent accidental full-table modifications.
// QRY012 -- no scope for delete
await db.Users().Delete().ExecuteNonQueryAsync();
// Fix: add Where or acknowledge All
await db.Users().Delete().Where(u => u.UserId == 1).ExecuteNonQueryAsync();
await db.Users().Delete().All().ExecuteNonQueryAsync(); // explicit full-table
Analyzer Rules (QRA)
These rules are provided by the Quarry.Analyzers package and are independent of the source generator. They perform pattern-based analysis of Quarry query call sites.
Simplification (QRA101--QRA106)
Rules that detect query patterns which can be written more simply.
| Code | Severity | Description | Code Fix |
|---|---|---|---|
| QRA101 | Info | Count compared to zero -- Count() > 0 or Count() == 0 can be replaced with Any() |
Yes |
| QRA102 | Info | Single-value IN clause -- new[] { x }.Contains(col) simplifies to col == x |
Yes |
| QRA103 | Info | Tautological condition -- always-true conditions like 1 == 1 or col == col |
|
| QRA104 | Info | Contradictory condition -- always-false conditions like x > 5 && x < 3 |
|
| QRA105 | Info | Redundant condition -- a condition subsumed by a stronger one (e.g., x > 5 && x > 3) |
|
| QRA106 | Info | Nullable column without null check -- nullable column compared with == without null handling |
QRA101 code fix rewrites Count() > 0 to Any() and Count() == 0 to !Any(), handling async variants.
QRA102 code fix converts new[] { x }.Contains(col) to col == x.
Wasteful Patterns (QRA201--QRA205)
Rules that detect unnecessary work in query construction.
| Code | Severity | Description | Code Fix |
|---|---|---|---|
| QRA201 | Warning | Unused join -- joined table not referenced in SELECT, WHERE, or ORDER BY | Yes |
| QRA202 | Info | Wide table SELECT -- Select(u => u) on a table exceeding the column threshold (default: 10) |
|
| QRA203 | Info | ORDER BY without LIMIT -- sorting without pagination on an unbounded result set | |
| QRA204 | Info | Duplicate projection column -- same column projected multiple times in SELECT | |
| QRA205 | Warning | Cartesian product -- JOIN with missing or trivial ON condition (1 == 1) |
QRA201 code fix removes the unused .Join(...) call from the query chain, preserving the receiver.
Performance (QRA301--QRA304)
Rules that flag potential performance concerns, primarily around index usage.
| Code | Severity | Description |
|---|---|---|
| QRA301 | Info | Leading wildcard LIKE -- Contains() translates to LIKE '%...%', preventing index usage |
| QRA302 | Info | Function on column in WHERE -- ToLower(), ToUpper(), Substring(), etc. on a column prevents index usage |
| QRA303 | Info | OR across different columns -- col1 == x \|\| col2 == y prevents single-index scan |
| QRA304 | Info | WHERE on non-indexed column -- filtering on a column not covered by any declared schema index |
Pattern Issues (QRA401--QRA402)
Rules that detect common anti-patterns in how queries are used.
| Code | Severity | Description |
|---|---|---|
| QRA401 | Warning | Query inside loop -- execution method called inside for/foreach/while or LINQ .Select(), indicating an N+1 risk |
| QRA402 | Info | Multiple queries on same table -- multiple independent queries on the same entity within one method; consider combining |
Dialect (QRA501--QRA502)
Rules that detect dialect-specific issues or missed optimizations.
| Code | Severity | Description |
|---|---|---|
| QRA501 | Info | Dialect-specific optimization available -- e.g., PostgreSQL ILIKE instead of LOWER() + LIKE, SQLite COLLATE NOCASE |
| QRA502 | Warning | Suboptimal for dialect -- feature unsupported or problematic for the target dialect (e.g., SQLite RIGHT JOIN, SQL Server OFFSET without ORDER BY) |
Suppressing Diagnostics
When a diagnostic is intentional or not applicable, suppress it with standard C# mechanisms.
EditorConfig (project-wide)
Use .editorconfig to change severity or disable rules across the project:
# Disable leading-wildcard LIKE warnings entirely
dotnet_diagnostic.QRA301.severity = none
# Promote tautological condition from Info to Warning
dotnet_diagnostic.QRA103.severity = warning
The Quarry.Analyzers package also supports an EditorConfig option for the wide-table column threshold:
# QRA202: column threshold for wide-table detection (default: 10)
quarry_wide_table_column_threshold = 15
Pragma (per-site)
Use #pragma warning to suppress a specific diagnostic at a single call site:
#pragma warning disable QRA301
var results = await db.Users()
.Where(u => u.UserName.Contains(searchTerm))
.Select(u => u)
.ExecuteFetchAllAsync();
#pragma warning restore QRA301
SuppressMessage attribute
For method-level or class-level suppression:
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"QuarryAnalyzer", "QRA401",
Justification = "Batch size is bounded and intentional")]
public async Task ProcessItems(List<int> ids) { ... }