Stored query signatures - value-free typed named parameters - #4438
Stored query signatures - value-free typed named parameters#4438sergei-pustovykh wants to merge 25 commits into
Conversation
…n ExpressionTests
| return namedParams.get(name); | ||
| } | ||
|
|
||
| public boolean hasNamedParamValue(@Nonnull String name) { |
There was a problem hiding this comment.
could you invoke this function to perform the assertion at line 93?
| * (value-bound) execution. | ||
| */ | ||
| @Nonnull | ||
| private final Map<String, Type> declaredTypes; |
There was a problem hiding this comment.
There seems to be a design flaw here, it binds a declared type to named parameters only leaving no room to extend support to unnamed parameters. It also uses Cascades Type however I think it should instead use our own DataType (and continue to use Type under the hood for SerDe).
You could instead introduce a PreparedParam record, that has an identifier (position or name), value, and a nullable DataType. This change PreparedParameters such that it could use a collection of PreparedParam instead.
| * the parameter is value-bound or unknown. | ||
| */ | ||
| @Nonnull | ||
| public Optional<Type> declaredTypeMaybe(@Nonnull String name) { |
There was a problem hiding this comment.
It is a bit strange, because if the parameter is value-bound, it has a, rather implicitly, declared type as well.
| // Using unmodifiableMap because it allows null values, which are valid here | ||
| // and represent either null constants or null prepared parameters in queries. | ||
| // Value-free literals are excluded: a present key with a null value means "bound to NULL", whereas an absent | ||
| // key means "no value at all", which is what leaves the constant id unbound in the evaluation context. |
There was a problem hiding this comment.
The added comments are overly-intensive about the differentiation between value-free literals and value-bound literals bound to null, can you please adjust the comments in this file and make them more about the code flow itself?
| } | ||
| } | ||
| literalReverseLookup.putIfAbsent(orderedLiteral.getLiteralObject(), orderedLiteral); | ||
| if (!orderedLiteral.isValueFree()) { |
There was a problem hiding this comment.
Can you add a positive check instead? if (orderedLiteral.isValueBound()
| if (param instanceof Array || param instanceof Struct) { | ||
| allowLiteralAddition = true; | ||
| } | ||
| if (!preparedStatementParameters.hasNamedParamValue(parameterName) |
There was a problem hiding this comment.
Can you add unit tests for this in AstNormalizerTests?
| ; | ||
|
|
||
| storedQueryParameter | ||
| : parameterName=uid (parameterType=functionColumnType | NULL_LITERAL) |
There was a problem hiding this comment.
The NULL literal is a bit awkward honestly.
| @Nonnull final Set<String> declaredNames, | ||
| @Nonnull final List<RelationalParser.UidContext> references) { | ||
| if (tree instanceof RelationalParser.FullColumnNameExpressionAtomContext atom) { | ||
| final var uids = atom.fullColumnName().fullId().uid(); |
There was a problem hiding this comment.
You should probably use IdentifierVisitor#visitUid
| final var storedQueryTexts = tempFunctionTexts.build(); | ||
| // The rewritten text is what gets persisted and re-parsed at warm-up, so prove now that it parses. | ||
| // This keeps a rewrite defect from becoming a startup-time failure that the author never sees. | ||
| validateRewrittenText(name, queryString, storedQueryTexts); |
There was a problem hiding this comment.
validateRewrittenText should probably be a test instead of having it running in production logic.
| // stored query its warm-up. | ||
| final PreparedParams warmupParams; | ||
| try { | ||
| warmupParams = warmupParamsFor(storedQuery); |
There was a problem hiding this comment.
why do we need to extract the parameters here? in other words, what's the use of the warmupParams when normalizing / generating the plan? It looks like the stored query itself is self-describing (the value-free parameters appear as "literals" in it) so it looks like we don't need to do this extra round of extraction, right?
Adds a typed named-parameter signature to
CREATE STORED QUERY. Parameters are declared once on the query and referenced as bare identifiers wherever the body or itsDECLAREd functions expect a value. At warm-up they are planned value-free from their declared types, and at runtime a client reuses the warmed plan by re-issuing the same SQL with those parameters bound by name.Previously, warming a stored query meant writing concrete literals in its body (e.g.
WHERE col1 = 10). But for a genuinely parameterized query the specific values are arbitrary — you'd have to invent placeholder literals purely to warm a plan, which don't reflect any real intent. A signature lets you declare the parameters by type (or NULL) instead, warming a reusable plan without naming any values.What it does
Signature.
CREATE STORED QUERY name(param type, ...)— a parameter may be a primitive type, or the keywordNULLto declare it exactly null (a null-specialized plan, see below).BOOLEANis excluded, and so areARRAYand composite types.Bare-identifier references. Signature parameters are used without a
?prefix, in the outer query and inside declared functions (captured as free variables). AtCREATEtime each reference in a value position is rewritten to a named parameter?paramin the persisted texts, so warm-up parses exactly what a runtime client re-issues. Because only value positions are rewritten, a parameter may share a column's name —t.paramstays a column reference andAS paramstays an alias.Value-free warm-up. Each parameter is planned as a typed
ConstantObjectValuewith no value, guarded by anOfType+IS_NOT_NULLplan constraint. Runtime reuse: the client re-declares the temp function(s) and runs the SELECT binding?paramby name; the bound value satisfies the constraint and hits the warmed plan (matched by canonical SQL + the temp-routine fingerprint, exactly as literal stored queries already reuse). The runtime statement must match the persisted form token for token, apart from keyword case and whitespace.NULL-specialized plans. A parameter declared
NULLis exactly null, so warm-up binds it to null — the same thingsetNulldoes at runtime — which yields anIS_NULLconstraint and lets the planner optimize the null case (e.g. foldingparam IS NULL). The value case and the null case are two separate stored queries that coexist as distinct plans under different cache keys; a value binding vs.setNullselects the matching one.How it works
Value-free
ConstantObjectValuecore — a COV that carries a declared type but no value.Bindings.MissingBindingExceptionplus a guard inQueryPlanConstraint.compileTimeEvalmake value-free plan-cache lookup a safe non-match.Value-free literals — a value-free constant is an
OrderedLiteralthat reserves a constant id and declares a type but has no value, and is filtered out ofLiterals.asMap(); that filter is what leaves the constant id unbound. Because they live in the literal table, they cross from a declared function body into the enclosing query's plan constraint by the same path as ordinary literals.Parse-tree-driven rewrite — a reference is recognised structurally (a
fullColumnNameused as anexpressionAtom, with a single unqualifieduid), not by matching token text, so qualified references, aliases, table names and function names are excluded by construction. The rewritten text is re-parsed before being persisted, so unparseable output cannot reach metadata.Signature metadata — persisted on
StoredQuery/PStoredQueryasrepeated PStoredQueryParameterentries of (name, type code); warm-up threads the declared types into planning viaPreparedParams.Scope / limitations
NULL. Non-primitive types are out of scope. Note the type is carried as aType.TypeCodename rather than a serializedPType, becausefdb-relational-apidoes not depend onfdb-record-layer-coreand both representations of a stored query must hold the same data.BOOLEANis rejected. A boolean's value can change the plan substantially — predicates fold away, branches disappear, index choices differ — so it belongs in the body as a literal, where the planner specializes on it and the resultingIS_TRUE/IS_FALSEplan constraint keepstrueandfalseas distinct cached plans. Declaring it as a value-free parameter would discard exactly that: the parameter could only carry "is not null", and one unspecialized plan would serve both values.AS SELECT ...); macro-function bodies do not participate.