Warm one plan per prepared case - #4572
Open
sergei-pustovykh wants to merge 2 commits into
Open
Conversation
sergei-pustovykh
force-pushed
the
apple/sergei-pustovykh/stored-query/warm-up
branch
from
September 7, 2026 22:13
46c4dd3 to
d211368
Compare
Warm-up built one plan per stored query. A stored query with a signature now gets one plan per prepared case, planned the way a client's prepared statement would be, so a client whose bindings fit a case finds a plan already built for it. PreparedCaseParams turns one case into the parameters to plan it with. IS_NULL, IS_TRUE and IS_FALSE bind a real value, so the planner folds predicates exactly as it would at run time; IS_NOT_NULL binds nothing and the parameter is planned from its declaration instead. The value map is a HashMap rather than Map.of because IS_NULL binds a real null, and the distinction is load-bearing: hasNamedParamValue asks containsKey, so a parameter bound to null is value-bound and reaches folding, while one merely absent would be planned value-free. Warm-up resolves no types. Declarations are passed on as the text the signature stored, and the planning path turns one into a type when it meets the parameter. Every declaration is passed on, not just the ones a case leaves value-free, because a declaration is only read for a parameter that carries no value — so nothing has to be filtered per case and PreparedCaseParams stays a pure function, with no database, schema template or planner behind it. Temporary functions are planned inside the per-case loop rather than once per query, because a signature parameter captured by a function's body changes that function's plan too, so each case needs its own compile from the original template with a fresh factory. Compiling them once would have leaked one case's folded function into the next. A case that fails no longer skips the rest: the plan for the null case failing is no reason to give up the plan for the non-null one. queriesProcessed still counts queries, one increment either way, and the new plansWarmed says how much the cache was actually filled — an upper bound, since two cases differing only in a parameter the body never references produce the same plan under the same constraint. A parameter declared with a schema template type is not warmed when a case leaves it value-free: the built template keeps no named types, so the declaration cannot be resolved. The failure comes from the planning path and is contained like any per-case planning failure. A lookup by name on the built template is planned separately; when it lands these queries start warming with no change here.
sergei-pustovykh
force-pushed
the
apple/sergei-pustovykh/stored-query/warm-up
branch
from
September 8, 2026 09:39
5d6ed4b to
8484644
Compare
sergei-pustovykh
marked this pull request as ready for review
September 8, 2026 13:50
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.
Now a stored query with a signature gets one plan per
PREPARE FORcase.Each
PREPARE FORcase is planned the way a client's prepared statement would be: warm-up binds the value a case pins, or passes only the type when the case pins none. A client whose bindings fit that case then finds a plan already built for it.This is the last PR of the chain. Until now a signature and its
PREPARE FORcases were only syntax and storage; this is what makes them do something.What a
PREPARE FORcase becomesPreparedCaseParamsturns one case into thePreparedParamsto plan it with. A state either binds a value or leaves the parameter without one:IS_NULLIS_TRUEBoolean.TRUEIS_FALSEBoolean.FALSEIS_NOT_NULLDecisions worth attention
Warm-up resolves no types. A declaration is passed on as the text the signature stored, and the planning path turns it into a type when it meets the parameter — that is where the schema template is in hand. So this PR adds no parsing and no type handling of its own; it decides only what each case binds.
Every declaration is passed on, not just the ones a case leaves value-free, because a declaration is only read for a parameter that carries no value: one that is also bound never consults it. Nothing has to be filtered per case, so
PreparedCaseParamsstays a pure function — no database, no schema template, no planner.Temporary functions are planned inside the per-case loop. A signature parameter captured by a function's body changes that function's plan too, so each case needs its own compile, from the original template with a fresh factory. Compiling them once per query would have leaked one case's folded function into the next.
A failing case no longer skips the rest of the query. The plan for the null case failing is no reason to give up the plan for the non-null one, so the loop continues.
queriesProcessedstill counts queries — one increment either way, so the existing counter keeps its meaning — and the newplansWarmedsays how much the cache was actually filled.plansWarmedis an upper bound, stated as such in its javadoc: two cases differing only in a parameter the body never references produce the same plan under the same constraint, since such a parameter contributes no literal and no constant, and the second warm-up then replaces the first rather than adding to the cache. Rejecting an unreferenced signature parameter would remove that case, but it is a new validation rule and belongs with the others in the first PR, not here.What is not warmed
A parameter declared with a schema template type — a struct, an enum, or an array of either — is not warmed when a case leaves it value-free. The built schema template drops its named types (
auxiliaryTypeslive in the builder, are used byresolveTypes()and are gone afterbuild()), so there is nothing to resolve the name against. The failure comes from the planning path, where the resolution happens, and is contained like any per-case planning failure: logged, counted, that case skipped. A lookup by name on the built template is already on the team's task list; when it lands these queries start warming, with no change here.Primitives,
UUID,VECTOR(128, FLOAT)and arrays of primitives all resolve —vectorTypeis part ofprimitiveType, so no lookup is needed for any of them.This is not a runtime limitation. A bound struct carries its own metadata, so the query plans and runs normally, only cold. Pinning the same parameter
IS NULLis warmed too, since that case binds a real value and needs no type.Behaviour changes
A stored query without a signature is planned exactly as before, with
PreparedParams.empty()and one plan.The behaviour change flagged in the first PR is resolved by this one: between them, a stored query that declared a signature failed warm-up and was skipped. It now warms.
Tests
PreparedCaseParamsTest, 7 cases, no database — the mapping from case to parameters is a pure function. What each state binds; thatIS NOT NULLleaves no value; that declarations are passed on for every parameter whatever its state; a case mixing all three kinds; and that a value-free state with no declaration behind it is rejected here, so the failure names the missing declaration rather than surfacing later as a missing value.StoredQueriesTestgains four cases, all against FDB. That one stored query with two cases warms two plans. That both are reachable from a client — proved by the cached-plan count not moving, since a miss would plan afresh and insert, which matters because the failure mode here is a silent miss and asserting on returned rows would not catch it. That a boolean pinned to each value gets a plan per value and each binding finds its own. And that a parameter type warm-up cannot resolve skips only that stored query, leaving the next one warmed — the same containment the temp-function failures already had.Documentation
STORED_QUERY.rstgains a What is not warmed section — the general shape of a warm-up failure, the filtered-index case, and the schema template type — and a note in Signature that a template type in a signature needs theTYPEkeyword, unlike a column definition which takes the bare name. That difference betweencolumnTypeandfunctionColumnTypewas not derivable from the docs, and it is the mistake I made myself while writing the tests.Stack
PREPARE FORtoCREATE STORED QUERY— declares each parameter's name, type and nullability, enumerates the plans to warm for them, and persists both alongside the query text.