If you have used psql, you have probably typed something like this:
\set ON_ERROR_STOP on
\set AUTOCOMMIT off
\set ECHO queries
These are PostreSQL variables. They change how the terminal behaves. But do you know what happens inside the source code when you type them? How does psql know that ON_ERROR_STOP should stop a script? Why does \set ON_ERROR_STOP xyz give you an error? Why does \unset AUTOCOMMIT turn autocommit off instead of removing it?
The answer to all these questions lies inside one small function src/bin/psql/variables.c
SetVariableHooks()
In this blog, we will see why it exists, read the code, and finish by adding our own psql variable with it.
Keep in mind: This is different from the server-side GUC system (shared_buffers, workmem, etc). Those live inside the PostgreSQL server. SetVariableHooks lives in psql, the client program.
Problem: Everything in psql is a String
Psql keeps its variables in a sample list. Each entry has a name and a value, and the value is always plain text.
AUTOCOMMIT-> “on”
ON_ERROR_STOP-> “off”
FETCH_COUNT-> “100”
ECHO-> “queries”
But the C code inside psql does not want strings. When psql is about to run a query and needs to know whether autocommit is on, it wants a real bool, not the text “on”. When it decides how many rows to fetch, it wants an int, not the text “100”.
So we have a gap,What the user types,
\set AUTOCOMMIT off
\set FETCH_COUNT 100
\set ECHO queries
What the C code wants,
pset.autocommit = false
pset.fetch_count = 100
pset.echo = PSQL_ECHO_QUERIES
pset.verbosity = PQERRORS_TERSE
Something has to sit in the middle and do the translation every time a variable changes. That's exactly the job of the hooks.
Why Do We Have Set VariableHooks?
Without hooks, psql would have several problems.
1. Constant Parsing: Every place in the code that needs AUTOCOMMIT would have to read the string and parse it again. This is repetitive and easy to get wrong. With a hook, the string is parsed once, when it is set, and stored in a typed field like pset.autocommit.
2. No Validation: Anyone could type /set AUTOCOMMIT xyz, and nothing would stop it. With the hooks, the value is checked before it is accepted. If it is invalid, the change is rejected, and the old value stays.
3. Shortcuts and defaults.
PSQL supports friendly behaviours:
\set ON_ERROR_STOP (with no value) means “turn it on”.
\unset ON_ERROR_STOP means “turn it off”
\unset ECHO means “back to none”.
Something needs to translate those into real values. That is the substitute hook.
The Two Kinds of Hooks
Both are function pointer types declared in variables.h;
typedef char *(*VariableSubstituteHook) (char *newval);
typedef bool (*VariableAssignHook) (const char *newval);
1. Substitute Hook: “Fix the value first”
Receives the (already substituted) value.
Validates it and updates the C state, such as pset.autocommit.
Returns true to accept, or false to reject.
An easy way to remember it:
- Substitute Hook - “What should this value become?”
- Assign Hook - “Is this value okay, and what should psql do with it?”
Reading SetVariableHooks in source code.
Here is the function signature:
void SetVariableHooks(VariableSpace space, const char *name,
VariableSubstituteHook shook,
VariableAssignHook ahook)
| Parameter | Meaning |
| space | The list that holds all psql variables(pset.vars) |
| name | The variable name, Example “AUTOCOMMIT” |
| shook | The substitute hook(or null) |
| ahook | The assign hook(or null) |
Note: If you need only one hook, pass NULL for the other. That is why some registrations in psql look like SetVariableHooks(pset.vars, “PROMPT1”, NULL, promt1_hook).
Step 1: safety checks:
if(! space || ! name)
return;
if(! valid_variable_name(name))
return;
If there is no variable space or no name, or the name has an illegal character, the function returns without doing anything.
Step 2: Search the list.
for(previous = space, current = space->next ; current ; previous = current, current = current->next){
Int cmp = strcmp(current->name, name);
. . .
}The variables live in LinkedList sorted alphabetically. The loop walks the list with two pointers, current (the node being examined) and previous (the node just before it). We need the previous later, if we have to insert a new node.
Strcmp gives us three possible outcomes:
cmp == 0
Meaning: Found the variable
Action Needed: Update its hook
cmp > 0
Meaning: We passed the place where it should be
Action Needed: Stop it does not exist
cmp < 0
Meaning: Not there yet
Action Needed: Keep walking
Because the list is sorted, cmp > 0 lets us stop early instead of scanning to the end.
space ? [AUTOCOMMIT] ? [ECHO] ? [FETCH_COUNT] ? [QUIET] ? NULL
Step 3: The Variable already exists
This is the default value when PostgreSQL starts.
if (cmp == 0)
{
/* found entry, so update */
current->substitute_hook = shook;
current->assign_hook = ahook;
if (shook)
current->value = (*shook) (current->value);
if (ahook)
(void) (*ahook) (current->value);
return;
}
Three things happen:
- The hooks are stored on the variables, so they run on every future change.
- The substitute hook is called immediately on the current value, and its result replaces the value.
- The assign hook is called immediately too, so psql’s internal state matches the stored value.
Step 4: The variable does not exist yet
/* not present, make new entry */
current = pg_malloc_object(struct _variable);
current->name = pg_strdup(name);
current->value = NULL;
current->substitute_hook = shook;
current->assign_hook = ahook;
current->next = previous->next;
previous->next = current;
if (shook)
current->value = (*shook) (current->value);
if (ahook)
(void) (*ahook) (current->value);
A new node is created with value NULL, just so there is a place to keep the hooks. This is the “attach hooks to a variable that doesn’t exist yet” case, and it is what EstablishVariableSpave() relies on at startup.
The node is then linked in at the correct sorted position using the previous:
Before: [AUTOCOMMIT] -> [FETCH_COUNT]
Insert: ECHO
After: [AUTOCOMMIT] -> [ECHO] -> [FETCH_COUNT]
Finally, the hooks are run once, exactly as in step 3.
The comment explains why creating a variable with a NULL value is fine: “this state is externally the same as the variable not being defined””. From the user’s point of view, an unset variable and a variable with NULL look the same.
The Order Matters: Substitute First, Then Assign
New Value ?Substitute Hook ? Assign Hook ? Accepted? (Store the value) / Rejected? (Keep the old value).
Real Examples from startup.c file,
Look at a real hook in src/bin/psql/startup.c.
static char *
bool_substitute_hook(char *newval)
{
if (newval == NULL)
{
/* "\unset FOO" becomes "\set FOO off" */
newval = pg_strdup("off");
}
else if (newval[0] == '\0')
{
/* "\set FOO" becomes "\set FOO on" */
pg_free(newval);
newval = pg_strdup("on");
This is the hook that makes boolean variables behave nicely:
| User Types | newval | Becomes |
| \unset FOO | NULL | “off” |
| \set | " " | “on” |
| \set FOO true | "true" | “true”(unchanged) |
It is reused by many variables: AUTOCOMMIT, ON_ERROR_STOP, QUIET, SINGLELINE, and more.
Example 2: autocommit_hook
static bool
autocommit_hook(const char *newval)
{
return ParseVariableBool(newval, "AUTOCOMMIT", &pset.autocommit);
}
ParseVariableBool converts text such as on, off, true, false, 1, 0, yes, no into a real bool and stores it in pset.autcommit. If the exist is not a valid boolean, it prints an error and returns false, so the assign is rejected.
Example 3: only one hook (PROMPT1)
SetVariableHooks(pset.vars, "PROMPT1", NULL, prompt1_hook);
Here there is no substitute hook, because there is no default to fill in. The assign_hook alone stores the prompt text.
Where Are the Hooks Registered?
All registrations happen in one place EstablishVariableSpace(),called from main() :
static void
EstablishVariableSpace(void)
{
pset.vars = CreateVariableSpace();
SetVariableHooks(pset.vars, "AUTOCOMMIT",
bool_substitute_hook,
autocommit_hook);
SetVariableHooks(pset.vars, "ON_ERROR_STOP",
bool_substitute_hook,
on_error_stop_hook);
SetVariableHooks(pset.vars, "FETCH_COUNT",
fetch_count_substitute_hook,
fetch_count_hook);
/* ... many more ... */
}
First, an empty variable space is created, then every special variable is registered with its hooks. Here are some of them:
| Variable | Substitute hook default | Assign Hook Updates |
| AUTOCOMMIT | off | pset.autocomit |
| ON_ERROR_STOP | off | pset.on_error_stop |
| FETCH_COUNT | 0 | pset.fetch_count |
| HISTSIZE | 500 | pset.histsize |
| ECHO | none | pset.echo |
| VERBOSITY | default | pset.verbosity |
| SHOW_CONTECT | errors | pset.show_context |
| WATCH_INTERVAL | DEFAULT_WATCH_INTERVAL | pset.watch_interval |
Following One Variable: The Life of AUTOCOMMIT
Let's trace AUTOCOMMIT from startup to a user command.
Step 1: establishVariableSpace() registers the hooks. The variable does not exist, so a new node is created with value NULL. The substitute hook turns NULL into “off”. The assign hook then sets pset.autocommit = false.
Step 2: main() sets the real default.
/* Default values for variables (that don't match the result of \unset) */
SetVariableBool(pset.vars, "AUTOCOMMIT");
The default for a fresh PSQL session is autocommit on, which differs from the \unset result of off. So this line sets the value to "on", and the hooks run again: pset.autocommit = true.
Step 3: the user types \set AUTOCOMMIT off. The hooks run again, and pset.autocommit becomes false.
Step 4: the user types \set AUTOCOMMIT banana. The substitute hook leaves the value unchanged. The assign hook prints an error and returns false. The stored value stays off.
This is the whole point of hooks. The string stored in the list and the C variable used by PSQL can never get out of sync, and invalid values never get in.
A detail worth noticing: SetVariableHooks ignores the return value of the assign hook ((void) (*ahook)(...)). This is safe because it runs at startup, before any user-supplied value exists. The comment explains that we don't expect failures there.
SetVariableHooks looks like a small helper, but it solves a real design problem. psql variables are just text, while psql's behaviour depends on typed C values. By attaching a substitute hook (which normalizes the value) and an assign hook (which validates it and updates the real state), psql keeps the two in sync at all times.