Skip to content

Commit f2feed1

Browse files
DJGosnellclaude
andauthored
Feat: Carrier class optimization for PrebuiltDispatch chains (#10)
* Feat: Carrier class optimization for PrebuiltDispatch chains (#9) Replace multi-allocation interceptor chains with a single file-sealed carrier class per analyzed chain. The carrier carries only the fields actually used (execution context, typed params, optional mask) and flows through interceptors via Unsafe.As casts. - Add QRY033 forked chain diagnostic (error) for builder variables consumed by multiple execution paths - Add InterceptorKind.WithTimeout for chain analysis tracking - Add QueryExecutor carrier execution methods that bypass QueryState (ExecuteCarrierAsync, ExecuteCarrierFirstAsync, etc.) - Add CarrierClassInfo/CarrierField/ChainParameterInfo models - Add CarrierClassBuilder to construct carrier class descriptions - Add InterceptorCodeGenerator.Carrier.cs for carrier class emission and carrier-aware clause body generation - Thread carrier info through existing interceptor generators so method signatures are shared and only the body differs - Carrier eligibility: single-expression fluent chains and linear variable-reassignment chains with unconditional first clause, SELECT queries only, no collection params or unresolved types Closes #9 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Refactor: Change entity set accessors from properties to methods Change QuarryContext entity set accessors from partial properties (db.Users) to partial methods (db.Users()) to enable [InterceptsLocation] interception of chain roots. This is a breaking API change required for carrier optimization of conditional variable-based chains. - Update ContextCodeGenerator to emit method syntax instead of property - Update ContextParser.DiscoverEntities to find MethodDeclarationSyntax - Update ScaffoldCodeGenerator for method syntax - Convert all 102 partial property declarations across 13 context files - Convert all 614 property access usages across 36 source files - Update doc comment examples in QuarryContext, QueryBuilder, Sql Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Feat: Add carrier base classes with interface implementations Add 8 abstract carrier base classes (CarrierBase<T>, CarrierBase<T,R>, JoinedCarrierBase variants for 2/3/4-entity joins) that implement all builder interface methods as explicit impls throwing InvalidOperationException. Generated carrier classes now inherit from the appropriate base class and declare only chain-specific fields (params, mask, pagination, timeout). The Ctx field moves to the base class. - Create CarrierBase.cs, JoinedCarrierBase.cs, JoinedCarrierBase3.cs, JoinedCarrierBase4.cs in Quarry/Internal/ - Update CarrierClassBuilder with base class selection algorithm - Add ResolveCarrierBaseClass in InterceptorCodeGenerator.Carrier.cs using proper tuple result type sanitization - Carrier classes now properly implement IQueryBuilder interfaces, enabling runtime type checks (builder is Chain_X) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Feat: Add Limit/Offset/Distinct/WithTimeout carrier interceptors Generate carrier-aware interceptors for Limit, Offset, Distinct, and WithTimeout clause types. Chains containing these clauses are now carrier-eligible instead of falling back to the non-carrier path. - Update early-skip in GenerateInterceptorMethod to allow carrier sites - Add GenerateCarrierPaginationInterceptor (Limit/Offset field setters) - Add GenerateCarrierDistinctInterceptor (noop — baked into SQL) - Add GenerateCarrierWithTimeoutInterceptor (Timeout field setter) - Add ResolveCarrierReceiverType helper with proper tuple handling - Remove Limit/Offset/Distinct/WithTimeout from eligibility exclusions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Feat: Add Delete/Update carrier branches and base classes Add carrier support for Delete and Update modification chains. - Create DeleteCarrierBase<T> and UpdateCarrierBase<T> in ModificationCarrierBase.cs implementing IDeleteBuilder, IUpdateBuilder, IExecutableDeleteBuilder, IExecutableUpdateBuilder interfaces - Add carrier branches to GenerateDeleteWhereInterceptor and GenerateUpdateWhereInterceptor in Modifications.cs - Update ResolveCarrierBaseClass to select Delete/Update base classes - Remove QueryKind.Select restriction from carrier eligibility - Remove DeleteWhere/UpdateWhere clause exclusions - Note: Set/UpdateSet clauses with open generic signatures remain excluded from carrier eligibility (they use BindParam on real builder) - Fix EmitCarrierClauseBody to use Unsafe.As for return type to handle interface crossings (IUpdateBuilder -> IExecutableUpdateBuilder) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Feat: Add nullable heuristic for carrier parameter fields Normalize carrier parameter field types: - Convert Nullable<T> to T? syntax - Append ? to reference types (string, class names) for #nullable enable - Preserve value types (int, decimal, etc.) without nullable annotation - Pass through already-nullable types, generics, and arrays Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix: Carrier eligibility validation and QRY033 false positive - Add WouldExecutionTerminalBeEmitted check to prevent carrier activation when the execution terminal would be skipped - Fix QRY033 false positive: skip fork detection for QuarryContext variables (context reuse across queries is expected) - Add ResolveExecutionResultTypePublic for cross-class access - Add 8 carrier generation snapshot tests Note: 95 integration test failures remain from the factory method refactor (§2) changing chain analysis for context-local-variable chains. The ResolveReceiverVariable now walks past db.Users() to find db as the receiver variable, changing chains from direct-fluent to variable-based analysis. This needs a deeper fix in chain analysis. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Feat: Add ChainRoot interception and fix chain analysis regression Register entity set factory methods (db.Users()) as InterceptorKind.ChainRoot. The ChainRoot interceptor creates the carrier directly from the context (zero QueryBuilder allocation) and is the unconditional entry point for all carrier chains. Fix chain analysis regression from factory method refactor: - ResolveReceiverVariable now skips QuarryContext locals/parameters (treats chains rooted on context as direct fluent) - DetectForkedChain skips context variables (QRY033 false positive fix) - Add WouldExecutionTerminalBeEmitted validation to prevent carrier activation when execution terminal would be skipped - Update inline test source strings for method syntax (db.Users()) Add ChainRoot to InterceptorKind and ClauseRole enums. Update UsageSiteDiscovery to detect context entity factory methods. Add GenerateCarrierChainRootInterceptor for carrier path. Add ResolveExecutionResultTypePublic for cross-class eligibility checks. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix: AnalyzabilityChecker and UsageSiteDiscovery for method-based accessors - Add InvocationExpressionSyntax handler in AnalyzabilityChecker for db.Users() pattern (method call on context returns builder) - Refine IsQuarryMethodCandidate to accept PascalCase parameterless method calls as potential context entity factory methods - IPropertySymbol audit: all 11 usages in generator confirmed safe — they naturally filter out methods via type checks Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Feat: Add IEntityAccessor<T> slim interface and EntityAccessor<T> struct IEntityAccessor<T> is the unified entry point for all entity operations. It does NOT extend IQueryBuilder<T> — it contains only chain-starting methods (Where, Select, Join, Distinct, WithTimeout, ToSql) plus modification entry points (Delete, Update, Insert, InsertMany). EntityAccessor<T> is a zero-allocation readonly struct for the runtime fallback path. Each method creates the appropriate builder on demand. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * WIP: IEntityAccessor unification — partial (136 build errors remain) Add IEntityAccessor<T> slim interface and EntityAccessor<T> struct as the unified entry point for all entity operations. Context methods now return IEntityAccessor<T> instead of IQueryBuilder<T>. Completed: - IEntityAccessor<T> interface (Where, Select, Join, Delete, Update, Insert) - EntityAccessor<T> readonly struct with public methods - ContextCodeGenerator returns IEntityAccessor<T> - ContextParser/UsageSiteDiscovery/ChainAnalyzer recognize EntityAccessor - CarrierBase<T> and CarrierBase<T,R> implement IEntityAccessor<T> - CarrierChainRoot interceptor returns IEntityAccessor<T> - 111 partial declarations updated - 171 Delete/Update/Insert call sites migrated - QuarryContext base Delete/Update/Insert methods removed Remaining (136 errors): - DeleteCarrierBase/UpdateCarrierBase need IEntityAccessor<T> stubs - All JoinedCarrierBase variants need IEntityAccessor<T> stubs - Test code using db.Users().GroupBy/Limit/OrderBy directly needs Where/Select first (not on slim accessor) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * WIP: IEntityAccessor — 0 build errors, runtime failures remain - Added IEntityAccessor<T> stubs to all carrier base classes (ModificationCarrierBase, JoinedCarrierBase, JoinedCarrierBase3/4) - Fixed GroupBy/Limit/OrderBy calls on accessor with Where(u => true) - Fixed cross-context Insert interceptor CS9144 with pragma and test simplification - Changed return type to IEntityAccessor<T> (interface) for carrier compatibility - 0 build errors achieved Runtime failures (~200): interceptor this parameter type mismatch. Generated interceptors have `this IQueryBuilder<T>` but call site receiver is now `IEntityAccessor<T>` (since db.Users() returns IEntityAccessor<T>). The interceptor generator needs to emit IEntityAccessor<T> as the receiver type for chain-starting methods. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Feat: Complete IEntityAccessor unification — 0 build errors, 2 pre-existing failures Complete the IEntityAccessor<T> unification: - Add IEntityAccessor<T> to BuilderTypeNames for interceptor discovery - Add ToReturnTypeName() mapping IEntityAccessor→IQueryBuilder for return types - Update all interceptor generators to use returnType for returns, thisType for receiver - Fix ResolveCarrierReceiverType to return IEntityAccessor when appropriate - Fix CarrierDistinctInterceptor return type for IEntityAccessor→IQueryBuilder crossing - Add IEntityAccessor<T> stubs to all carrier base classes (8 query + 2 modification) - Fix 112 test code patterns for slim accessor (GroupBy/Limit/OrderBy need Where first) - Fix generator assertion patterns for EntityAccessor return types - Suppress CS9144 for pre-existing cross-context Insert interceptor type mismatch - Remove QuarryContext Delete/Update/Insert base methods Remaining failures (2, both pre-existing): - Integration_DialectAwareMapping: NullRef in QueryState on fallback path - Generator_WithValidEntityReader: inline compilation interceptor discovery Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix: IEntityAccessor Unsafe.As crash in standalone interceptors When a standalone interceptor (Select, Where, Join) receives IEntityAccessor<T>, the builder is a boxed EntityAccessor struct — not a QueryBuilder. Unsafe.As<QueryBuilder<T>>(builder) reinterpreted the struct's memory layout as a class, causing NullReferenceException on QueryState.Parameters. Fix: Generator now emits ((EntityAccessor<T>)(object)builder) .CreateQueryBuilder() before Unsafe.As when receiver is IEntityAccessor. EntityAccessor.CreateQueryBuilder() made public for this purpose. Also fixes GeneratorTests.Generator_WithValidEntityReader test syntax (db.Users property → db.Users() method, complete chain with terminal). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Feat: Delete/Update/All transitions, WHERE TRUE elision, ExecuteScalar carrier fix - Add DeleteTransition, UpdateTransition, AllTransition InterceptorKinds so .Delete()/.Update()/.All() are recognized chain nodes with carrier noop interceptors (Unsafe.As cast between implemented interfaces) - Elide constant-true WHERE clauses (.Where(u => true) → "TRUE"/"1") in both standalone interceptors and prebuilt SQL dispatch tables - Fix ExecuteScalar carrier terminal: was missing from carrier executor switch, causing Unsafe.As<QueryBuilder> on carrier → AccessViolation - Fix EntityAccessor.ToSql() to delegate to CreateQueryBuilder().ToSql() instead of throwing - Update scaffold test expectations for IEntityAccessor<T> return type 0 build errors, 2777 passed, 1 skipped (inline projection analysis) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Feat: Add QueryPlan API, remove CS9144 suppression, document IParameterSymbol - Add QueryPlan type (Sql, Tier, Dialect) and QueryPlanTier enum - Add IEntityAccessor<T>.ToQueryPlan() for query diagnostics - Implement on EntityAccessor (delegates to CreateQueryBuilder().ToSql()) - Add throwing stubs to all 12 carrier base classes - Remove CS9144 NoWarn from test project — cross-context Insert interceptors are correctly scoped by the existing GroupByFileAndProcess logic; the suppression was vestigial - Remove stale CS9144 pragma and TODO comments from schema tests - Document IParameterSymbol handling in ResolveReceiverVariable: parameters lack declaration sites for variable-flow analysis, correctly treated as direct fluent chains 0 build errors, 2777 passed, 1 skipped Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Feat: Add ExecutionMode enum and 7 WithCommand carrier execution methods Phase 1 of carrier inline parameter binding. Adds command-based executor methods that accept a pre-built DbCommand from the terminal interceptor, separating command creation (terminal) from execution/materialization (executor). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Feat: Add IsSensitive, IsEnum, EnumUnderlyingType to chain parameter model Phase 2 of carrier inline parameter binding. Propagates enum type info from expression translation through ParameterInfo to ChainParameterInfo, enabling compile-time enum cast codegen in the terminal interceptor. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Feat: Add CarrierStaticField model, emit static FieldInfo caches on carrier class Phase 3 of carrier inline parameter binding. Carrier classes now declare static FieldInfo? F0, F1, ... fields for cached expression extraction, and the remark reflects the 1-allocation design. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Feat: Terminal inline parameter binding — eliminate param array allocation Phase 4 of carrier inline parameter binding. The carrier terminal now creates DbCommand inline with per-parameter CreateParameter/Value/Add, replacing the object?[] array + CreateCarrierCommand loop. Includes sensitivity-aware parameter logging and BoundSensitive log method. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Feat: Remap FieldInfo caches to carrier class, skip interceptor-class emission Phase 5 of carrier inline parameter binding. Clause interceptors now reference static FieldInfo caches on the carrier class (Chain_N.F0) instead of the interceptor class. FieldInfo declarations for carrier chain members are no longer emitted on the interceptor class. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Feat: Remove old array-based carrier executor methods and helpers Phase 7 cleanup. Removes ExecuteCarrierAsync (7 overloads), CreateCarrierCommand (2 overloads), and LogCarrierParameters — all superseded by the inline command-based WithCommand methods. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix: Only emit static FieldInfo cache for captured parameters Static FieldInfo? fields on the carrier class are only needed for parameters extracted via expression tree reflection. Skip emission for non-captured parameters to eliminate CS0649 warnings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix: Inline __timeout variable and fix UPDATE parameter offset for conditional WHERE clauses Inline the __timeout variable directly into CommandTimeout assignment, and fix a bug where conditional WHERE clause parameters in UPDATE statements were bound to the same index as the SET parameter (@p0) instead of being offset correctly (@p1). The root cause was that BuildTemplates did not create synthetic templates for UpdateSet clauses whose SetClauseInfo was lost during enrichment fallback, so ComputeParameterBaseOffsets did not account for the SET parameter in the running offset. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Refactor: Unify Delete/Update Where interceptors and extract shared modification helpers - Merge GenerateDeleteWhereInterceptor and GenerateUpdateWhereInterceptor into a single GenerateModificationWhereInterceptor parameterized by isDelete flag, eliminating ~160 lines of near-duplicate code - Extract GetColumnValueExpression helper for FK navigation and type mapping used across Insert, Update POCO, and other entity property extraction - Extract EmitInsertColumnSetup and EmitInsertEntityBindings shared helpers to deduplicate insert interceptor code - Fix CS8604 nullable warning in EmitCarrierExecutionTerminal parameter Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Feat: Add carrier optimization for Insert operations - Add InsertTransition InterceptorKind and ClauseRole for .Insert(entity) calls - Add QueryKind.Insert with pre-built INSERT SQL via CompileTimeSqlBuilder - Create InsertCarrierBase<T> runtime base class with IInsertBuilder<T> stubs - Add Entity field (FieldRole.Entity) to carrier class for insert chains - Generate carrier insert transition interceptor (stores entity on carrier) - Generate carrier insert execution terminals with inline parameter binding from entity properties (ExecuteNonQuery, ExecuteScalar, ToSql) - Extend chain analysis pipeline: IsExecutionKind, MapInterceptorKindToClauseRole, DetermineQueryKind, BuildChainParameters, and BuildPrebuiltChainInfo - Skip carrier path for MySQL ExecuteScalar inserts (requires separate SELECT LAST_INSERT_ID() query incompatible with single-command carrier) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix: Include execution sites in chain member tracking and add carrier tests - Add execution site UniqueIds to chainMemberUniqueIds and fileChainMemberIds so non-analyzable execution sites (like InsertExecuteNonQuery) are included in the site list passed to the code generator - Skip ShouldSkipNonTranslatableClause for carrier-optimized sites since their metadata is on the chain, not the individual site - Add carrier generation tests for Insert ExecuteNonQuery, Insert ExecuteScalar, and Update with Set+Where chains Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix: Address PR review items for carrier optimization - Suppress CS8602 warnings in generated interceptor code via pragma - Remove planning documents (impl-plan*.md) from repo - Guard against null EnumUnderlyingType in GetParameterValueExpression - Extract shared terminal eligibility predicates (CanEmitReaderTerminal, CanEmitScalarTerminal, CanEmitNonQueryTerminal, CanEmitInsertTerminal) used by both WouldExecutionTerminalBeEmitted and terminal generators - Fix mapping field name reference (s_ prefix → GetMappingFieldName) - Add IDialectAwareTypeMapping.ConfigureParameter for mapped types in inline command creation, matching QueryExecutor.CreateCommand behavior Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix: Reject 3+ param lambdas in two-entity join translator to enable carrier optimization ExtractTwoParameterLambdaExpression used `< 2` instead of `!= 2`, causing it to accept chained join lambdas (e.g., 4-param `(u, o, oi, p) => ...`) and return only the first two parameter names. This produced a "successful" but incorrect JoinClauseInfo during discovery (e.g., `ON @p0 = @p1` instead of column references), which prevented the enrichment phase from re-translating with the correct multi-entity context via TranslateChainedJoinFromEntityInfo. With the fix, chained join lambdas now correctly fall through to the enrichment path, producing proper column-to-column SQL (e.g., `ON "t2"."ProductName" = "t3"."ProductName"`). Since join conditions resolve to zero runtime parameters, BuildChainParameters returns an empty list, making 4-way join chains carrier-eligible using the existing JoinedCarrierBase4 infrastructure. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3cc8fdc commit f2feed1

92 files changed

Lines changed: 6361 additions & 1404 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/Quarry.Benchmarks/Benchmarks/AggregateBenchmarks.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public async Task<int> EfCore_Count()
3333
[Benchmark]
3434
public async Task<int> Quarry_Count()
3535
{
36-
return await QuarryDb.Users
36+
return await QuarryDb.Users()
3737
.Select(u => Sql.Count())
3838
.ExecuteScalarAsync<int>();
3939
}
@@ -64,7 +64,7 @@ public async Task<decimal> EfCore_Sum()
6464
[Benchmark]
6565
public async Task<decimal> Quarry_Sum()
6666
{
67-
return await QuarryDb.Orders
67+
return await QuarryDb.Orders()
6868
.Select(o => Sql.Sum(o.Total))
6969
.ExecuteScalarAsync<decimal>();
7070
}
@@ -95,7 +95,7 @@ public async Task<decimal> EfCore_Avg()
9595
[Benchmark]
9696
public async Task<decimal> Quarry_Avg()
9797
{
98-
return await QuarryDb.Orders
98+
return await QuarryDb.Orders()
9999
.Select(o => Sql.Avg(o.Total))
100100
.ExecuteScalarAsync<decimal>();
101101
}

src/Quarry.Benchmarks/Benchmarks/ComplexQueryBenchmarks.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public async Task<List<UserOrderDto>> EfCore_JoinFilterPaginate()
6161
[Benchmark]
6262
public async Task<List<UserOrderDto>> Quarry_JoinFilterPaginate()
6363
{
64-
return await QuarryDb.Users
64+
return await QuarryDb.Users()
6565
.Where(u => u.IsActive)
6666
.Join<Order>((u, o) => u.UserId == o.UserId.Id)
6767
.Select((u, o) => new UserOrderDto
@@ -116,7 +116,7 @@ public async Task<int> EfCore_MultiJoinAggregate()
116116
[Benchmark]
117117
public async Task<int> Quarry_MultiJoinAggregate()
118118
{
119-
var results = await QuarryDb.Users
119+
var results = await QuarryDb.Users()
120120
.Where(u => u.IsActive)
121121
.Join<Order>((u, o) => u.UserId == o.UserId.Id)
122122
.Join<OrderItem>((u, o, oi) => o.OrderId == oi.OrderId.Id)

src/Quarry.Benchmarks/Benchmarks/FilterBenchmarks.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public async Task<List<EfUser>> EfCore_WhereActive()
5050
[Benchmark]
5151
public async Task<List<EfUser>> Quarry_WhereActive()
5252
{
53-
return await QuarryDb.Users
53+
return await QuarryDb.Users()
5454
.Where(u => u.IsActive)
5555
.Select(u => new EfUser
5656
{
@@ -109,7 +109,7 @@ public async Task<List<UserSummaryDto>> EfCore_WhereCompound()
109109
[Benchmark]
110110
public async Task<List<UserSummaryDto>> Quarry_WhereCompound()
111111
{
112-
return await QuarryDb.Users
112+
return await QuarryDb.Users()
113113
.Where(u => u.IsActive)
114114
.Where(u => u.Email != null)
115115
.Select(u => new UserSummaryDto
@@ -163,7 +163,7 @@ public async Task<List<UserSummaryDto>> Quarry_WhereCompound()
163163
[Benchmark]
164164
public async Task<EfUser?> Quarry_WhereById()
165165
{
166-
return await QuarryDb.Users
166+
return await QuarryDb.Users()
167167
.Where(u => u.UserId == 42)
168168
.Select(u => new EfUser
169169
{

src/Quarry.Benchmarks/Benchmarks/InsertBenchmarks.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public async Task<int> EfCore_SingleInsert()
6565
[Benchmark]
6666
public async Task<int> Quarry_SingleInsert()
6767
{
68-
return await QuarryDb.Insert(new User
68+
return await QuarryDb.Users().Insert(new User
6969
{
7070
UserName = "BenchUser",
7171
Email = "bench@example.com",
@@ -134,6 +134,6 @@ public async Task<int> Quarry_BatchInsert10()
134134
IsActive = true,
135135
CreatedAt = DateTime.UtcNow
136136
});
137-
return await QuarryDb.InsertMany(users).ExecuteNonQueryAsync();
137+
return await QuarryDb.Users().InsertMany(users).ExecuteNonQueryAsync();
138138
}
139139
}

src/Quarry.Benchmarks/Benchmarks/JoinBenchmarks.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public async Task<List<UserOrderDto>> EfCore_InnerJoin()
4747
[Benchmark]
4848
public async Task<List<UserOrderDto>> Quarry_InnerJoin()
4949
{
50-
return await QuarryDb.Users
50+
return await QuarryDb.Users()
5151
.Join<Order>((u, o) => u.UserId == o.UserId.Id)
5252
.Select((u, o) => new UserOrderDto
5353
{
@@ -112,7 +112,7 @@ public async Task<List<UserOrderItemDto>> EfCore_ThreeTableJoin()
112112
[Benchmark]
113113
public async Task<List<UserOrderItemDto>> Quarry_ThreeTableJoin()
114114
{
115-
return await QuarryDb.Users
115+
return await QuarryDb.Users()
116116
.Join<Order>((u, o) => u.UserId == o.UserId.Id)
117117
.Join<OrderItem>((u, o, oi) => o.OrderId == oi.OrderId.Id)
118118
.Select((u, o, oi) => new UserOrderItemDto

src/Quarry.Benchmarks/Benchmarks/PaginationBenchmarks.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public async Task<List<EfUser>> EfCore_LimitOffset()
5151
[Benchmark]
5252
public async Task<List<EfUser>> Quarry_LimitOffset()
5353
{
54-
return await QuarryDb.Users
54+
return await QuarryDb.Users()
5555
.Select(u => new EfUser
5656
{
5757
UserId = u.UserId,
@@ -108,7 +108,7 @@ public async Task<List<EfUser>> EfCore_FirstPage()
108108
[Benchmark]
109109
public async Task<List<EfUser>> Quarry_FirstPage()
110110
{
111-
return await QuarryDb.Users
111+
return await QuarryDb.Users()
112112
.Select(u => new EfUser
113113
{
114114
UserId = u.UserId,

src/Quarry.Benchmarks/Benchmarks/SelectBenchmarks.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public async Task<List<EfUser>> EfCore_SelectAll()
4848
[Benchmark]
4949
public async Task<List<EfUser>> Quarry_SelectAll()
5050
{
51-
return await QuarryDb.Users
51+
return await QuarryDb.Users()
5252
.Select(u => new EfUser
5353
{
5454
UserId = u.UserId,
@@ -105,7 +105,7 @@ public async Task<List<UserSummaryDto>> EfCore_SelectProjection()
105105
[Benchmark]
106106
public async Task<List<UserSummaryDto>> Quarry_SelectProjection()
107107
{
108-
return await QuarryDb.Users
108+
return await QuarryDb.Users()
109109
.Select(u => new UserSummaryDto
110110
{
111111
UserId = u.UserId,

src/Quarry.Benchmarks/Benchmarks/StringOpBenchmarks.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public async Task<List<UserSummaryDto>> EfCore_Contains()
5353
[Benchmark]
5454
public async Task<List<UserSummaryDto>> Quarry_Contains()
5555
{
56-
return await QuarryDb.Users
56+
return await QuarryDb.Users()
5757
.Where(u => u.UserName.Contains("User05"))
5858
.Select(u => new UserSummaryDto
5959
{
@@ -109,7 +109,7 @@ public async Task<List<UserSummaryDto>> EfCore_StartsWith()
109109
[Benchmark]
110110
public async Task<List<UserSummaryDto>> Quarry_StartsWith()
111111
{
112-
return await QuarryDb.Users
112+
return await QuarryDb.Users()
113113
.Where(u => u.UserName.StartsWith("User0"))
114114
.Select(u => new UserSummaryDto
115115
{

src/Quarry.Benchmarks/Context/BenchDb.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace Quarry.Benchmarks.Context;
55
[QuarryContext(Dialect = SqlDialect.SQLite)]
66
public partial class BenchDb : QuarryContext
77
{
8-
public partial IQueryBuilder<User> Users { get; }
9-
public partial IQueryBuilder<Order> Orders { get; }
10-
public partial IQueryBuilder<OrderItem> OrderItems { get; }
8+
public partial IEntityAccessor<User> Users();
9+
public partial IEntityAccessor<Order> Orders();
10+
public partial IEntityAccessor<OrderItem> OrderItems();
1111
}

src/Quarry.Generator/DiagnosticDescriptors.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,21 @@ internal static class DiagnosticDescriptors
444444
"The existing runtime SqlBuilder path will be used. This is not an error — " +
445445
"consider restructuring the query to enable optimization.");
446446

447+
/// <summary>
448+
/// QRY033: Forked query chain — builder variable consumed by multiple execution paths.
449+
/// Severity: Error
450+
/// </summary>
451+
public static readonly DiagnosticDescriptor ForkedQueryChain = new(
452+
id: "QRY033",
453+
title: "Forked query chain",
454+
messageFormat: "Query builder variable '{0}' is consumed by multiple execution paths. Each execution path must use its own builder chain expression.",
455+
category: Category,
456+
defaultSeverity: DiagnosticSeverity.Error,
457+
isEnabledByDefault: true,
458+
description: "A query builder variable is used as the receiver for multiple execution-terminating calls " +
459+
"(e.g., ExecuteFetchAllAsync). Each execution path must use its own independent builder chain " +
460+
"to avoid confusing aliasing behavior from the immutable builder contract.");
461+
447462
// ─── Migration diagnostics (QRY050–QRY055) ────────────────────────
448463

449464
/// <summary>

0 commit comments

Comments
 (0)