Fix QRY037: CTE chains with captured inner and outer parameters (#305)#306
Open
DJGosnell wants to merge 9 commits into
Open
Fix QRY037: CTE chains with captured inner and outer parameters (#305)#306DJGosnell wants to merge 9 commits into
DJGosnell wants to merge 9 commits into
Conversation
) BuildSiteParamsMap and BuildParamConditionalMap advanced the global parameter offset by 0 for CteDefinition sites (their Clause is null), even though ChainAnalyzer allocates the CTE's inner params ahead of all clause params. Every param-bearing clause after a param-bearing With() then resolved its carrier P-fields short by the inner-param count: the outer Where assigned P0 (the CTE's slot) and P1 was never assigned, tripping the QRY037 self-check and making inner+outer parameterized CTE chains unbuildable. Advance the offset by the matching CteDef's inner-param count, matched by CTE short name (same first-match rule as EmitCteDefinition). Add a generation guard: the issue #305 chain shape produces no QRY037 and the generated interceptors assign both P0 and P1.
…CTE (#305) Pins the previously-unbuildable shape on all four dialects: exact WITH statement rendering (inner param at slot 0, outer Where param at slot 1) plus execute-and-verify against all four backends.
…test, exact-count assertions (#305) - F2: ConditionalOuterClause_AfterParameterizedCte_MySQL_NoQRY048 — the only shape that observes BuildParamConditionalMap's CteDefinition offset advance (verified to fail when only that branch is removed). - F3: Cte_TwoChainedWiths_CapturedParams_AndOuterCapturedParam — cumulative offset accumulation across two param-bearing CTEs plus an outer captured param, SQL + execution on all four dialects. - F4: generation guard tightened to exact-count P0/P1 assertions so a wrong-interceptor routing cannot pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
With<T>(...)lambda with a captured parameter in an outer clause (WhereafterFromCte<T>()).Reason for Change
The inner+outer parameterized CTE shape — valid SQL on all four dialects — was unbuildable: the generated carrier declared the outer parameter's field (
P1) but no clause interceptor assigned it, tripping the generator's QRY037 self-check.Root cause (differs from the issue's suspicion):
ChainAnalyzer's parameter rebasing andSqlAssembler's placeholder rebasing are both correct. The defect was inAssembledPlan.BuildSiteParamsMap/BuildParamConditionalMap: both walk clause entries accumulating the global parameter offset each emitter uses to route__c.P{n}assignments, but aCteDefinitionentry (whoseClauseis null — it is not clause-bearing) advanced the offset by 0 despite owningInnerParameters.Countslots in the chain's parameter list. Every param-bearing clause after a param-bearingWith()therefore resolved its carrier P-fields short by the inner-param count: the outerWherewroteP0(the CTE's slot) andP1was never assigned. With zero inner params the skew is zero — exactly why inner-only, two-inner, and outer-only chains all worked.Fix: advance the offset by the matching
CteDef.InnerParameters.Count, matched by CTE short name viaCteNameHelpers.ExtractShortName(the same first-match rule asTransitionBodyEmitter.EmitCteDefinition; duplicate names are already QRY082 errors, failed-analysis CTEs already QRY080). All emitter paths funnel through this one map, so the single fix point covers every consumer.Impact
CteDefinitionentries, and every shape whose offsets would differ was a hard QRY037 build error before.Plan items implemented as specified
AssembledPlanoffset-walk fix + generation guard (CteInnerAndOuterCapturedParams_NoQRY037_AssignsBothPFields) — verified to fail without the fix.Cte_FromCte_InnerAndOuterCapturedParams) — exact WITH rendering and row verification on all four backends.ParameterizedCteInnerAndOuterParams_OnMySQL_BindsInnerBeforeOuter), exactly as specified in QRY037: CTE chain with captured inner and outer parameters fails to build — outer carrier param field never assigned (valid SQL) #305, plus retirement of the QRY037 caveats in the siblingParameterizedCte*pins.Deviations from plan implemented
GetCteInnerParamCounthelper called from an inline branch in each walk (review F1, dismissed as benign — behavior identical, duplication reduced).Gaps in original plan implemented
Review findings addressed post-implementation:
ConditionalOuterClause_AfterParameterizedCte_MySQL_NoQRY048— the only shape that observes theBuildParamConditionalMaphalf of the fix (a conditional outer clause after a parameterized CTE on MySQL, newly reachable). Verified to fail when only that branch is removed.Cte_TwoChainedWiths_CapturedParams_AndOuterCapturedParam— cumulative offset accumulation across two param-bearing CTEs plus an outer captured param, on all four dialects.Deferred (review F5, class C, pending confirmation): consolidating the CteDef-by-short-name first-match lookup (now present in
AssembledPlan,CarrierAnalyzer, and twice inTransitionBodyEmitter) into a single shared helper — a cleanup, not a defect.Performance Considerations
The added lookup is a short linear scan over
Plan.CteDefinitions(almost always 0–3 entries) inside two already-cached map builders — no measurable generator cost.Breaking Changes