Prepared Queries
The Problem
A typical Quarry query chain ends with a single terminal method -- ExecuteFetchAllAsync, ToDiagnostics, etc. Each terminal triggers the source generator to emit an interceptor for that specific call site. This works well when you need one result, but sometimes you need to do more than one thing with the same query: inspect the generated SQL, then execute it; or fetch all rows and also stream them. Without .Prepare(), you would have to duplicate the entire chain for each terminal, causing the generator to emit redundant interceptors with identical SQL.
.Prepare() solves this by freezing a query chain into a PreparedQuery<TResult> that you can call multiple terminal methods on -- build once, execute multiple ways.
Basic Usage
var prepared = db.Users()
.Where(u => u.IsActive)
.Select(u => (u.UserId, u.UserName))
.Prepare();
var diag = prepared.ToDiagnostics(); // inspect SQL
var all = await prepared.ExecuteFetchAllAsync(); // fetch all rows
var first = await prepared.ExecuteFetchFirstAsync(); // fetch first row
With Modifications
.Prepare() works with insert, update, and delete chains too:
var prepared = db.Users()
.Insert(new User { UserName = "x", IsActive = true })
.Prepare();
var diag = prepared.ToDiagnostics();
var affected = await prepared.ExecuteNonQueryAsync();
Available Terminal Methods
PreparedQuery<TResult> exposes the same terminal methods available on regular query chains:
| Method | Returns | Description |
|---|---|---|
ExecuteFetchAllAsync() |
Task<List<TResult>> |
Execute and return all rows |
ExecuteFetchFirstAsync() |
Task<TResult> |
Execute and return the first row (throws if empty) |
ExecuteFetchFirstOrDefaultAsync() |
Task<TResult?> |
Execute and return the first row, or default if empty |
ExecuteFetchSingleAsync() |
Task<TResult> |
Execute and return exactly one row (throws if not exactly one) |
ExecuteScalarAsync<TScalar>() |
Task<TScalar> |
Execute and return a scalar value |
ExecuteNonQueryAsync() |
Task<int> |
Execute a modification and return rows affected |
ToAsyncEnumerable() |
IAsyncEnumerable<TResult> |
Stream results row by row |
ToDiagnostics() |
QueryDiagnostics |
Inspect SQL, parameters, and optimization metadata without executing |
Not all methods make sense on every query type. For example, ExecuteNonQueryAsync applies to insert/update/delete chains, while ExecuteFetchAllAsync applies to select chains. The generator validates this at compile time.
Inspecting SQL with ToDiagnostics
ToDiagnostics() is particularly useful on a prepared query for verifying the generated SQL before executing it -- for example, in tests or during development:
var prepared = db.Users()
.Where(u => u.IsActive)
.OrderBy(u => u.UserName)
.Select(u => (u.UserId, u.UserName))
.Prepare();
var diag = prepared.ToDiagnostics();
// Inspect before executing
Console.WriteLine(diag.Sql); // SELECT "UserId", "UserName" FROM "users" WHERE "IsActive" = 1 ORDER BY "UserName"
Console.WriteLine(diag.Dialect); // SQLite
Console.WriteLine(diag.Tier); // PrebuiltDispatch
foreach (var p in diag.Parameters)
Console.WriteLine($"{p.Name} = {p.Value}");
// Now execute
var results = await prepared.ExecuteFetchAllAsync();
Since ToDiagnostics() does not hit the database, calling it before execution is a zero-cost way to assert SQL correctness in unit tests:
var q = db.Users().Where(u => u.IsActive).Select(u => u).Prepare();
Assert.That(q.ToDiagnostics().Sql, Does.Contain("WHERE"));
var users = await q.ExecuteFetchAllAsync();
Scope Constraint
PreparedQuery variables must not escape their declaring method scope. The generator needs to see the .Prepare() call and all terminals within the same method body to emit correct interceptors. If the variable escapes, the generator cannot track which terminals will be called.
The following patterns produce compile error QRY035 (PreparedQuery escapes scope):
Returning from a method:
// QRY035 - prepared query returned from method
PreparedQuery<User> GetQuery()
{
return db.Users().Select(u => u).Prepare();
}
Passing as an argument:
// QRY035 - prepared query passed to another method
var prepared = db.Users().Select(u => u).Prepare();
SomeMethod(prepared);
Lambda capture:
// QRY035 - prepared query captured by lambda
var prepared = db.Users().Select(u => u).Prepare();
var func = () => prepared.ExecuteFetchAllAsync();
The fix is always the same: keep the Prepare() call and all terminal calls in the same method, with no indirection.
Prepare with No Terminals (QRY036)
If you call .Prepare() but never call any terminal method on the resulting variable, the generator reports compile error QRY036. A prepared query with no terminals serves no purpose -- there is no SQL to emit.
// QRY036 - no terminal called on prepared query
var prepared = db.Users().Select(u => u).Prepare();
// ... prepared is never used
Performance
Single terminal: When only one terminal is called after .Prepare(), the generator produces identical code to calling that terminal directly on the chain. The PreparedQuery wrapper is elided entirely via Unsafe.As -- there is zero overhead compared to a non-prepared chain.
// These two produce identical generated code:
var a = await db.Users().Select(u => u).ExecuteFetchAllAsync();
var b = db.Users().Select(u => u).Prepare();
var result = await b.ExecuteFetchAllAsync();
Multiple terminals: When two or more terminals are called, the generator emits a carrier class that covers all observed terminal methods. Each terminal gets its own interceptor pointing at the same carrier, with pre-built SQL shared across them. The carrier handles parameter storage and clause-mask dispatch once, and each terminal reuses that state.