SET search_path TO provsql; /** @defgroup update_provenance Update provenance (PostgreSQL 14+) * Extended provenance tracking for INSERT, UPDATE, DELETE, and UNDO * operations, including temporal validity ranges. * @{ */ /** * @brief Table recording the history of INSERT, UPDATE, DELETE, and UNDO operations * * Each row records one provenance-tracked modification, linking the * operation's provenance token to metadata (query text, type, user, * timestamp) and the temporal validity range of the affected rows. * * A row of type @c TRANSACTION stands for the transaction the statements * around it belong to (see @c provsql.transaction_token): @c xid is its * transaction id and its own @c tx_token is NULL, while every statement * row of that transaction carries the transaction's token in @c tx_token. * A modified tuple's provenance names both -- the effect is * @c times(tx_token, statement_token) -- so @c undo() reverses either one * statement or the whole transaction, and the temporal semiring reads the * transaction's validity through the shared factor. * * @c ts and the lower bound of @c valid_time are stamped at commit, not * at the statement: @c CURRENT_TIMESTAMP is the transaction's start time, * so two overlapping transactions could otherwise commit in the opposite * order of their recorded validity. */ CREATE TABLE update_provenance ( provsql uuid, query text, query_type query_type_enum, username text, ts timestamp DEFAULT CURRENT_TIMESTAMP, valid_time tstzmultirange DEFAULT tstzmultirange(tstzrange(CURRENT_TIMESTAMP, NULL)), xid xid8, tx_token uuid ); /** * @brief The update gate standing for the current transaction * * Each data-modification statement already mints an @c update gate of its * own, but nothing tied the statements of one transaction together: a * reader of @c update_provenance could not tell that two rows came from * the same transaction, and @c undo() could reverse a statement but not * "the transaction". This mints one gate per transaction, on the first * tracked modification, and hands the same one back for the rest of it. * * Where a transaction's gate lives is @c SET @c LOCAL, so it vanishes * when the transaction ends, whether it commits or rolls back -- and a * rolled-back transaction leaves no @c update_provenance row for it * either, since that row is an ordinary heap insert. */ CREATE OR REPLACE FUNCTION transaction_token() RETURNS uuid LANGUAGE plpgsql AS $$ DECLARE tok text; new_tok uuid; query_text text; BEGIN tok := current_setting('provsql.transaction_token', true); IF tok IS NOT NULL AND tok <> '' THEN RETURN tok::uuid; END IF; new_tok := public.uuid_generate_v4(); PERFORM create_gate(new_tok, 'update'); PERFORM set_config('provsql.transaction_token', new_tok::text, true); -- A transaction has no query text of its own: query is left NULL, so -- looking a statement up by its text finds the statement and not the -- transaction that carried it. query_text := NULL; -- The transaction's own validity is the universal range, the -- multiplicative identity of the temporal m-semiring: it is a factor of -- every effect of the transaction, and what a tuple is valid for is the -- statement's business, not the transaction's. Giving it a real -- interval would intersect it into every one of them. INSERT INTO update_provenance(provsql, query, query_type, username, ts, valid_time, xid) VALUES (new_tok, query_text, 'TRANSACTION', current_user, CURRENT_TIMESTAMP, '{(,)}'::tstzmultirange, pg_current_xact_id()); RETURN new_tok; END; $$; /** * @brief Deferred trigger stamping a log row with its commit time * * @c CURRENT_TIMESTAMP is the transaction's *start* time, so two * overlapping transactions can commit in the opposite order of the * validity they recorded. This fires at commit -- it is a constraint * trigger declared @c DEFERRABLE @c INITIALLY @c DEFERRED -- and moves * the row's timestamp and the lower bound of its validity to * @c clock_timestamp(), which by then is the commit time to within the * commit itself. */ CREATE OR REPLACE FUNCTION stamp_commit_time() RETURNS trigger AS $$ DECLARE now_ts timestamptz := clock_timestamp(); BEGIN UPDATE update_provenance SET ts = now_ts, valid_time = CASE WHEN query_type IN ('TRANSACTION', 'REPLACE') THEN valid_time ELSE tstzmultirange(tstzrange(now_ts, NULL)) END WHERE provsql = NEW.provsql; RETURN NULL; END; $$ LANGUAGE plpgsql; DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = 'provsql.update_provenance'::regclass AND tgname = 'stamp_commit_time') THEN CREATE CONSTRAINT TRIGGER stamp_commit_time AFTER INSERT ON provsql.update_provenance DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION provsql.stamp_commit_time(); END IF; END $$; /** @cond INTERNAL */ /* Enable provenance tracking on an existing table (PostgreSQL 14+ version). * Overrides the common version; documented via add_provenance in provsql.common.sql. */ CREATE OR REPLACE FUNCTION add_provenance(_tbl regclass) RETURNS void AS $$ BEGIN -- Idempotence: a second add_provenance on an already-tracked table is -- a no-op with a NOTICE, so setup scripts and notebook cells can be -- re-run freely. IF EXISTS ( SELECT 1 FROM pg_attribute WHERE attrelid = _tbl AND attname = 'provsql' AND NOT attisdropped ) THEN RAISE NOTICE 'table % already has provenance tracking', _tbl; RETURN; END IF; -- See the common-version body for the rationale of dropping the -- column DEFAULT and UNIQUE in favour of provenance_guard + a -- plain index. EXECUTE format('ALTER TABLE %s ADD COLUMN provsql UUID', _tbl); EXECUTE format( 'UPDATE %s SET provsql = public.uuid_generate_v4() WHERE provsql IS NULL', _tbl); EXECUTE format('CREATE INDEX ON %s(provsql)', _tbl); EXECUTE format( 'CREATE TRIGGER provenance_guard BEFORE INSERT OR UPDATE OF provsql ' 'ON %s FOR EACH ROW EXECUTE FUNCTION provsql.provenance_guard()', _tbl); EXECUTE format('CREATE TRIGGER insert_statement AFTER INSERT ON %s REFERENCING NEW TABLE AS NEW_TABLE FOR EACH STATEMENT EXECUTE FUNCTION provsql.insert_statement_trigger()', _tbl); EXECUTE format('CREATE TRIGGER delete_statement AFTER DELETE ON %s REFERENCING OLD TABLE AS OLD_TABLE FOR EACH STATEMENT EXECUTE FUNCTION provsql.delete_statement_trigger()', _tbl); EXECUTE format('CREATE TRIGGER update_statement AFTER UPDATE ON %s REFERENCING OLD TABLE AS OLD_TABLE NEW TABLE AS NEW_TABLE FOR EACH STATEMENT EXECUTE FUNCTION provsql.update_statement_trigger()', _tbl); PERFORM provsql.set_table_info(_tbl::oid, 'tid'); PERFORM provsql.set_ancestors(_tbl::oid, ARRAY[_tbl::oid]); END $$ LANGUAGE plpgsql SECURITY DEFINER; /** @endcond */ /** @cond INTERNAL */ /* Trigger function for DELETE statement provenance tracking (PostgreSQL 14+). * Overrides the common version; documented via delete_statement_trigger in provsql.common.sql. */ CREATE OR REPLACE FUNCTION delete_statement_trigger() RETURNS TRIGGER AS $$ DECLARE query_text TEXT; delete_token UUID; old_token UUID; new_token UUID; r RECORD; tx_token UUID; enable_trigger BOOL; BEGIN enable_trigger := current_setting('provsql.update_provenance', true); IF enable_trigger = 'f' THEN RETURN NULL; END IF; delete_token := public.uuid_generate_v4(); PERFORM create_gate(delete_token, 'update'); SELECT query INTO query_text FROM pg_stat_activity WHERE pid = pg_backend_pid(); tx_token := transaction_token(); INSERT INTO update_provenance (provsql, query, query_type, username, ts, valid_time, xid, tx_token) VALUES (delete_token, query_text, 'DELETE', current_user, CURRENT_TIMESTAMP, tstzmultirange(tstzrange(CURRENT_TIMESTAMP, NULL)), pg_current_xact_id(), tx_token); -- The effect this statement has on a row names both the statement and -- the transaction it belongs to, so undo() can reverse either. delete_token := provenance_times(tx_token, delete_token); PERFORM set_config('provsql.update_provenance', 'off', true); EXECUTE format('INSERT INTO %I.%I SELECT * FROM OLD_TABLE;', TG_TABLE_SCHEMA, TG_TABLE_NAME); PERFORM set_config('provsql.update_provenance', 'on', true); FOR r IN (SELECT * FROM OLD_TABLE) LOOP old_token := r.provsql; new_token := provenance_monus(old_token, delete_token); PERFORM set_config('provsql.update_provenance', 'off', true); EXECUTE format('UPDATE %I.%I SET provsql = $1 WHERE provsql = $2;', TG_TABLE_SCHEMA, TG_TABLE_NAME) USING new_token, old_token; PERFORM set_config('provsql.update_provenance', 'on', true); END LOOP; RETURN NULL; END $$ LANGUAGE plpgsql SET search_path=provsql,pg_temp SECURITY DEFINER; /** @endcond */ /** * @brief Trigger function for INSERT statement provenance tracking * * Records the insertion in update_provenance and multiplies provenance * tokens of inserted rows with the insert token. */ CREATE OR REPLACE FUNCTION insert_statement_trigger() RETURNS TRIGGER AS $$ DECLARE query_text TEXT; insert_token UUID; old_token UUID; new_token UUID; r RECORD; tx_token UUID; enable_trigger BOOL; BEGIN enable_trigger := current_setting('provsql.update_provenance', true); IF enable_trigger = 'f' THEN RETURN NULL; END IF; insert_token := public.uuid_generate_v4(); PERFORM create_gate(insert_token, 'update'); SELECT query INTO query_text FROM pg_stat_activity WHERE pid = pg_backend_pid(); tx_token := transaction_token(); INSERT INTO update_provenance (provsql, query, query_type, username, ts, valid_time, xid, tx_token) VALUES (insert_token, query_text, 'INSERT', current_user, CURRENT_TIMESTAMP, tstzmultirange(tstzrange(CURRENT_TIMESTAMP, NULL)), pg_current_xact_id(), tx_token); -- The effect this statement has on a row names both the statement and -- the transaction it belongs to, so undo() can reverse either. insert_token := provenance_times(tx_token, insert_token); FOR r IN (SELECT * FROM NEW_TABLE) LOOP old_token := r.provsql; new_token := provenance_times(old_token, insert_token); PERFORM set_config('provsql.update_provenance', 'off', true); EXECUTE format('UPDATE %I.%I SET provsql = $1 WHERE provsql = $2;', TG_TABLE_SCHEMA, TG_TABLE_NAME) USING new_token, old_token; PERFORM set_config('provsql.update_provenance', 'on', true); END LOOP; RETURN NULL; END $$ LANGUAGE plpgsql SET search_path=provsql,pg_temp SECURITY DEFINER; /** * @brief Trigger function for UPDATE statement provenance tracking * * Records the update in update_provenance. Multiplies new-row tokens * with the update token and applies monus to old-row tokens. */ CREATE OR REPLACE FUNCTION update_statement_trigger() RETURNS TRIGGER AS $$ DECLARE query_text TEXT; update_token UUID; old_token UUID; new_token UUID; r RECORD; tx_token UUID; enable_trigger BOOL; BEGIN enable_trigger := current_setting('provsql.update_provenance', true); IF enable_trigger = 'f' THEN RETURN NULL; END IF; update_token := public.uuid_generate_v4(); PERFORM create_gate(update_token, 'update'); SELECT query INTO query_text FROM pg_stat_activity WHERE pid = pg_backend_pid(); tx_token := transaction_token(); INSERT INTO update_provenance (provsql, query, query_type, username, ts, valid_time, xid, tx_token) VALUES (update_token, query_text, 'UPDATE', current_user, CURRENT_TIMESTAMP, tstzmultirange(tstzrange(CURRENT_TIMESTAMP, NULL)), pg_current_xact_id(), tx_token); -- The effect this statement has on a row names both the statement and -- the transaction it belongs to, so undo() can reverse either. update_token := provenance_times(tx_token, update_token); FOR r IN (SELECT * FROM NEW_TABLE) LOOP old_token := r.provsql; new_token := provenance_times(old_token, update_token); PERFORM set_config('provsql.update_provenance', 'off', true); EXECUTE format('UPDATE %I.%I SET provsql = $1 WHERE provsql = $2;', TG_TABLE_SCHEMA, TG_TABLE_NAME) USING new_token, old_token; PERFORM set_config('provsql.update_provenance', 'on', true); END LOOP; PERFORM set_config('provsql.update_provenance', 'off', true); EXECUTE format('INSERT INTO %I.%I SELECT * FROM OLD_TABLE;', TG_TABLE_SCHEMA, TG_TABLE_NAME); PERFORM set_config('provsql.update_provenance', 'on', true); FOR r IN (SELECT * FROM OLD_TABLE) LOOP old_token := r.provsql; new_token := provenance_monus(old_token, update_token); PERFORM set_config('provsql.update_provenance', 'off', true); EXECUTE format('UPDATE %I.%I SET provsql = $1 WHERE provsql = $2;', TG_TABLE_SCHEMA, TG_TABLE_NAME) USING new_token, old_token; PERFORM set_config('provsql.update_provenance', 'on', true); END LOOP; RETURN NULL; END $$ LANGUAGE plpgsql SET search_path=provsql,pg_temp SECURITY DEFINER; /** @} */ /** @defgroup temporal_db Temporal DB (PostgreSQL 14+) * Functions for temporal database support. These use provenance * evaluation over the multirange semiring to track temporal validity * of tuples. * @{ */ SET search_path TO provsql; /** * @brief Evaluate provenance over the temporal (interval-union) m-semiring * * Inputs are read as %tstzmultirange validity intervals; the additive * identity is '{}'::%tstzmultirange (empty), the multiplicative * identity is '{(,)}'::%tstzmultirange (universal). Returns the union * of intervals supporting the result, computed via the compiled circuit * traversal. * * @param token Provenance token to evaluate. * @param token2value Mapping from input gates to validity multiranges. */ CREATE FUNCTION sr_temporal(token ANYELEMENT, token2value regclass) RETURNS tstzmultirange AS $$ BEGIN RETURN provsql.provenance_evaluate_compiled( token, token2value, 'interval_union', '{(,)}'::tstzmultirange ); END $$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE; /** * @brief Evaluate provenance over the interval-union m-semiring * with a numeric multirange carrier * * Inputs are read as %nummultirange validity ranges over a numeric * domain (e.g. sensor measurement-validity ranges). Addition is * multirange union, multiplication is intersection, monus is set * difference; the additive identity is '{}'::%nummultirange * and the multiplicative identity is '{(,)}'::%nummultirange * (universal range). * * @param token Provenance token to evaluate. * @param token2value Mapping from input gates to numeric multiranges. */ CREATE FUNCTION sr_interval_num(token ANYELEMENT, token2value regclass) RETURNS nummultirange AS $$ BEGIN RETURN provsql.provenance_evaluate_compiled( token, token2value, 'interval_union', '{(,)}'::nummultirange ); END $$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE; /** * @brief Evaluate provenance over the interval-union m-semiring * with an int4 multirange carrier * * Inputs are read as %int4multirange validity ranges over the * integers (e.g. page or line ranges of supporting documents). * Addition is multirange union, multiplication is intersection, * monus is set difference; the additive identity is * '{}'::%int4multirange and the multiplicative identity is * '{(,)}'::%int4multirange. * * @param token Provenance token to evaluate. * @param token2value Mapping from input gates to int4 multiranges. */ CREATE FUNCTION sr_interval_int(token ANYELEMENT, token2value regclass) RETURNS int4multirange AS $$ BEGIN RETURN provsql.provenance_evaluate_compiled( token, token2value, 'interval_union', '{(,)}'::int4multirange ); END $$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE; /** * @brief Evaluate temporal provenance as a timestamp multirange * * Thin wrapper around :sqlfunc:`sr_temporal` retained for backward * compatibility; both compute the same union of validity intervals. * * @param token provenance token to evaluate * @param token2value mapping table from tokens to temporal validity ranges */ CREATE OR REPLACE FUNCTION union_tstzintervals( token UUID, token2value regclass ) RETURNS tstzmultirange AS $$ SELECT sr_temporal(token, token2value) $$ LANGUAGE SQL PARALLEL SAFE STABLE; /** * @brief Query a table as it was at a specific point in time * * Returns all rows whose temporal validity includes the given timestamp. * * @param tablename name of the provenance-tracked table * @param at_time the point in time to query */ CREATE OR REPLACE FUNCTION timetravel( tablename text, at_time timestamptz ) RETURNS SETOF record LANGUAGE plpgsql AS $$ BEGIN RETURN QUERY EXECUTE format( ' SELECT %1$I.*, sr_temporal(provenance(), %2$L) FROM %1$I WHERE sr_temporal(provenance(), %2$L) @> %3$L::timestamptz ', tablename, 'provsql.time_validity_view', at_time::text ); END; $$; /** * @brief Query a table for rows valid during a time interval * * Returns all rows whose temporal validity overlaps the given range. * * @param tablename name of the provenance-tracked table * @param from_time start of the time interval * @param to_time end of the time interval */ CREATE OR REPLACE FUNCTION timeslice( tablename text, from_time timestamptz, to_time timestamptz ) RETURNS SETOF record LANGUAGE plpgsql AS $$ BEGIN RETURN QUERY EXECUTE format( ' SELECT %1$I.*, sr_temporal(provenance(), %2$L) FROM %1$I WHERE sr_temporal(provenance(), %2$L) && tstzrange(%3$L::timestamptz, %4$L::timestamptz) ', tablename, 'provsql.time_validity_view', from_time::text, to_time::text ); END; $$; /** * @brief Query the full temporal history of specific rows * * Returns all versions of rows matching the given column values, * with their temporal validity ranges. * * @param tablename name of the provenance-tracked table * @param col_names array of column names to filter on * @param col_values array of corresponding values to match */ CREATE OR REPLACE FUNCTION history( tablename text, col_names text[], col_values text[] ) RETURNS SETOF record LANGUAGE plpgsql AS $$ DECLARE condition text := ''; i int; BEGIN IF array_length(col_names, 1) IS NULL OR array_length(col_values, 1) IS NULL OR array_length(col_names, 1) != array_length(col_values, 1) THEN RAISE EXCEPTION 'col_names and col_values must have the same (non-null) length'; END IF; FOR i IN 1..array_length(col_names, 1) LOOP IF i > 1 THEN condition := condition || ' AND '; END IF; condition := condition || format('%I = %L', col_names[i], col_values[i]); END LOOP; RETURN QUERY EXECUTE format( ' SELECT %I.*, sr_temporal(provenance(), %L) FROM %I WHERE %s ', tablename, 'provsql.time_validity_view', tablename, condition ); END; $$; /** * @brief Get the valid time range for a specific tuple * * @param token provenance token of the tuple * @param tablename name of the table containing the tuple */ CREATE OR REPLACE FUNCTION get_valid_time( token uuid, tablename text ) RETURNS tstzmultirange LANGUAGE plpgsql AS $$ DECLARE result tstzmultirange; BEGIN EXECUTE format( ' SELECT sr_temporal(provenance(), %L) FROM %I WHERE provsql = %L ', 'provsql.time_validity_view', tablename, token ) INTO result; RETURN result; END; $$; /** * @brief Undo a previously recorded update operation * * Traverses all provenance-tracked tables and rewrites their circuits * to apply monus with respect to the given update token, effectively * undoing the operation. * * @param c UUID of the update operation to undo (from update_provenance) */ CREATE OR REPLACE FUNCTION undo( c uuid ) RETURNS uuid LANGUAGE plpgsql AS $$ DECLARE undo_query text; undone_query text; undo_token uuid; schema_rec RECORD; table_rec RECORD; row_rec RECORD; new_x uuid; tracking text := current_setting('provsql.update_provenance'); BEGIN -- Test for the row, not for its query text: a TRANSACTION row has no -- query of its own, and undoing a whole transaction is exactly what it -- is there for. SELECT query INTO undone_query FROM update_provenance WHERE provsql = c LIMIT 1; IF NOT FOUND THEN RAISE NOTICE 'Unable to find % in update_provenance', c; RETURN c; END IF; SELECT query INTO undo_query FROM pg_stat_activity WHERE pid = pg_backend_pid(); undo_token := public.uuid_generate_v4(); PERFORM create_gate(undo_token, 'update'); INSERT INTO update_provenance(provsql, query, query_type, username, ts, valid_time, xid, tx_token) VALUES ( undo_token, undo_query, 'UNDO', current_user, CURRENT_TIMESTAMP, tstzmultirange(tstzrange(CURRENT_TIMESTAMP, NULL)), pg_current_xact_id(), transaction_token() ); -- Off for the rewriting below, then back to what it was, for this -- transaction only: a session-level change would outlive it. PERFORM set_config('provsql.update_provenance', 'off', true); FOR schema_rec IN SELECT nspname FROM pg_namespace WHERE nspname NOT IN ('pg_catalog','information_schema','pg_toast','pg_temp_1','pg_toast_temp_1') LOOP FOR table_rec IN EXECUTE format('SELECT tablename AS tname FROM pg_tables WHERE schemaname = %L', schema_rec.nspname) LOOP IF EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = schema_rec.nspname AND table_name = table_rec.tname AND table_name <> 'update_provenance' AND column_name = 'provsql' ) THEN FOR row_rec IN EXECUTE format('SELECT provsql AS x FROM %I.%I', schema_rec.nspname, table_rec.tname) LOOP new_x := replace_the_circuit(row_rec.x, c, undo_token); EXECUTE format('UPDATE %I.%I SET provsql = $1 WHERE provsql = $2', schema_rec.nspname, table_rec.tname) USING new_x, row_rec.x; END LOOP; END IF; END LOOP; END LOOP; PERFORM set_config('provsql.update_provenance', tracking, true); RETURN undo_token; END; $$; /** * @brief Recursively rewrite a circuit to undo a specific operation * * Helper for undo(). Walks the circuit and replaces occurrences of * the target update gate with its monus. * * @param x provenance token to rewrite * @param c UUID of the update operation to undo * @param u UUID of the undo operation */ CREATE OR REPLACE FUNCTION replace_the_circuit( x uuid, c uuid, u uuid ) RETURNS uuid LANGUAGE plpgsql AS $$ DECLARE nchildren uuid[]; child uuid; ntoken uuid; ntype provenance_gate; BEGIN IF x = c THEN RETURN provenance_monus(c, u); -- update and input gates cannot have children ELSIF get_gate_type(x) = 'update' OR get_gate_type(x) = 'input' THEN RETURN x; ELSE nchildren := '{}'; FOREACH child IN ARRAY get_children(x) LOOP nchildren := array_append(nchildren, replace_the_circuit(child, c, u)); END LOOP; ntoken := public.uuid_generate_v4(); ntype := get_gate_type(x); PERFORM create_gate(ntoken, ntype, nchildren); RETURN ntoken; END IF; END; $$; /** * @brief Rewrite a circuit, substituting one gate for another * * Walks @p x and rebuilds every gate above an occurrence of @p old over * @p new instead. Leaves that are not @p old come back unchanged, so a * token that does not mention @p old is returned as it is. * * @param x the token to rewrite * @param old the gate to substitute away * @param new the gate to put in its place */ CREATE OR REPLACE FUNCTION substitute_gate( x uuid, old uuid, new uuid ) RETURNS uuid LANGUAGE plpgsql AS $$ DECLARE nchildren uuid[]; child uuid; rewritten uuid; changed boolean := false; ntoken uuid; ntype provenance_gate; BEGIN IF x = old THEN RETURN new; END IF; ntype := get_gate_type(x); -- Leaves have no children to walk into. IF ntype IN ('input', 'update', 'rv', 'value', 'zero', 'one') THEN RETURN x; END IF; nchildren := '{}'; FOREACH child IN ARRAY get_children(x) LOOP rewritten := substitute_gate(child, old, new); IF rewritten <> child THEN changed := true; END IF; nchildren := array_append(nchildren, rewritten); END LOOP; IF NOT changed THEN RETURN x; END IF; ntoken := public.uuid_generate_v4(); PERFORM create_gate(ntoken, ntype, nchildren); RETURN ntoken; END; $$; /** * @brief Give a recorded data modification a different probability * * The @c update-gate counterpart of @c provsql.replace_input. A gate's * probability is written once, so "how likely is it that this * modification happened" is changed by minting a new @c update gate with * the new probability, logging it in @c update_provenance beside the one * it replaces, and rewriting every tracked row whose provenance mentions * the old gate to mention the new one instead -- the same walk @c undo * performs. The old gate and its log row are kept: the history of the * database is not rewritten, it is extended. The new log row, of type * @c REPLACE, has the validity of the row it replaces: the modification * keeps its time, only its probability changes. * * @param old the @c update gate to replace, as found in * @c update_provenance * @param p the new probability, in [0,1] * @return the new @c update gate */ CREATE OR REPLACE FUNCTION replace_update( old uuid, p double precision ) RETURNS uuid LANGUAGE plpgsql AS $$ DECLARE new_token uuid; old_row RECORD; schema_rec RECORD; table_rec RECORD; row_rec RECORD; new_x uuid; tracking text := current_setting('provsql.update_provenance'); BEGIN IF old IS NULL OR p IS NULL THEN RAISE EXCEPTION 'replace_update: neither argument may be NULL'; END IF; IF get_gate_type(old) <> 'update' THEN RAISE EXCEPTION 'replace_update: % is not an update gate', old USING HINT = 'Use provsql.replace_input() for a tuple''s own input gate.'; END IF; SELECT * INTO old_row FROM update_provenance WHERE provsql = old LIMIT 1; IF old_row IS NULL THEN RAISE EXCEPTION 'replace_update: % is not recorded in update_provenance', old; END IF; new_token := public.uuid_generate_v4(); PERFORM create_gate(new_token, 'update'); PERFORM set_prob(new_token, p); INSERT INTO update_provenance(provsql, query, query_type, username, ts, valid_time, xid, tx_token) VALUES (new_token, old_row.query, 'REPLACE', current_user, CURRENT_TIMESTAMP, -- The modification keeps its time: only its probability changes, -- and the rows will carry the new gate in place of the old one. old_row.valid_time, pg_current_xact_id(), transaction_token()); -- Off for the rewriting below, then back to what it was, for this -- transaction only: a session-level change would outlive it. PERFORM set_config('provsql.update_provenance', 'off', true); FOR schema_rec IN SELECT nspname FROM pg_namespace WHERE nspname NOT IN ('pg_catalog','information_schema','pg_toast','pg_temp_1','pg_toast_temp_1') LOOP FOR table_rec IN EXECUTE format('SELECT tablename AS tname FROM pg_tables WHERE schemaname = %L', schema_rec.nspname) LOOP IF EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = schema_rec.nspname AND table_name = table_rec.tname AND table_name <> 'update_provenance' AND column_name = 'provsql' ) THEN FOR row_rec IN EXECUTE format('SELECT provsql AS x FROM %I.%I', schema_rec.nspname, table_rec.tname) LOOP new_x := substitute_gate(row_rec.x, old, new_token); IF new_x <> row_rec.x THEN EXECUTE format('UPDATE %I.%I SET provsql = $1 WHERE provsql = $2', schema_rec.nspname, table_rec.tname) USING new_x, row_rec.x; END IF; END LOOP; END IF; END LOOP; END LOOP; PERFORM set_config('provsql.update_provenance', tracking, true); RETURN new_token; END; $$; -- The base validity mapping is a plain view over the data-modification log: -- update_provenance is append-only and never has its provsql rewritten, so a -- view stays correct (unlike a tracked table's mapping, which must be a -- maintained mapping table -- see create_provenance_mapping(maintained)). CREATE VIEW provsql.time_validity_view AS SELECT valid_time AS value, provsql AS provenance FROM provsql.update_provenance; /** @} */ SET search_path TO public; -- Final constants-cache refresh: same rationale as at the end of -- provsql.common.sql. On PG14+ this file is appended after the common -- script, so this is the last statement of the generated install script; -- the refresh must come after every object has been created for the -- installing session's memoized constants to be complete. SELECT provsql.reset_constants_cache();