Skip to content

Add transaction-scoped local variables (SET LOCAL / @var)#4198

Draft
arnaud-lacurie wants to merge 13 commits into
FoundationDB:mainfrom
arnaud-lacurie:variables
Draft

Add transaction-scoped local variables (SET LOCAL / @var)#4198
arnaud-lacurie wants to merge 13 commits into
FoundationDB:mainfrom
arnaud-lacurie:variables

Conversation

@arnaud-lacurie
Copy link
Copy Markdown
Collaborator

Summary

  • Adds SET LOCAL variable = value to bind a named variable scoped to the current FDB transaction
  • Adds @variable as an expression atom, resolved at normalization time as a named prepared-statement parameter
  • Variables are stored in the FDBRecordContext session layer (same mechanism as BoundSchemaTemplate) and are not visible after commit/rollback
  • The plan cache is unaffected: @x normalizes to ?x, so a single cached plan is reused across all values of a variable

Adds `SET LOCAL variable = value` and `@variable` reference syntax, scoped to the current FDB transaction. Variables are stored in the FDBRecordContext session layer and injected as named prepared-statement parameters at normalization time, so the plan cache sees a single plan structure per query and reuses it across different variable values.
- Replace incorrect reference-equality check in mergeNamedParams with a plain key-presence check
- Remove inaccurate Javadoc claim about case normalization in Transaction.setLocalVariable
- Add missing HashMap import
@arnaud-lacurie arnaud-lacurie added the enhancement New feature or request label May 23, 2026
@arnaud-lacurie arnaud-lacurie marked this pull request as draft May 23, 2026 11:37
…ordContextTransaction

AbstractMetadataOperationsFactory was delegating getSetLocalVariableConstantAction to
NoOpMetadataOperationsFactory, which silently dropped SET LOCAL statements in
TransactionBoundDatabase connections. Since SetLocalVariableConstantAction only touches
the transaction's session layer (no catalog dependencies), it should be available in all
factory contexts.

Also adds a test exercising SET LOCAL through the TransactionBoundDatabase/
RecordStoreAndRecordContextTransaction path.
`@body_var` referenced directly in a temp TVF body requires the
variable to exist at CREATE time, but uses a live reference to the
transaction's local-variable map, so it always reflects the current
value at the time the function is called.
…arams injection

When a temporary table-valued function body contains a local variable ref (@x),
the memoized compilation lambda was using the CREATE-time PreparedParams (empty),
causing the COV placeholder to have an incompatible type that crashed plan
compilation with COMPARAND_TO_COMPARISON_IS_OF_COMPLEX_TYPE.

Fix: inject SELECT-time PreparedParams into the CREATE-time MutablePlanGenerationContext
via a ThreadLocal set by BaseVisitor.resolveTableValuedFunction before the lambda runs.
The lambda temporarily swaps in the real params for the duration of compilation then
restores the originals. With the actual variable value and type available, the COV
gets the right type (e.g. STRING) and the binary comparison operator (EQ_SS) is chosen
correctly.

Also adds variableValueCapturedInContinuation test verifying that the variable value
bound at query execution time is captured in the continuation and cannot be overwritten
by a subsequent SET LOCAL before EXECUTE CONTINUATION.
…pared params

When a temp TVF body containing `?param` was compiled lazily at SELECT time,
the ThreadLocal carried the full SELECT-time PreparedParams (which included
local variable bindings merged with `?param` bindings). This replaced the
CREATE-time `?param` value with the SELECT-time one, causing wrong results.

Fix: thread only local variable bindings (@var) through the ThreadLocal
and through PlanContext/MutablePlanGenerationContext as a separate field.
In the memoized lambda, MERGE the local vars into the CREATE-time params
(PreparedParams.withAdditionalNamed) rather than replacing them, so that
the function's own ?param bindings are preserved.

EmbeddedRelationalStatement and EmbeddedRelationalPreparedStatement both
pass the local-vars-only map via PlanContext.withLocalVariables, which
PlanGenerator copies into MutablePlanGenerationContext.setLocalVariables.
BaseVisitor.resolveTableValuedFunction then sets TVFUNCTION_COMPILATION_PARAMS
to that local-vars map (not the full PreparedParams) before triggering the
lazy compilation.
1. Restore 4-arg AstNormalizer constructor (backward compat)
   Our branch added a 5th `deferMissingVarsInFunctionBodies` parameter,
   removing the 4-arg form that AstNormalizerTests.visitFullDescribeStatementThrows
   looked up via reflection. Restore it as a delegating overload.

2. Fix function-body cache-key normalization to use CREATE-time ?param bindings
   normalizeAst's temp-function loop calls normalizeFunctionBody to compute the
   cache-key contribution of each registered TVF. We mistakenly passed
   preparedStatementParameters (SELECT-time, no ?param) instead of the routine's
   own CREATE-time PreparedParams. This broke any test that created a TVF with
   ?param and then called it from a plain SELECT statement.

   Fix: add a private normalizeAst overload that carries localVariables (only
   @var bindings) from normalizeQuery. normalizeFunctionBody now receives
   PreparedParams.withAdditionalNamed(routine.getPreparedParams(), localVariables):
   - CREATE-time ?param bindings are preserved (function body semantics)
   - SELECT-time @var bindings are layered on top (local variable resolution)
   Since ?param names and @var names are enforced-disjoint at prepare time,
   no collision is possible. The public 8-arg normalizeAst passes Map.of()
   so all test call sites remain unchanged.
Remove unreachable BaseVisitor.visitVariableRef (ExpressionVisitor calls
visitVariableRef directly, never via ANTLR dispatch). Restore
DelegatingVisitor delegation stubs required by TypedVisitor interface.
Add direct test for RecordStoreAndRecordContextTransaction.setLocalVariable
and getLocalVariables delegation methods.
visitSetLocalVariable, visitVariableRef, and visitVariableRefAtom in
DelegatingVisitor are interface-compliance stubs that are never called
at runtime — DdlVisitor and ExpressionVisitor always override them.
Annotate with @ExcludeFromJacocoGeneratedReport to suppress Teamscale
test gap warnings for unreachable delegation boilerplate.
… gaps

Add visitSetLocalVariableTest, visitVariableRefTest, and
visitVariableRefAtomTest to DelegatingVisitorTest using the existing
Mockito delegation pattern. Remove @ExcludeFromJacocoGeneratedReport
annotations added in the previous approach.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant