ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
table_info.c
Go to the documentation of this file.
1/**
2 * @file table_info.c
3 * @brief Heap-backed storage for per-relation provenance metadata.
4 *
5 * The @c provsql.table_info table holds one row per relation ProvSQL
6 * tracks: its TID / BID / OPAQUE classification, the block-key columns
7 * of a BID relation, and the base relations its atoms come from. This
8 * file implements the SQL entry points that read and write it
9 * (@c set_table_info, @c remove_table_info, @c get_table_info,
10 * @c set_ancestors, @c remove_ancestors, @c get_ancestors) and the
11 * uncached C fetchers behind the planner-hot-path caches in
12 * @c provsql_utils.c.
13 *
14 * Metadata about relations is catalog-shaped data, so the heap is its
15 * natural home: every change follows the transaction that made it, a
16 * concurrent session sees it only once it commits, and @c pg_dump
17 * carries it (the table is registered with
18 * @c pg_extension_config_dump). The circuit store proper holds only
19 * the circuit.
20 *
21 * Reads go through @c systable_beginscan rather than SPI: the planner
22 * hook consults them for every provenance-tracked range-table entry,
23 * and running a full query through the planner from inside the planner
24 * hook is both slow and needlessly re-entrant. Writes, which happen
25 * once per @c add_provenance / @c repair_key / guard-trigger fire, use
26 * SPI for brevity.
27 *
28 * The @c provsql_table_info_invalidate row trigger on the table
29 * broadcasts a relcache invalidation for each changed relation, which
30 * is what drops the stale entry from every backend's cache -- including
31 * after a direct @c UPDATE on the table or a @c pg_restore that loads
32 * it with @c COPY.
33 */
34#include "postgres.h"
35
36#include "access/htup_details.h"
37#if PG_VERSION_NUM >= 120000
38#include "access/table.h" /* table_open / table_close */
39#else
40#include "access/heapam.h" /* heap_open / heap_close (PG <12) */
41#define table_open(r, l) heap_open((r), (l))
42#define table_close(r, l) heap_close((r), (l))
43#endif
44#include "access/genam.h"
45#include "access/skey.h"
46#include "catalog/namespace.h"
47#include "catalog/pg_type.h"
48#include "commands/trigger.h"
49#include "executor/spi.h"
50#include "fmgr.h"
51#include "funcapi.h"
52#include "utils/array.h"
53#include "utils/builtins.h"
54#include "utils/fmgroids.h"
55#include "utils/inval.h"
56#include "utils/lsyscache.h"
57#include "utils/rel.h"
58#include "utils/syscache.h"
59
60#include "provsql_utils.h"
61#include "MMappedTableInfo.h"
62
63/** @brief Column numbers of @c provsql.table_info. */
64#define PROVSQL_TABLE_INFO_ATT_RELID 1
65#define PROVSQL_TABLE_INFO_ATT_KIND 2
66#define PROVSQL_TABLE_INFO_ATT_BLOCK_KEY 3
67#define PROVSQL_TABLE_INFO_ATT_ANCESTORS 4
68
69/**
70 * @brief Resolve @c provsql.table_info and its primary-key index.
71 *
72 * Returns @c InvalidOid (and leaves @p *index_oid untouched) when the
73 * table does not exist -- which is the normal state while
74 * @c CREATE @c EXTENSION runs, and on an installation still on an
75 * extension version that predates it. Every caller then behaves as
76 * "no metadata recorded", the conservative direction.
77 */
78static Oid provsql_table_info_relation(Oid *index_oid)
79{
80 Oid nsp = get_namespace_oid("provsql", true);
81 Oid rel;
82
83 if(!OidIsValid(nsp))
84 return InvalidOid;
85 rel = get_relname_relid("table_info", nsp);
86 if(!OidIsValid(rel))
87 return InvalidOid;
88 if(index_oid)
89 *index_oid = get_relname_relid("table_info_pkey", nsp);
90 return rel;
91}
92
93/**
94 * @brief Read the row of @p relid into @p out.
95 *
96 * @return @c true when a row exists; @p out is then fully populated
97 * (both the kind and the ancestor halves).
98 */
99static bool provsql_read_table_info(Oid relid, ProvenanceTableInfo *out)
100{
101 Oid rel_oid, index_oid = InvalidOid;
102 Relation rel;
103 SysScanDesc scan;
104 ScanKeyData skey;
105 HeapTuple htup;
106 bool found = false;
107
108 if(relid == InvalidOid)
109 return false;
110
111 rel_oid = provsql_table_info_relation(&index_oid);
112 if(!OidIsValid(rel_oid))
113 return false;
114
115 memset(out, 0, sizeof(*out));
116 out->relid = relid;
118
119 rel = table_open(rel_oid, AccessShareLock);
120 ScanKeyInit(&skey, PROVSQL_TABLE_INFO_ATT_RELID,
121 BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(relid));
122 scan = systable_beginscan(rel, index_oid, OidIsValid(index_oid),
123 NULL, 1, &skey);
124
125 if(HeapTupleIsValid(htup = systable_getnext(scan))) {
126 TupleDesc tupdesc = RelationGetDescr(rel);
127 bool isnull;
128 Datum d;
129
130 found = true;
131
132 d = heap_getattr(htup, PROVSQL_TABLE_INFO_ATT_KIND, tupdesc, &isnull);
133 if(!isnull) {
134 char *kind = text_to_cstring(DatumGetTextPP(d));
135 if(strcmp(kind, "tid") == 0)
136 out->kind = PROVSQL_TABLE_TID;
137 else if(strcmp(kind, "bid") == 0)
138 out->kind = PROVSQL_TABLE_BID;
139 else
141 pfree(kind);
142 }
143
144 d = heap_getattr(htup, PROVSQL_TABLE_INFO_ATT_BLOCK_KEY, tupdesc, &isnull);
145 if(!isnull) {
146 ArrayType *arr = DatumGetArrayTypeP(d);
147 if(ARR_NDIM(arr) == 1 && !array_contains_nulls(arr)) {
148 int n = ARR_DIMS(arr)[0];
151 out->block_key_n = (uint16) n;
152 memcpy(out->block_key, ARR_DATA_PTR(arr), n * sizeof(AttrNumber));
153 }
154 }
155
156 d = heap_getattr(htup, PROVSQL_TABLE_INFO_ATT_ANCESTORS, tupdesc, &isnull);
157 if(!isnull) {
158 ArrayType *arr = DatumGetArrayTypeP(d);
159 if(ARR_NDIM(arr) == 1 && !array_contains_nulls(arr)) {
160 int n = ARR_DIMS(arr)[0];
163 out->ancestor_n = (uint16) n;
164 memcpy(out->ancestors, ARR_DATA_PTR(arr), n * sizeof(Oid));
165 }
166 }
167 }
168
169 systable_endscan(scan);
170 table_close(rel, AccessShareLock);
171
172 return found;
173}
174
176{
177 return provsql_read_table_info(relid, out);
178}
179
180bool provsql_fetch_ancestry(Oid relid, uint16 *ancestor_n_out,
181 Oid *ancestors_out)
182{
184
185 *ancestor_n_out = 0;
186 if(!provsql_read_table_info(relid, &info))
187 return false;
188 *ancestor_n_out = info.ancestor_n;
189 memcpy(ancestors_out, info.ancestors, info.ancestor_n * sizeof(Oid));
190 /* "Row present but no ancestors" collapses to the same return as
191 * "no row": both make the safe-query rewriter take the conservative
192 * path. */
193 return info.ancestor_n > 0;
194}
195
196/** @brief Run @p sql with @p nargs bound parameters through SPI. */
197static void provsql_table_info_exec(const char *sql, int nargs,
198 Oid *argtypes, Datum *values)
199{
200 int rc;
201
202 if(SPI_connect() != SPI_OK_CONNECT)
203 provsql_error("Cannot connect to SPI while updating provsql.table_info");
204 rc = SPI_execute_with_args(sql, nargs, argtypes, values, NULL, false, 0);
205 if(rc != SPI_OK_INSERT && rc != SPI_OK_UPDATE && rc != SPI_OK_DELETE)
206 provsql_error("Cannot update provsql.table_info (SPI code %d)", rc);
207 SPI_finish();
208}
209
210/** @brief Translate a SQL-side kind label into the persisted enum value. */
211static uint8_t parse_table_kind(const char *label)
212{
213 if(strcmp(label, "tid") == 0) return PROVSQL_TABLE_TID;
214 if(strcmp(label, "bid") == 0) return PROVSQL_TABLE_BID;
215 if(strcmp(label, "opaque") == 0) return PROVSQL_TABLE_OPAQUE;
216 provsql_error("set_table_info: unknown table kind '%s' (expected "
217 "'tid', 'bid', or 'opaque')", label);
218 return PROVSQL_TABLE_TID; /* unreachable */
219}
220
221/** @brief Inverse of @c parse_table_kind for use by @c get_table_info. */
222static const char *table_kind_label(uint8_t kind)
223{
224 switch(kind) {
225 case PROVSQL_TABLE_TID: return "tid";
226 case PROVSQL_TABLE_BID: return "bid";
227 case PROVSQL_TABLE_OPAQUE: return "opaque";
228 }
229 provsql_error("get_table_info: unknown table kind value %u", kind);
230 return NULL; /* unreachable */
231}
232
233PG_FUNCTION_INFO_V1(set_table_info);
234/**
235 * @brief Upsert the kind half of a relation's @c provsql.table_info row.
236 *
237 * @p relid is the @c pg_class OID of the relation; @p kind is one of
238 * the textual labels @c 'tid' / @c 'bid' / @c 'opaque' (see
239 * @c provsql_table_kind in @c MMappedTableInfo.h); @p block_key is an
240 * @c int2 array (possibly empty) listing the block-key column numbers
241 * when @p kind is @c 'bid'. The relation's existing @c ancestors are
242 * preserved.
243 */
244Datum set_table_info(PG_FUNCTION_ARGS)
245{
246 Oid relid;
247 char *kind_str;
248 ArrayType *block_key;
249 uint16 block_key_n = 0;
250 Oid argtypes[3] = { OIDOID, TEXTOID, INT2ARRAYOID };
251 Datum values[3];
252
253 if(PG_ARGISNULL(0) || PG_ARGISNULL(1))
254 provsql_error("Invalid NULL value passed to set_table_info");
255
256 relid = PG_GETARG_OID(0);
257 kind_str = text_to_cstring(PG_GETARG_TEXT_PP(1));
258 (void) parse_table_kind(kind_str); /* validate, raising on a typo */
259
260 block_key = PG_ARGISNULL(2) ? NULL : PG_GETARG_ARRAYTYPE_P(2);
261 if(block_key) {
262 if(ARR_NDIM(block_key) > 1)
263 provsql_error("Invalid multi-dimensional array passed to set_table_info");
264 else if(ARR_NDIM(block_key) == 1)
265 block_key_n = *ARR_DIMS(block_key);
266 }
267 if(block_key_n > PROVSQL_TABLE_INFO_MAX_BLOCK_KEY)
268 provsql_error("set_table_info: block key wider than %d columns "
269 "(%u given) is not supported",
271
272 values[0] = ObjectIdGetDatum(relid);
273 values[1] = CStringGetTextDatum(kind_str);
274 values[2] = block_key ? PointerGetDatum(block_key)
275 : PointerGetDatum(construct_empty_array(INT2OID));
276
278 "INSERT INTO provsql.table_info(relid, kind, block_key) "
279 "VALUES ($1::regclass, $2, $3) "
280 "ON CONFLICT (relid) DO UPDATE "
281 "SET kind = EXCLUDED.kind, block_key = EXCLUDED.block_key",
282 3, argtypes, values);
283
284 pfree(kind_str);
285 PG_RETURN_VOID();
286}
287
288PG_FUNCTION_INFO_V1(remove_table_info);
289/** @brief Delete a relation's @c provsql.table_info row. No-op when absent. */
290Datum remove_table_info(PG_FUNCTION_ARGS)
291{
292 Oid argtypes[1] = { OIDOID };
293 Datum values[1];
294
295 if(PG_ARGISNULL(0))
296 provsql_error("Invalid NULL value passed to remove_table_info");
297
298 values[0] = ObjectIdGetDatum(PG_GETARG_OID(0));
300 "DELETE FROM provsql.table_info WHERE relid = $1::regclass",
301 1, argtypes, values);
302
303 PG_RETURN_VOID();
304}
305
306PG_FUNCTION_INFO_V1(set_ancestors);
307/**
308 * @brief Replace the ancestor half of a relation's row, keeping its
309 * @c kind / @c block_key.
310 *
311 * Silently no-op when @p relid has no row yet: the safe-query rewriter
312 * only consults ancestry for tracked relations, so callers should run
313 * @c add_provenance / @c repair_key / @c set_table_info first.
314 */
315Datum set_ancestors(PG_FUNCTION_ARGS)
316{
317 Oid relid;
318 ArrayType *ancestors;
319 uint16 ancestor_n = 0;
320 Oid argtypes[2] = { OIDOID, OIDARRAYOID };
321 Datum values[2];
322
323 if(PG_ARGISNULL(0))
324 provsql_error("Invalid NULL value passed to set_ancestors");
325
326 relid = PG_GETARG_OID(0);
327 ancestors = PG_ARGISNULL(1) ? NULL : PG_GETARG_ARRAYTYPE_P(1);
328
329 if(ancestors) {
330 if(ARR_NDIM(ancestors) > 1)
331 provsql_error("Invalid multi-dimensional array passed to set_ancestors");
332 else if(ARR_NDIM(ancestors) == 1)
333 ancestor_n = *ARR_DIMS(ancestors);
334 }
335 if(ancestor_n > PROVSQL_TABLE_INFO_MAX_ANCESTORS)
336 provsql_error("set_ancestors: ancestor set wider than %d entries "
337 "(%u given) is not supported",
339
340 values[0] = ObjectIdGetDatum(relid);
341 values[1] = ancestors ? PointerGetDatum(ancestors)
342 : PointerGetDatum(construct_empty_array(OIDOID));
343
345 "UPDATE provsql.table_info SET ancestors = $2 WHERE relid = $1::regclass",
346 2, argtypes, values);
347
348 PG_RETURN_VOID();
349}
350
351PG_FUNCTION_INFO_V1(remove_ancestors);
352/** @brief Clear a relation's ancestor set, keeping @c kind / @c block_key. */
353Datum remove_ancestors(PG_FUNCTION_ARGS)
354{
355 Oid argtypes[1] = { OIDOID };
356 Datum values[1];
357
358 if(PG_ARGISNULL(0))
359 provsql_error("Invalid NULL value passed to remove_ancestors");
360
361 values[0] = ObjectIdGetDatum(PG_GETARG_OID(0));
363 "UPDATE provsql.table_info SET ancestors = ARRAY[]::oid[] "
364 "WHERE relid = $1::regclass",
365 1, argtypes, values);
366
367 PG_RETURN_VOID();
368}
369
370PG_FUNCTION_INFO_V1(get_table_info);
371/**
372 * @brief PostgreSQL-callable wrapper around the cached kind lookup.
373 *
374 * Returns @c NULL when no row exists for @p relid; otherwise a record
375 * @c (kind text, block_key int2[]). Goes through
376 * @c provsql_lookup_table_info so repeated calls in the same session
377 * do not re-scan the table.
378 */
379Datum get_table_info(PG_FUNCTION_ARGS)
380{
381 Oid relid;
383 TupleDesc tupdesc;
384 Datum values[2];
385 bool nulls[2] = {false, false};
386 Datum *elems;
387 ArrayType *arr;
388
389 if(PG_ARGISNULL(0))
390 PG_RETURN_NULL();
391
392 relid = PG_GETARG_OID(0);
393
394 if(!provsql_lookup_table_info(relid, &info))
395 PG_RETURN_NULL();
396
397 if(get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
398 provsql_error("get_table_info: expected composite return type");
399 tupdesc = BlessTupleDesc(tupdesc);
400
401 values[0] = CStringGetTextDatum(table_kind_label(info.kind));
402
403 elems = palloc(info.block_key_n * sizeof(Datum));
404 for(uint16 i = 0; i < info.block_key_n; ++i)
405 elems[i] = Int16GetDatum(info.block_key[i]);
406 arr = construct_array(elems, info.block_key_n, INT2OID, 2, true, 's');
407 pfree(elems);
408 values[1] = PointerGetDatum(arr);
409
410 PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
411}
412
413PG_FUNCTION_INFO_V1(get_ancestors);
414/**
415 * @brief PostgreSQL-callable wrapper around the cached ancestry lookup.
416 *
417 * Returns @c NULL when no row exists for @p relid, or its ancestor set
418 * is empty; otherwise an @c oid[] listing the base-relation OIDs.
419 */
420Datum get_ancestors(PG_FUNCTION_ARGS)
421{
422 Oid relid;
423 uint16 ancestor_n;
425 Datum *elems;
426 ArrayType *arr;
427
428 if(PG_ARGISNULL(0))
429 PG_RETURN_NULL();
430
431 relid = PG_GETARG_OID(0);
432
433 if(!provsql_lookup_ancestry(relid, &ancestor_n, ancestors))
434 PG_RETURN_NULL();
435
436 elems = palloc(ancestor_n * sizeof(Datum));
437 for(uint16 i = 0; i < ancestor_n; ++i)
438 elems[i] = ObjectIdGetDatum(ancestors[i]);
439 arr = construct_array(elems, ancestor_n, OIDOID,
440 sizeof(Oid), true, 'i');
441 pfree(elems);
442
443 PG_RETURN_ARRAYTYPE_P(arr);
444}
445
446PG_FUNCTION_INFO_V1(provsql_table_info_invalidate);
447/**
448 * @brief Row trigger on @c provsql.table_info: broadcast a relcache
449 * invalidation for the relation whose metadata changed.
450 *
451 * Each backend caches the metadata of the relations its queries touch
452 * (@c provsql_lookup_table_info / @c provsql_lookup_ancestry) and drops
453 * an entry when PostgreSQL invalidates that relation's relcache entry.
454 * Putting the broadcast in a trigger rather than in the setters covers
455 * every writer: the setters, a hand-written @c UPDATE on the table, and
456 * the @c COPY a @c pg_restore performs.
457 *
458 * The @c pg_class probe skips relations that are already gone, which is
459 * the normal case for the @c DELETE the @c sql_drop event trigger
460 * performs.
461 */
462Datum provsql_table_info_invalidate(PG_FUNCTION_ARGS)
463{
464 TriggerData *trigdata = (TriggerData *) fcinfo->context;
465 TupleDesc tupdesc;
466 HeapTuple tuples[2];
467 int n = 0;
468
469 if(!CALLED_AS_TRIGGER(fcinfo))
470 provsql_error("provsql_table_info_invalidate: not called as a trigger");
471
472 tupdesc = trigdata->tg_relation->rd_att;
473
474 if(TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
475 tuples[n++] = trigdata->tg_trigtuple;
476 else if(TRIGGER_FIRED_BY_DELETE(trigdata->tg_event))
477 tuples[n++] = trigdata->tg_trigtuple;
478 else {
479 /* UPDATE: the row may have been re-keyed, so invalidate both. */
480 tuples[n++] = trigdata->tg_trigtuple;
481 tuples[n++] = trigdata->tg_newtuple;
482 }
483
484 for(int i = 0; i < n; ++i) {
485 bool isnull;
486 Datum d;
487 Oid relid;
488
489 if(!HeapTupleIsValid(tuples[i]))
490 continue;
491 d = heap_getattr(tuples[i], PROVSQL_TABLE_INFO_ATT_RELID, tupdesc, &isnull);
492 if(isnull)
493 continue;
494 relid = DatumGetObjectId(d);
495 if(OidIsValid(relid) && SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
496 CacheInvalidateRelcacheByRelid(relid);
497 }
498
499 return PointerGetDatum(NULL);
500}
Per-relation provenance metadata: the in-memory record.
#define PROVSQL_TABLE_INFO_MAX_BLOCK_KEY
Cap on the number of block-key columns recorded per relation.
@ PROVSQL_TABLE_TID
@ PROVSQL_TABLE_BID
@ PROVSQL_TABLE_OPAQUE
#define PROVSQL_TABLE_INFO_MAX_ANCESTORS
Cap on the number of base ancestors recorded per relation.
Datum set_ancestors(PG_FUNCTION_ARGS)
Replace the ancestor half of a relation's row, keeping its kind / block_key.
Definition table_info.c:315
Datum set_table_info(PG_FUNCTION_ARGS)
Forward declaration of the C SQL entry points.
Definition table_info.c:244
#define provsql_error(fmt,...)
Report a fatal ProvSQL error and abort the current transaction.
bool provsql_lookup_ancestry(Oid relid, uint16 *ancestor_n_out, Oid *ancestors_out)
Look up the base-ancestor set of a tracked relation.
bool provsql_lookup_table_info(Oid relid, ProvenanceTableInfo *out)
Look up per-table provenance metadata with a backend-local cache.
Core types, constants, and utilities shared across ProvSQL.
Per-relation metadata for the safe-query optimisation.
Oid relid
pg_class OID of the relation (primary key)
AttrNumber block_key[PROVSQL_TABLE_INFO_MAX_BLOCK_KEY]
Block-key column numbers.
uint16_t block_key_n
Number of valid entries in block_key.
Oid ancestors[PROVSQL_TABLE_INFO_MAX_ANCESTORS]
Sorted, deduplicated base-relation OIDs.
uint8_t kind
One of provsql_table_kind.
uint16_t ancestor_n
Number of valid entries in ancestors (0 = no registry info).
Datum set_ancestors(PG_FUNCTION_ARGS)
Replace the ancestor half of a relation's row, keeping its kind / block_key.
Definition table_info.c:315
#define PROVSQL_TABLE_INFO_ATT_KIND
Definition table_info.c:65
static Oid provsql_table_info_relation(Oid *index_oid)
Resolve provsql.table_info and its primary-key index.
Definition table_info.c:78
#define PROVSQL_TABLE_INFO_ATT_RELID
Column numbers of provsql.table_info.
Definition table_info.c:64
static const char * table_kind_label(uint8_t kind)
Inverse of parse_table_kind for use by get_table_info.
Definition table_info.c:222
Datum set_table_info(PG_FUNCTION_ARGS)
Upsert the kind half of a relation's provsql.table_info row.
Definition table_info.c:244
#define table_close(r, l)
Definition table_info.c:42
Datum get_table_info(PG_FUNCTION_ARGS)
PostgreSQL-callable wrapper around the cached kind lookup.
Definition table_info.c:379
Datum remove_ancestors(PG_FUNCTION_ARGS)
Clear a relation's ancestor set, keeping kind / block_key.
Definition table_info.c:353
Datum provsql_table_info_invalidate(PG_FUNCTION_ARGS)
Row trigger on provsql.table_info: broadcast a relcache invalidation for the relation whose metadata ...
Definition table_info.c:462
Datum get_ancestors(PG_FUNCTION_ARGS)
PostgreSQL-callable wrapper around the cached ancestry lookup.
Definition table_info.c:420
static bool provsql_read_table_info(Oid relid, ProvenanceTableInfo *out)
Read the row of relid into out.
Definition table_info.c:99
static uint8_t parse_table_kind(const char *label)
Translate a SQL-side kind label into the persisted enum value.
Definition table_info.c:211
Datum remove_table_info(PG_FUNCTION_ARGS)
Delete a relation's provsql.table_info row.
Definition table_info.c:290
bool provsql_fetch_table_info(Oid relid, ProvenanceTableInfo *out)
Raw IPC fetch (no cache).
Definition table_info.c:175
bool provsql_fetch_ancestry(Oid relid, uint16 *ancestor_n_out, Oid *ancestors_out)
Raw IPC fetch for the ancestry half (no cache).
Definition table_info.c:180
static void provsql_table_info_exec(const char *sql, int nargs, Oid *argtypes, Datum *values)
Run sql with nargs bound parameters through SPI.
Definition table_info.c:197
#define PROVSQL_TABLE_INFO_ATT_ANCESTORS
Definition table_info.c:67
#define PROVSQL_TABLE_INFO_ATT_BLOCK_KEY
Definition table_info.c:66
#define table_open(r, l)
Definition table_info.c:41