ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
probability_store.c
Go to the documentation of this file.
1/**
2 * @file probability_store.c
3 * @brief Write-once probabilities and their rollback.
4 *
5 * The circuit store is append-only: a gate, once created, never
6 * changes, and a gate created by a transaction that later rolls back
7 * is an orphan rather than an inconsistency -- no committed row can
8 * reference it, and the same expression recomputed lands on the same
9 * content-addressed UUID. A probability is the one thing about a gate
10 * that used to be rewritten in place, which is what made a rolled-back
11 * @c set_prob stick.
12 *
13 * This file makes a probability a fact appended to the circuit like
14 * the gate itself:
15 *
16 * - @c set_prob writes it when the gate has none, reports success
17 * without writing when the gate already holds exactly that value
18 * (so setup scripts and notebook cells stay re-runnable, the same
19 * idempotence @c create_gate and @c add_provenance offer), and
20 * raises otherwise;
21 * - every write is recorded in a backend-local list keyed by
22 * subtransaction nesting level, and an aborted (sub)transaction
23 * clears the probabilities it wrote. No old value is kept, because
24 * there never was one: this is an undo list of tokens, not of
25 * values;
26 * - changing a probability means minting a fresh input gate with the
27 * new probability and rewriting the rows that carry the old token,
28 * which @c provsql.replace_input does; the base table's token
29 * column is the place of truth, exactly as it is for the token
30 * rewrites the data-modification triggers perform.
31 *
32 * What is not closed here is isolation: another session can read a
33 * probability between the write and the rollback. Closing that would
34 * mean deferring the write to @c XACT_EVENT_PRE_COMMIT behind a
35 * backend-local overlay, at the price of making every reader consult
36 * the overlay.
37 */
38#include "postgres.h"
39
40#include <math.h>
41
42#include "access/xact.h"
43#include "fmgr.h"
44#include "utils/builtins.h"
45#include "utils/uuid.h"
46
47#include "probability_store.h"
48#include "provsql_mmap.h"
49#include "provsql_utils.h"
50
51/**
52 * @brief One recorded probability write, awaiting commit or rollback.
53 *
54 * @c nest_level is the subtransaction nesting level the write happened
55 * at (@c GetCurrentTransactionNestLevel). A subtransaction abort
56 * clears every entry at or below its own level; a subtransaction
57 * commit re-parents them to the parent level, so an outer @c ROLLBACK
58 * still undoes a write made inside a released savepoint.
59 */
64
65static prob_write *prob_writes = NULL;
66static int prob_writes_len = 0;
67static int prob_writes_cap = 0;
68static bool prob_callbacks_registered = false;
69
70/** Replacement leaves minted by this transaction; see the block below. */
71static pg_uuid_t *fresh_leaves = NULL;
72static int fresh_leaves_len = 0;
73static int fresh_leaves_cap = 0;
74
75/** @brief Drop the recorded writes at nesting level @p level or deeper,
76 * clearing each one's probability in the store when @p undo. */
77static void prob_writes_unwind(int level, bool undo)
78{
79 int keep = 0;
80
81 for(int i = 0; i < prob_writes_len; ++i) {
82 if(prob_writes[i].nest_level >= level) {
83 if(undo)
85 } else {
86 prob_writes[keep++] = prob_writes[i];
87 }
88 }
89 prob_writes_len = keep;
90}
91
92/** @brief Move the recorded writes at nesting level @p level up to its
93 * parent, so a later outer rollback still undoes them. */
94static void prob_writes_reparent(int level)
95{
96 for(int i = 0; i < prob_writes_len; ++i)
97 if(prob_writes[i].nest_level >= level)
98 prob_writes[i].nest_level = level - 1;
99}
100
101/** @brief Release the list without touching the store. */
102static void prob_writes_forget(void)
103{
104 prob_writes_len = 0;
105}
106
107static void prob_xact_callback(XactEvent event, void *arg)
108{
109 (void) arg;
110 switch(event) {
111 case XACT_EVENT_COMMIT:
112 case XACT_EVENT_PARALLEL_COMMIT:
113 case XACT_EVENT_PREPARE:
116 break;
117 case XACT_EVENT_ABORT:
118 case XACT_EVENT_PARALLEL_ABORT:
119 /* No SPI and no new transaction here, but a pipe write is fine: the
120 worker is a separate process and the message needs no snapshot.
121 An error raised here would escalate to FATAL -- the transaction is
122 already aborting -- so a store that has become unreachable is
123 reported and the list dropped, rather than taking the session down
124 with it. */
125 PG_TRY();
126 {
127 prob_writes_unwind(0, true);
128 }
129 PG_CATCH();
130 {
131 prob_writes_len = 0;
132 FlushErrorState();
133 provsql_warning("could not clear the probabilities written by the "
134 "transaction that just rolled back; they stand as "
135 "written");
136 }
137 PG_END_TRY();
139 break;
140 case XACT_EVENT_PRE_PREPARE:
141 if(prob_writes_len > 0)
142 provsql_error("cannot PREPARE a transaction that has written "
143 "provenance probabilities: the write would have to be "
144 "undone if the prepared transaction rolled back, and "
145 "ProvSQL keeps that undo list in the backend");
146 break;
147 default:
148 break;
149 }
150}
151
152static void prob_subxact_callback(SubXactEvent event,
153 SubTransactionId mySubid,
154 SubTransactionId parentSubid,
155 void *arg)
156{
157 (void) mySubid; (void) parentSubid; (void) arg;
158 switch(event) {
159 case SUBXACT_EVENT_ABORT_SUB:
160 /* Same reasoning as the transaction-level abort above. */
161 PG_TRY();
162 {
163 prob_writes_unwind(GetCurrentTransactionNestLevel(), true);
164 }
165 PG_CATCH();
166 {
167 prob_writes_unwind(GetCurrentTransactionNestLevel(), false);
168 FlushErrorState();
169 provsql_warning("could not clear the probabilities written by the "
170 "subtransaction that just rolled back; they stand "
171 "as written");
172 }
173 PG_END_TRY();
174 break;
175 case SUBXACT_EVENT_COMMIT_SUB:
176 prob_writes_reparent(GetCurrentTransactionNestLevel());
177 break;
178 default:
179 break;
180 }
181}
182
183/** @brief Record that this transaction wrote @p token's probability. */
184static void prob_writes_record(const pg_uuid_t *token)
185{
187 RegisterXactCallback(prob_xact_callback, NULL);
188 RegisterSubXactCallback(prob_subxact_callback, NULL);
190 }
191
193 int newcap = prob_writes_cap ? prob_writes_cap * 2 : 64;
194 prob_write *grown = realloc(prob_writes, newcap * sizeof(prob_write));
195 if(!grown)
196 provsql_error("ProvSQL: out of memory recording a probability write");
197 prob_writes = grown;
198 prob_writes_cap = newcap;
199 }
200 prob_writes[prob_writes_len].token = *token;
201 prob_writes[prob_writes_len].nest_level = GetCurrentTransactionNestLevel();
203}
204
205/* -------------------------------------------------------------------------
206 * Freshly minted replacement leaves
207 *
208 * @c provsql.replace_input and its siblings mint a new leaf gate for a row
209 * whose probability is to change; the row's provsql column is then rewritten
210 * to carry it. @c provenance_guard cannot tell such a token from an
211 * arbitrary UUID a user pasted in, and it flips a table to OPAQUE on the
212 * latter because TID independence can no longer be assumed. A replacement
213 * leaf *is* an independent fresh leaf, so the table stays what it was --
214 * provided the guard can recognise it, which is what this list is for.
215 *
216 * The list is per-transaction: a token minted by a transaction that rolls
217 * back never reaches a committed row.
218 * ------------------------------------------------------------------------- */
219
220/** @brief Remember that this transaction minted @p token as a replacement
221 * leaf for a tracked row. */
222static void fresh_leaves_record(const pg_uuid_t *token)
223{
225 int newcap = fresh_leaves_cap ? fresh_leaves_cap * 2 : 64;
226 pg_uuid_t *grown = realloc(fresh_leaves, newcap * sizeof(pg_uuid_t));
227 if(!grown)
228 provsql_error("ProvSQL: out of memory recording a replacement leaf");
229 fresh_leaves = grown;
230 fresh_leaves_cap = newcap;
231 }
232 fresh_leaves[fresh_leaves_len++] = *token;
233}
234
235PG_FUNCTION_INFO_V1(note_fresh_leaf);
236/**
237 * @brief Declare a just-minted leaf gate a replacement for a tracked row.
238 *
239 * Called by @c provsql.replace_input / @c replace_block right after they
240 * create the gate, so that the @c UPDATE which stores it does not look to
241 * @c provenance_guard like a user pasting in an arbitrary token.
242 */
243Datum note_fresh_leaf(PG_FUNCTION_ARGS)
244{
245 if(PG_ARGISNULL(0))
246 provsql_error("Invalid NULL value passed to note_fresh_leaf");
247 fresh_leaves_record(DatumGetUUIDP(PG_GETARG_DATUM(0)));
248 PG_RETURN_VOID();
249}
250
251PG_FUNCTION_INFO_V1(is_fresh_leaf);
252/** @brief Whether @p token was minted as a replacement leaf by this
253 * transaction (see @c note_fresh_leaf). */
254Datum is_fresh_leaf(PG_FUNCTION_ARGS)
255{
256 pg_uuid_t *token;
257
258 if(PG_ARGISNULL(0))
259 PG_RETURN_BOOL(false);
260
261 token = DatumGetUUIDP(PG_GETARG_DATUM(0));
262 for(int i = 0; i < fresh_leaves_len; ++i)
263 if(memcmp(&fresh_leaves[i], token, sizeof(pg_uuid_t)) == 0)
264 PG_RETURN_BOOL(true);
265 PG_RETURN_BOOL(false);
266}
267
268void provsql_set_prob_tracked(const pg_uuid_t *token, double prob)
269{
270 double existing = 0.;
272
273 if(isnan(prob))
274 provsql_error("set_prob: NaN is not a probability");
275 if(prob < 0. || prob > 1.)
276 provsql_error("set_prob: probability %g is outside [0, 1]", prob);
277
278 result = provsql_internal_set_prob(token, prob, &existing);
279
280 switch(result) {
282 prob_writes_record(token);
283 break;
285 break;
287 provsql_error("set_prob called on non-input gate");
288 break;
290 {
291 unsigned nb_children = 0;
292 pg_uuid_t *children = NULL;
293 gate_type type = provsql_fetch_gate(token, &nb_children, &children);
294 const char *hint;
295
296 if(children)
297 free(children);
298
299 /* Which replacement applies depends on what the gate is: sending a
300 block value or an update gate to replace_input() only earns the
301 user a second refusal, since replace_input() redirects them here
302 anyway. */
303 switch(type) {
304 case gate_mulinput:
305 hint = "Use provsql.replace_block() to give a repair_key block a "
306 "different set of probabilities: a block's values share one "
307 "key gate and their masses are meaningful together, so they "
308 "are replaced together.";
309 break;
310 case gate_update:
311 hint = "Use provsql.replace_update() to give a recorded data "
312 "modification a different probability.";
313 break;
314 default:
315 hint = "Use provsql.replace_input() to give a tuple a different "
316 "probability: it mints a fresh input gate and returns it, "
317 "for the row's provsql column to carry.";
318 break;
319 }
320
321 ereport(ERROR,
322 (errmsg("probability of gate %s is already set to %g",
323 DatumGetCString(DirectFunctionCall1(
324 uuid_out, UUIDPGetDatum(token))),
325 existing),
326 errdetail("Probabilities are written once, so that a "
327 "transaction that rolls back leaves the circuit as "
328 "it found it."),
329 errhint("%s", hint)));
330 break;
331 }
332 }
333}
334
335PG_FUNCTION_INFO_V1(set_prob);
336/**
337 * @brief Write a gate's probability, once.
338 *
339 * Transparent @c gate_annotation wrappers (an inversion-free
340 * certificate / order marker attached by the planner to a certified
341 * query's row roots) are peeled first: a probability set on a wrapped
342 * token belongs to the input gate underneath, so the documented
343 * @c "set_prob(provenance(), p) FROM t" pattern keeps working when the
344 * query happens to be certified.
345 */
346Datum set_prob(PG_FUNCTION_ARGS)
347{
348 pg_uuid_t *token;
349 double prob;
350 pg_uuid_t peeled;
351
352 if(PG_ARGISNULL(0) || PG_ARGISNULL(1))
353 provsql_error("Invalid NULL value passed to set_prob");
354
355 token = DatumGetUUIDP(PG_GETARG_DATUM(0));
356 prob = PG_GETARG_FLOAT8(1);
357
358 for(;;) {
359 unsigned nb_children = 0;
360 pg_uuid_t *children = NULL;
361 gate_type type = provsql_fetch_gate(token, &nb_children, &children);
362 if(type != gate_annotation || nb_children != 1) {
363 if(children) free(children);
364 break;
365 }
366 peeled = children[0];
367 token = &peeled;
368 free(children);
369 }
370
371 provsql_set_prob_tracked(token, prob);
372
373 PG_RETURN_VOID();
374}
375
376PG_FUNCTION_INFO_V1(probability_is_set);
377/**
378 * @brief Report whether a probability has been written on a gate.
379 *
380 * @c get_prob() answers the value an evaluation would use, so it cannot
381 * tell a gate nobody gave a probability from one written as 1. This
382 * can, which is what a UI needs in order to offer "set" on the first
383 * and "replace" on the second.
384 */
385Datum probability_is_set(PG_FUNCTION_ARGS)
386{
387 pg_uuid_t *token;
388
389 if(PG_ARGISNULL(0))
390 PG_RETURN_NULL();
391
392 token = DatumGetUUIDP(PG_GETARG_DATUM(0));
393 PG_RETURN_BOOL(provsql_internal_get_prob_written(token, NULL));
394}
static bool prob_callbacks_registered
static void fresh_leaves_record(const pg_uuid_t *token)
Remember that this transaction minted token as a replacement leaf for a tracked row.
static int fresh_leaves_len
static void prob_subxact_callback(SubXactEvent event, SubTransactionId mySubid, SubTransactionId parentSubid, void *arg)
static int prob_writes_len
static void prob_writes_reparent(int level)
Move the recorded writes at nesting level level up to its parent, so a later outer rollback still und...
static void prob_writes_unwind(int level, bool undo)
Drop the recorded writes at nesting level level or deeper, clearing each one's probability in the sto...
static void prob_xact_callback(XactEvent event, void *arg)
static int fresh_leaves_cap
void provsql_set_prob_tracked(const pg_uuid_t *token, double prob)
Write token's probability and record the write, so that a rollback of the current (sub)transaction cl...
Datum is_fresh_leaf(PG_FUNCTION_ARGS)
Whether token was minted as a replacement leaf by this transaction (see note_fresh_leaf).
static pg_uuid_t * fresh_leaves
Replacement leaves minted by this transaction; see the block below.
Datum probability_is_set(PG_FUNCTION_ARGS)
Report whether a probability has been written on a gate.
static int prob_writes_cap
static void prob_writes_record(const pg_uuid_t *token)
Record that this transaction wrote token's probability.
static void prob_writes_forget(void)
Release the list without touching the store.
static prob_write * prob_writes
Datum note_fresh_leaf(PG_FUNCTION_ARGS)
Declare a just-minted leaf gate a replacement for a tracked row.
Datum set_prob(PG_FUNCTION_ARGS)
Write a gate's probability, once.
Write-once probabilities: the entry points other files use.
#define provsql_error(fmt,...)
Report a fatal ProvSQL error and abort the current transaction.
#define provsql_warning(fmt,...)
Emit a ProvSQL warning message (execution continues).
gate_type provsql_fetch_gate(const pg_uuid_t *token, unsigned *nb_children_out, pg_uuid_t **children_out)
PostgreSQL-callable wrapper for get_gate_type().
provsql_set_prob_result provsql_internal_set_prob(const pg_uuid_t *token, double prob, double *existing)
Write a gate's probability from in-extension C/C++ code.
bool provsql_internal_get_prob_written(const pg_uuid_t *token, double *prob)
Report whether a probability has been written on a gate.
void provsql_internal_clear_prob(const pg_uuid_t *token)
Drop a gate's probability, leaving it as it was before anyone wrote one.
Background worker and IPC primitives for mmap-backed circuit storage.
provsql_set_prob_result
Outcome of a probability write, mirroring MMappedCircuit::SetProbResult across the IPC boundary.
@ PROVSQL_SET_PROB_NOT_PROB_GATE
The gate carries no probability.
@ PROVSQL_SET_PROB_WRITTEN
Written; undo on rollback.
@ PROVSQL_SET_PROB_ALREADY_SET
Holds a different value; refused.
@ PROVSQL_SET_PROB_UNCHANGED
Already held exactly this value.
Core types, constants, and utilities shared across ProvSQL.
@ gate_annotation
Transparent single-child wrapper carrying a query-level annotation in extra (inversion-free certifica...
One recorded probability write, awaiting commit or rollback.
pg_uuid_t token