Warm one plan per prepared case - #4572
Open
sergei-pustovykh wants to merge 4 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
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
sergei-pustovykh
force-pushed
the
apple/sergei-pustovykh/stored-query/warm-up
branch
from
September 8, 2026 16:21
e68191a to
41bbcc9
Compare
sergei-pustovykh
force-pushed
the
apple/sergei-pustovykh/stored-query/warm-up
branch
2 times, most recently
from
September 11, 2026 11:44
f513766 to
ffa198d
Compare
sergei-pustovykh
force-pushed
the
apple/sergei-pustovykh/stored-query/warm-up
branch
from
September 11, 2026 13:36
ffa198d to
e40e619
Compare
sergei-pustovykh
force-pushed
the
apple/sergei-pustovykh/stored-query/warm-up
branch
from
September 11, 2026 14:15
e40e619 to
47d3f8c
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.
The cases this PR warms are written in the same shape as everywhere else.
Follows the rename in the layers below; comments and one message fragment a test matches on.
sergei-pustovykh
force-pushed
the
apple/sergei-pustovykh/stored-query/warm-up
branch
from
September 11, 2026 14:59
47d3f8c to
3a16f71
Compare
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.he cached-plan count not moving, which is the only way to catch a silent miss;
PreparedCaseParamsTestcovers the mapping without a database.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_NULLWhere to look
OfflineStoredQueriesProcessor.planStoredQuery— the per-case loop, and all five counter increments in one place.planOneCasein the same class — why the temporary functions are compiled inside the loop rather than once.PreparedCaseParams.of— the whole of what a case decides: which parameters get a value and which are left without one.StoredQueriesTestproves a hit by tWhat 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 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.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, and asserts the counters alongside the cached-plan count: two plans warmed and none failed for one query with two cases, and — where a declaration cannot be resolved — one warmed, one failed, one query processed and one failed. That last set is what catches a counter incremented per query where it should be per case, since a wrong increment would make the four numbers agree. 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.