ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
tool_registry_sql.cpp
Go to the documentation of this file.
1/**
2 * @file tool_registry_sql.cpp
3 * @brief SQL surface for the external-tool registry (@ref ToolRegistry.h).
4 *
5 * Exposes the in-memory catalog to SQL:
6 *
7 * - @c tool_registry_list(): set-returning, backs the read-only
8 * @c provsql.tools view; reports each record plus an @c available flag
9 * computed with the same @c find_external_tool the dispatchers use.
10 * - @c tool_registry_register() / @c tool_registry_unregister() /
11 * @c tool_registry_set_enabled() / @c tool_registry_set_preference():
12 * mutators, **superuser-only**.
13 *
14 * @par Security
15 * A CLI tool record names an executable that ProvSQL runs as the PostgreSQL
16 * OS user, so editing a record is equivalent to OS-level trust on the
17 * server account (the same trust as setting @c provsql.tool_search_path or
18 * dropping a binary on it). The mutators therefore refuse non-superusers;
19 * the read-only listing is unrestricted, like @c tool_available.
20 *
21 * @par Lifetime
22 * @par Persistence
23 * The compiled-in defaults live in C (@ref ToolRegistry.h); admin changes are
24 * persisted in the @c provsql.tool_overrides table and overlaid on the seed
25 * by @ref provsql_sync_tool_registry, which every registry-consuming SQL
26 * function calls so changes are seen across sessions and backends. An empty
27 * overrides table is exactly the compiled defaults; the table being absent
28 * (an extension older than 1.8.0) is treated the same way.
29 */
30extern "C" {
31#include "postgres.h"
32#include "fmgr.h"
33#include "funcapi.h"
34#include "miscadmin.h"
35#include "executor/spi.h"
36#include "catalog/pg_type.h"
37#include "utils/array.h"
38#include "utils/builtins.h"
39#include "utils/tuplestore.h"
40#if PG_VERSION_NUM >= 160000
41#include "varatt.h"
42#endif
43
44#include "compatibility.h" /* TYPALIGN_INT fallback for PG < 11 */
45
46PG_FUNCTION_INFO_V1(tool_registry_list);
47PG_FUNCTION_INFO_V1(tool_registry_register);
48PG_FUNCTION_INFO_V1(tool_registry_unregister);
49PG_FUNCTION_INFO_V1(tool_registry_set_enabled);
50PG_FUNCTION_INFO_V1(tool_registry_set_preference);
51}
52
53#include "ToolRegistry.h"
54#include "tool_registry_sync.h"
55#include "external_tool.h"
56#include "provsql_error.h"
57
58#include <functional>
59#include <string>
60#include <vector>
61
62namespace {
63
64/// Read a SQL text into a std::string.
65std::string text_to_string(text *t)
66{
67 return std::string(VARDATA_ANY(t), VARSIZE_ANY_EXHDR(t));
68}
69
70/// Build a text[] Datum from a vector of strings (never NULL elements).
71Datum string_vector_to_text_array(const std::vector<std::string> &v)
72{
73 if (v.empty())
74 return PointerGetDatum(construct_empty_array(TEXTOID));
75
76 std::vector<Datum> elems;
77 elems.reserve(v.size());
78 for (const auto &s : v)
79 elems.push_back(PointerGetDatum(cstring_to_text_with_len(s.data(),
80 s.size())));
81
82 ArrayType *arr = construct_array(elems.data(),
83 static_cast<int>(elems.size()),
84 TEXTOID, -1, false, TYPALIGN_INT);
85 return PointerGetDatum(arr);
86}
87
88/// Decode a (non-NULL) text[] argument into a vector of strings, dropping
89/// NULL elements.
90std::vector<std::string> text_array_to_string_vector(ArrayType *arr)
91{
92 std::vector<std::string> out;
93 Datum *elems;
94 bool *nulls;
95 int n;
96 deconstruct_array(arr, TEXTOID, -1, false, TYPALIGN_INT,
97 &elems, &nulls, &n);
98 for (int i = 0; i < n; ++i) {
99 if (nulls[i])
100 continue;
101 out.push_back(text_to_string(DatumGetTextPP(elems[i])));
102 }
103 return out;
104}
105
106/// Reject non-superusers from a registry mutator.
107void require_superuser(const char *fn)
108{
109 if (!superuser())
110 provsql_error("%s: must be superuser (a tool record can run arbitrary "
111 "commands as the PostgreSQL OS user)", fn);
112}
113
114// ---- provsql.tool_overrides persistence (caller manages SPI_connect) ----
115
116/// Read a text[] column of the current SPI tuple as a vector<string>.
117std::vector<std::string> spi_text_array(HeapTuple t, TupleDesc td, int col)
118{
119 bool isnull;
120 Datum d = SPI_getbinval(t, td, col, &isnull);
121 if (isnull)
122 return {};
123 return text_array_to_string_vector(DatumGetArrayTypeP(d));
124}
125
126/// Read a text column of the current SPI tuple ("" when NULL).
127std::string spi_text(HeapTuple t, TupleDesc td, int col)
128{
129 char *s = SPI_getvalue(t, td, col);
130 return s ? std::string(s) : std::string();
131}
132
133/// True iff provsql.tool_overrides exists (an extension < 1.8.0 lacks it).
134/// to_regclass returns NULL rather than erroring on a missing relation.
135bool overrides_table_exists()
136{
137 if (SPI_execute("SELECT to_regclass('provsql.tool_overrides') IS NOT NULL",
138 true, 1) != SPI_OK_SELECT || SPI_processed != 1)
139 return false;
140 bool isnull;
141 Datum d = SPI_getbinval(SPI_tuptable->vals[0], SPI_tuptable->tupdesc,
142 1, &isnull);
143 return !isnull && DatumGetBool(d);
144}
145
146/// Upsert a complete record (removed=false) into provsql.tool_overrides.
147void upsert_override(const provsql::ToolRecord &rec)
148{
149 Oid types[13] = {TEXTOID, TEXTOID, TEXTOID, TEXTARRAYOID, TEXTARRAYOID,
150 TEXTOID, TEXTOID, INT4OID, BOOLOID, TEXTARRAYOID,
151 TEXTOID, TEXTOID, TEXTOID};
152 Datum vals[13] = {
153 CStringGetTextDatum(rec.name.c_str()),
154 CStringGetTextDatum(rec.kind.c_str()),
155 CStringGetTextDatum(rec.binary.c_str()),
156 string_vector_to_text_array(rec.operations),
157 string_vector_to_text_array(rec.input_formats),
158 CStringGetTextDatum(rec.output_format.c_str()),
159 CStringGetTextDatum(rec.parser.c_str()),
160 Int32GetDatum(rec.preference),
161 BoolGetDatum(rec.enabled),
162 string_vector_to_text_array(rec.dependencies),
163 CStringGetTextDatum(rec.argtpl.c_str()),
164 CStringGetTextDatum(rec.argtpl_circuit.c_str()),
165 CStringGetTextDatum(rec.endpoint.c_str()),
166 };
167 SPI_execute_with_args(
168 "INSERT INTO provsql.tool_overrides "
169 "(name, removed, kind, executable, operations, input_formats, "
170 " output_format, parser, preference, enabled, dependencies, argtpl, "
171 " argtpl_circuit, endpoint) "
172 "VALUES ($1, false, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) "
173 "ON CONFLICT (name) DO UPDATE SET "
174 " removed=false, kind=$2, executable=$3, operations=$4, "
175 " input_formats=$5, output_format=$6, parser=$7, preference=$8, "
176 " enabled=$9, dependencies=$10, argtpl=$11, argtpl_circuit=$12, "
177 " endpoint=$13",
178 13, types, vals, NULL, false, 0);
179}
180
181/// Tombstone a name (removed=true) so the seeded default, if any, is hidden.
182void tombstone_override(const std::string &name)
183{
184 Oid types[1] = {TEXTOID};
185 Datum vals[1] = {CStringGetTextDatum(name.c_str())};
186 SPI_execute_with_args(
187 "INSERT INTO provsql.tool_overrides (name, removed) VALUES ($1, true) "
188 "ON CONFLICT (name) DO UPDATE SET removed=true, kind=NULL, "
189 " executable=NULL, operations=NULL, input_formats=NULL, "
190 " output_format=NULL, parser=NULL, preference=NULL, enabled=NULL, "
191 " dependencies=NULL, argtpl=NULL, argtpl_circuit=NULL, endpoint=NULL",
192 1, types, vals, NULL, false, 0);
193}
194
195} // namespace
196
197/**
198 * @brief Rebuild the in-memory registry as "compiled seed overlaid with the
199 * provsql.tool_overrides rows". See @ref tool_registry_sync.h.
200 */
202{
204 reg.reset(); // back to the compiled-in defaults
205
206 if (SPI_connect() != SPI_OK_CONNECT)
207 return; // cannot read; the seed stands
208 if (overrides_table_exists()) {
209 if (SPI_execute(
210 "SELECT name, removed, kind, executable, operations, input_formats, "
211 " output_format, parser, preference, enabled, dependencies, argtpl, "
212 " argtpl_circuit, endpoint FROM provsql.tool_overrides", true, 0)
213 == SPI_OK_SELECT) {
214 TupleDesc td = SPI_tuptable->tupdesc;
215 for (uint64 i = 0; i < SPI_processed; ++i) {
216 HeapTuple t = SPI_tuptable->vals[i];
217 std::string name = spi_text(t, td, 1);
218 bool isnull;
219 Datum rd = SPI_getbinval(t, td, 2, &isnull);
220 if (!isnull && DatumGetBool(rd)) { // tombstone
221 reg.remove(name);
222 continue;
223 }
225 rec.name = name;
226 rec.kind = spi_text(t, td, 3);
227 rec.binary = spi_text(t, td, 4);
228 rec.operations = spi_text_array(t, td, 5);
229 rec.input_formats = spi_text_array(t, td, 6);
230 rec.output_format = spi_text(t, td, 7);
231 rec.parser = spi_text(t, td, 8);
232 Datum pd = SPI_getbinval(t, td, 9, &isnull);
233 rec.preference = isnull ? 0 : DatumGetInt32(pd);
234 Datum ed = SPI_getbinval(t, td, 10, &isnull);
235 rec.enabled = isnull ? true : DatumGetBool(ed);
236 rec.dependencies = spi_text_array(t, td, 11);
237 rec.argtpl = spi_text(t, td, 12);
238 rec.argtpl_circuit = spi_text(t, td, 13);
239 rec.endpoint = spi_text(t, td, 14);
240 reg.upsert(rec);
241 }
242 }
243 }
244 SPI_finish();
245}
246
247/**
248 * @brief Set-returning listing of the registry, one row per record.
249 *
250 * Columns: name, kind, binary, operations (text[]), input_formats (text[]),
251 * output_format (text), parser (text), preference (int), enabled (bool),
252 * argtpl (text), argtpl_circuit (text), available (bool). @c operations /
253 * @c input_formats /
254 * @c output_format use the KCMCP registry names; @c parser is the CLI-only
255 * decode tag. @c available is true iff @c binary (when set) and every
256 * dependency resolve via @c find_external_tool, so the view reflects what a
257 * subsequent dispatch would actually find on the backend's PATH.
258 */
259extern "C" Datum
260tool_registry_list(PG_FUNCTION_ARGS)
261{
262 // Reflect any persisted overrides (from this or another backend).
264
265 ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
266
267 MemoryContext per_query_ctx = rsinfo->econtext->ecxt_per_query_memory;
268 MemoryContext oldcontext = MemoryContextSwitchTo(per_query_ctx);
269
270 TupleDesc tupdesc;
271 if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) {
272 MemoryContextSwitchTo(oldcontext);
273 provsql_error("tool_registry_list: function must return a row type");
274 }
275 tupdesc = BlessTupleDesc(tupdesc);
276
277 Tuplestorestate *tupstore = tuplestore_begin_heap(
278 rsinfo->allowedModes & SFRM_Materialize_Random, false, work_mem);
279 rsinfo->returnMode = SFRM_Materialize;
280 rsinfo->setResult = tupstore;
281 rsinfo->setDesc = tupdesc;
282
283 try {
284 for (const provsql::ToolRecord &rec : provsql::tool_registry().records()) {
285 Datum values[13];
286 bool nulls[13] = {false, false, false, false, false, false, false,
287 false, false, false, false, false, false};
288
289 values[0] = PointerGetDatum(cstring_to_text_with_len(rec.name.data(),
290 rec.name.size()));
291 values[1] = PointerGetDatum(cstring_to_text_with_len(rec.kind.data(),
292 rec.kind.size()));
293 values[2] = PointerGetDatum(cstring_to_text_with_len(rec.binary.data(),
294 rec.binary.size()));
295 values[3] = string_vector_to_text_array(rec.operations);
296 values[4] = string_vector_to_text_array(rec.input_formats);
297 values[5] = PointerGetDatum(cstring_to_text_with_len(
298 rec.output_format.data(), rec.output_format.size()));
299 values[6] = PointerGetDatum(cstring_to_text_with_len(rec.parser.data(),
300 rec.parser.size()));
301 values[7] = Int32GetDatum(rec.preference);
302 values[8] = BoolGetDatum(rec.enabled);
303 values[9] = PointerGetDatum(cstring_to_text_with_len(rec.argtpl.data(),
304 rec.argtpl.size()));
305 values[10] = PointerGetDatum(cstring_to_text_with_len(
306 rec.argtpl_circuit.data(), rec.argtpl_circuit.size()));
307 values[11] = PointerGetDatum(cstring_to_text_with_len(
308 rec.endpoint.data(), rec.endpoint.size()));
309 values[12] = BoolGetDatum(toolAvailable(rec));
310
311 tuplestore_putvalues(tupstore, tupdesc, values, nulls);
312 }
313 } catch (const std::exception &e) {
314 MemoryContextSwitchTo(oldcontext);
315 provsql_error("tool_registry_list: %s", e.what());
316 } catch (...) {
317 MemoryContextSwitchTo(oldcontext);
318 provsql_error("tool_registry_list: unknown exception");
319 }
320
321 MemoryContextSwitchTo(oldcontext);
322 PG_RETURN_NULL();
323}
324
325/**
326 * @brief Register a tool, or replace the record with the same name.
327 *
328 * Args (in order): name text, executable text, kind text, operations text[],
329 * input_formats text[], output_format text, parser text, argtpl text,
330 * argtpl_circuit text, preference int, enabled bool, endpoint text. A NULL
331 * @c executable defaults to @c name; a NULL @c kind defaults to @c 'cli';
332 * NULL arrays are empty; NULL text fields default to empty; NULL
333 * @c preference is 0 and NULL @c enabled is true; @c endpoint is the KCMCP
334 * server address for a @c 'kcmcp' record. Superuser-only.
335 */
336extern "C" Datum
337tool_registry_register(PG_FUNCTION_ARGS)
338{
339 require_superuser("register_tool");
340
341 if (PG_ARGISNULL(0))
342 provsql_error("register_tool: name must not be NULL");
343
344 try {
346 rec.name = text_to_string(PG_GETARG_TEXT_PP(0));
347 rec.binary = PG_ARGISNULL(1) ? rec.name
348 : text_to_string(PG_GETARG_TEXT_PP(1));
349 rec.kind = PG_ARGISNULL(2) ? std::string("cli")
350 : text_to_string(PG_GETARG_TEXT_PP(2));
351 if (!PG_ARGISNULL(3))
352 rec.operations = text_array_to_string_vector(PG_GETARG_ARRAYTYPE_P(3));
353 if (!PG_ARGISNULL(4))
354 rec.input_formats = text_array_to_string_vector(PG_GETARG_ARRAYTYPE_P(4));
355 if (!PG_ARGISNULL(5))
356 rec.output_format = text_to_string(PG_GETARG_TEXT_PP(5));
357 if (!PG_ARGISNULL(6))
358 rec.parser = text_to_string(PG_GETARG_TEXT_PP(6));
359 if (!PG_ARGISNULL(7))
360 rec.argtpl = text_to_string(PG_GETARG_TEXT_PP(7));
361 if (!PG_ARGISNULL(8))
362 rec.argtpl_circuit = text_to_string(PG_GETARG_TEXT_PP(8));
363 rec.preference = PG_ARGISNULL(9) ? 0 : PG_GETARG_INT32(9);
364 rec.enabled = PG_ARGISNULL(10) ? true : PG_GETARG_BOOL(10);
365 if (!PG_ARGISNULL(11))
366 rec.endpoint = text_to_string(PG_GETARG_TEXT_PP(11));
367
368 if (rec.name.empty())
369 provsql_error("register_tool: name must not be empty");
370
371 // Persist the full record (create or replace) in the overrides table.
372 if (SPI_connect() != SPI_OK_CONNECT)
373 provsql_error("register_tool: SPI_connect failed");
374 upsert_override(rec);
375 SPI_finish();
376 } catch (const std::exception &e) {
377 provsql_error("register_tool: %s", e.what());
378 }
379
380 PG_RETURN_VOID();
381}
382
383/**
384 * @brief Remove a tool record. Errors if no tool of that name is currently
385 * effective, so a typo fails loudly rather than silently doing nothing.
386 * A removed seeded default is recorded as a tombstone; the change persists.
387 */
388extern "C" Datum
390{
391 require_superuser("unregister_tool");
392 std::string name = text_to_string(PG_GETARG_TEXT_PP(0));
393
395 if (provsql::tool_registry().find(name) == nullptr)
396 provsql_error("unregister_tool: no tool named '%s' is registered",
397 name.c_str());
398
399 if (SPI_connect() != SPI_OK_CONNECT)
400 provsql_error("unregister_tool: SPI_connect failed");
401 tombstone_override(name);
402 SPI_finish();
403 PG_RETURN_VOID();
404}
405
406/// Persist a single-field change to an existing tool: load the effective
407/// record, apply @p mutate, and write the full record back. Errors on an
408/// unknown tool name.
409static void persist_tool_change(const char *fn, const std::string &name,
410 const std::function<void(provsql::ToolRecord&)> &mutate)
411{
414 if (cur == nullptr)
415 provsql_error("%s: no tool named '%s' is registered", fn, name.c_str());
416 provsql::ToolRecord rec = *cur;
417 mutate(rec);
418 if (SPI_connect() != SPI_OK_CONNECT)
419 provsql_error("%s: SPI_connect failed", fn);
420 upsert_override(rec);
421 SPI_finish();
422}
423
424/** @brief Enable or disable a tool. Errors on an unknown tool name. */
425extern "C" Datum
427{
428 require_superuser("set_tool_enabled");
429 std::string name = text_to_string(PG_GETARG_TEXT_PP(0));
430 bool enabled = PG_GETARG_BOOL(1);
431 persist_tool_change("set_tool_enabled", name,
432 [enabled](provsql::ToolRecord &r) { r.enabled = enabled; });
433 PG_RETURN_VOID();
434}
435
436/** @brief Set a tool's preference. Errors on an unknown tool name. */
437extern "C" Datum
439{
440 require_superuser("set_tool_preference");
441 std::string name = text_to_string(PG_GETARG_TEXT_PP(0));
442 int preference = PG_GETARG_INT32(1);
443 persist_tool_change("set_tool_preference", name,
444 [preference](provsql::ToolRecord &r) { r.preference = preference; });
445 PG_RETURN_VOID();
446}
In-memory catalog of the external tools ProvSQL can invoke.
The process-local registry singleton.
void upsert(const ToolRecord &rec)
Register a new tool or replace the record with the same name.
void reset()
Discard all records and re-seed the compiled-in defaults.
bool remove(const std::string &name)
Remove the record named name; returns false if none existed.
const ToolRecord * find(const std::string &name) const
Find a record by logical name, or nullptr if none is registered.
PostgreSQL cross-version compatibility shims for ProvSQL.
#define TYPALIGN_INT
Alignment codes for the array routines (construct_array / deconstruct_array).
bool toolAvailable(const provsql::ToolRecord &rec)
True iff a registry tool can currently be used.
Helpers for invoking external command-line tools.
ToolRegistry & tool_registry()
Shorthand for ToolRegistry::instance().
Uniform error-reporting macros for ProvSQL.
#define provsql_error(fmt,...)
Report a fatal ProvSQL error and abort the current transaction.
One registered external tool.
std::string output_format
std::vector< std::string > dependencies
std::string argtpl_circuit
std::string kind
"cli" (spawn a binary) or "kcmcp" (talk to a socket server at endpoint).
std::string endpoint
KCMCP server address for kind "kcmcp": "unix:/path" or "host:port".
std::vector< std::string > input_formats
std::vector< std::string > operations
static void persist_tool_change(const char *fn, const std::string &name, const std::function< void(provsql::ToolRecord &)> &mutate)
Persist a single-field change to an existing tool: load the effective record, apply mutate,...
Datum tool_registry_set_enabled(PG_FUNCTION_ARGS)
Enable or disable a tool.
Datum tool_registry_list(PG_FUNCTION_ARGS)
Set-returning listing of the registry, one row per record.
Datum tool_registry_unregister(PG_FUNCTION_ARGS)
Remove a tool record.
void provsql_sync_tool_registry()
Rebuild the in-memory registry as "compiled seed overlaid with the provsql.tool_overrides rows"...
Datum tool_registry_set_preference(PG_FUNCTION_ARGS)
Set a tool's preference.
Datum tool_registry_register(PG_FUNCTION_ARGS)
Register a tool, or replace the record with the same name.
Reload the in-memory external-tool registry from its persistent overrides.