Read non-future-helper marker without SPI - #330
Conversation
Durofut::ensure consults the df.non_future_helper marker GUC on every non-JSON operand, and mark_non_future_helper_call writes it. Both went through SPI, so each composer call paid a full plan+execute round trip for current_setting() and statement_timestamp() -- on the hot path for every plain-SQL operand in a chain. Read and write the marker through the C API instead: GetConfigOption, set_config_option with GUC_ACTION_LOCAL, and GetCurrentStatementStartTimestamp. No planner, no executor. The marker now carries the internal TimestampTz integer rather than a formatted timestamp string. The GUC is transaction-local and is only ever written and read within a single statement by the same binary, so the encoding is private and needs no compatibility handling. The unit test that synthesizes the marker in SQL is updated to match. Behaviour is unchanged: same detection semantics, same error messages. No schema change, no upgrade script, no SQL query text changes.
Measured impactA/B of the same Binary verification. The build timestamp reported at startup is not reliable here — BEGIN;
SELECT df.setvar('p','1');
SELECT replace(current_setting('df.non_future_helper', true), E'\n', ' | ');
COMMIT;
Fixed cost per composer call
~5.4× faster on the composer's fixed cost; ~29 µs saved per call, i.e. ~7 µs per eliminated SPI round trip. Note the first run after a server restart is cold (11.9 µs on this branch) and understates the gain; the numbers above are warm steady state. Independent cross-checkLeft-deep
At n=10 that is ~25 µs/step. A chain step does only 1 lookup — the accumulator starts with CaveatThis is a fixed per-call win, so it does not help long chains: per-step cost still climbs 58 → 230 → 476 → 1018 µs as n goes 10 → 200, and by n=200 the saving is inside the noise of the O(N²) accumulator copying. What this change does is remove the SPI cost that was previously masking that quadratic term. |
Implements Step 0 of
docs/plan-graph-rework.md.Problem
Durofut::ensurecallssame_statement_non_future_helper_namebefore doing anything else, and that helper issued SPI queries to read thedf.non_future_helpersession GUC andstatement_timestamp().mark_non_future_helper_callissued a third forset_config.Every plain-SQL operand flowing through a composer paid a full plan + execute round trip. For the small graphs users actually write, this very likely dominates the composition cost.
Change
Read and write the marker through the C API instead:
GetConfigOptionreplacescurrent_setting()set_config_optionwithGUC_ACTION_LOCALreplacesset_config(..., true)GetCurrentStatementStartTimestampreplacesstatement_timestamp()No planner, no executor.
The marker now carries the internal
TimestampTzinteger rather than a formatted timestamp string. The GUC is transaction-local and is only ever written and read within a single statement by the same binary, so the encoding is private and needs no compatibility handling. The one unit test that synthesizes the marker in SQL is updated to match.Behaviour
Unchanged. Same detection semantics, same error messages, same transaction-local scope. The existing helper-misuse tests passing unchanged is the whole acceptance criterion.
Why this is first
Two reasons from the plan:
It also shrinks what Step 5 has to preserve: this check is the only hard error the composers currently raise.
Upgrade & migration
None. No schema change, no upgrade script, no wire-format change, no runtime schema detection. No SQL query text changes, so B1 backward compatibility is unaffected — the new
.soissues no new or altered queries against any schema version.Testing
cargo check --features pg17— cleancargo clippy --features pg17 -- -D warnings— cleancargo fmt -p pg_durable -- --check— clean./scripts/test-unit.sh cannot_be_used_in_seq_composition— 4 passedThe four covering tests exercise
df.setvar,df.unsetvar,df.clearvarsanddf.await_instancemisuse detection against a real PG17 backend, including the manually synthesized marker path.