-- Deterministic: no timestamps, no durations, no oids. Where a time matters the -- test asserts that it EXISTS rather than what it says. -- -- terse because the default DETAIL prints the failing row -- which carries a -- clock_timestamp and current_user -- and CONTEXT prints line numbers inside -- plpgsql functions. Both would make the expected output depend on the machine -- and on where a line happens to sit in the file. \set VERBOSITY terse CREATE EXTENSION pg_living_assertions; SET search_path = living_assertions, public; -- ------------------------------------------------------------ the states -- -- The whole extension is the claim that these are six different things. If any -- two of them collapsed into one answer, the piece would not be worth having. CREATE TABLE blanco (x int); SELECT declare('true_one', 'this one is simply true', 'select true as holds') > 0 AS declared; declared ---------- t (1 row) SELECT declare('false_one', 'this one is simply false', 'select false as holds') > 0 AS declared; declared ---------- t (1 row) SELECT declare('no_data', 'nothing to judge with yet', 'select null::boolean as holds') > 0 AS declared; declared ---------- t (1 row) SELECT declare('five_rows', 'answers with more than one row','select true as holds from generate_series(1,5)') > 0 AS declared; declared ---------- t (1 row) SELECT declare('typo', 'the check has a typo in it', 'select tru as holds') > 0 AS declared; declared ---------- t (1 row) SELECT declare('writes', 'the check tries to write', 'insert into blanco values (1) returning true as holds') > 0 AS declared; declared ---------- t (1 row) SELECT declare('never_run', 'declared but not checked yet', 'select true as holds', NULL, NULL, false) > 0 AS declared; declared ---------- t (1 row) SELECT name, state FROM status ORDER BY name; name | state -----------+----------- false_one | broken five_rows | erroring never_run | unchecked no_data | unknown true_one | holds typo | erroring writes | erroring (7 rows) -- A name nobody registered is not "fine": it is nothing watching. SELECT state('nobody_declared_this') AS unregistered; unregistered -------------- unregistered (1 row) -- THE GUARANTEE THAT IS NOT A COMMENT: the stored SQL runs inside a STABLE -- function, so PostgreSQL itself refuses the write. Reporting erroring is only -- half the proof -- the other half is that nothing was written. SELECT count(*) AS rows_written_by_the_check FROM blanco; rows_written_by_the_check --------------------------- 0 (1 row) -- And erroring is not unknown. One is a defect to fix, the other is a normal -- wait, and a registry that cannot tell them apart lets a typo sit forever -- looking like it is patiently waiting for data. SELECT state('typo') <> state('no_data') AS erroring_is_not_unknown; erroring_is_not_unknown ------------------------- t (1 row) SELECT state('never_run') <> state('no_data') AS unchecked_is_not_unknown; unchecked_is_not_unknown -------------------------- t (1 row) SELECT state('false_one') <> state('typo') AS broken_is_not_erroring; broken_is_not_erroring ------------------------ t (1 row) -- ------------------------------------------------------------- the detail -- -- A check may return a second column explaining itself. Optional: its absence -- is not an error, which is why the evaluator retries without it. SELECT declare('with_detail', 'carries its own explanation', $$select false as holds, 'only 3 of the 5 expected rows' as detail$$) > 0 AS declared; declared ---------- t (1 row) SELECT detail FROM status WHERE name = 'with_detail'; detail ------------------------------- only 3 of the 5 expected rows (1 row) -- REGRESSION: `detail` is optional, so the evaluator retries without it when -- the column is missing. If the missing column is in the CALLER's own query -- instead, the retry must raise again and be reported -- not swallowed as "this -- check simply has no detail". Until now that was only argued in a comment. -- The proof is that the message names the caller's column. SELECT declare('bad_column', 'names a column that does not exist', 'select x.nope as holds from (select 1) x') > 0 AS declared; declared ---------- t (1 row) SELECT state('bad_column') AS must_be_erroring; must_be_erroring ------------------ erroring (1 row) SELECT detail LIKE '%nope%' AS blames_the_real_column FROM status WHERE name = 'bad_column'; blames_the_real_column ------------------------ t (1 row) -- ---------------------------------------------------------------- the age -- -- The thesis in one assertion: a verdict never travels without its age. SELECT count(*) FILTER (WHERE age IS NULL AND state <> 'unchecked') AS verdicts_with_no_age FROM status; verdicts_with_no_age ---------------------- 0 (1 row) -- An unchecked assertion has no age and says so, rather than showing a blank -- that reads like a clean bill of health. SELECT name, state, age IS NULL AS no_age, detail FROM status WHERE name = 'never_run'; name | state | no_age | detail -----------+-----------+--------+--------------------------------------------------- never_run | unchecked | t | never checked: this is not a clean bill of health (1 row) -- ------------------------------------------------------------- the gate -- -- Different message per reason. A gate that treats unknown, unchecked and -- broken the same is a gate people learn to skip. -- Passing means NOT RAISING, so it is proven by getting to the notice. Written -- as `assert_holds(...) IS NULL` first, which printed f -- a void function -- returns an empty void value and not NULL, so that line asserted the opposite -- of what it read, and freezing it as expected output would have made the -- backwards version the spec. DO $$ BEGIN PERFORM assert_holds('true_one'); RAISE NOTICE 'the gate let it through'; END $$; NOTICE: the gate let it through SELECT assert_holds('false_one'); ERROR: assertion false_one is broken SELECT assert_holds('no_data'); ERROR: assertion no_data is unknown SELECT assert_holds('typo'); ERROR: assertion typo is erroring SELECT assert_holds('never_run'); ERROR: assertion never_run is unchecked SELECT assert_holds('nobody_declared_this'); ERROR: assertion nobody_declared_this is unregistered -- ------------------------------------------- declare_unchanged (0.2.0) -- -- The shape two of the four guards were writing by hand: approve what an -- expression says today, and report when it changes. CREATE TABLE mundo (x int); INSERT INTO mundo VALUES (1), (2); SELECT declare_unchanged('the_world', 'the contents of mundo are what I approved', 'select string_agg(x::text, $$,$$ ORDER BY x) from mundo') > 0 AS approved; approved ---------- t (1 row) SELECT state('the_world') AS right_after_approving; right_after_approving ----------------------- holds (1 row) SELECT detail FROM status WHERE name = 'the_world'; detail -------------------------- unchanged since approved (1 row) -- The world moves. Nobody re-supplies the value: the stored check re-evaluates -- the EXPRESSION, which is the whole point -- a stored value would be compared -- against itself forever and could never fail. INSERT INTO mundo VALUES (3); SELECT (run('the_world')).state AS after_the_world_moved; after_the_world_moved ----------------------- broken (1 row) SELECT detail LIKE 'approved %, now %' AS says_both_sides FROM status WHERE name = 'the_world'; says_both_sides ----------------- t (1 row) -- An expression that yields nothing is refused AT APPROVAL TIME, rather than -- stored and reported as a broken check forever after. SELECT declare_unchanged('nothing_there', 'approving a null', 'select null::text'); ERROR: the expression returned NULL, so there is nothing to approve -- --------------------------------------------------- not renegotiable -- -- Recording the declaration date buys nothing if UPDATE is allowed: softening -- an assertion would leave no trace and the date would be decoration. UPDATE assertions SET check_sql = 'select true as holds' WHERE name = 'false_one'; ERROR: an assertion is not edited in place UPDATE assertions SET claim = 'a nicer wording' WHERE name = 'false_one'; ERROR: an assertion is not edited in place DELETE FROM assertions WHERE name = 'false_one'; ERROR: an assertion is not deleted -- Both halves of the prohibition are tested, because a trigger that blocked -- EVERYTHING would pass "cannot edit" and "cannot delete" while being useless. -- Retiring must still work. SELECT retire('true_one', 'no longer relevant to this test'); retire -------- (1 row) -- REGRESSION: the first version of state() answered `unregistered` here, which -- is the extension committing the exact sin it exists to prevent. "Nobody ever -- watched this" and "somebody turned it off on purpose, with a reason" are -- opposite facts and send you to opposite places. SELECT state('true_one') AS retired_reads_as; retired_reads_as ------------------ retired (1 row) SELECT state('true_one') <> state('nobody_declared_this') AS retired_is_not_unregistered; retired_is_not_unregistered ----------------------------- t (1 row) SELECT retired_why FROM assertions WHERE name = 'true_one'; retired_why --------------------------------- no longer relevant to this test (1 row) -- The audit trail is append-only for the same reason. UPDATE checks SET state = 'holds' WHERE state = 'broken'; ERROR: checks are the audit trail of an assertion: they are inserted, never edited or deleted DELETE FROM checks; ERROR: checks are the audit trail of an assertion: they are inserted, never edited or deleted -- ----------------------------------------------------------- superseding -- -- Replacing costs writing down why, and the replacement retires its -- predecessor in the same statement -- leaving that to the caller is how two -- live versions of one assertion end up disagreeing. SELECT declare('false_one', 'the replacement, with a reason', 'select true as holds', 'false_one', 'the original was measuring the wrong table entirely') > 0 AS superseded; superseded ------------ t (1 row) SELECT count(*) AS live_versions FROM assertions WHERE name = 'false_one' AND retired_at IS NULL; live_versions --------------- 1 (1 row) -- A replacement with no reason is refused by the CHECK, not by convention. SELECT declare('no_data', 'a replacement with no reason given', 'select true as holds', 'no_data', 'too short'); ERROR: new row for relation "assertions" violates check constraint "replacing_an_assertion_cannot_be_silent" -- WHAT THE VIEW CAN AND CANNOT SAY. It cannot tell you an arbitrary SQL check -- got looser -- that is undecidable, and pretending otherwise is how a -- dashboard starts lying. It reports the TIMING, and flags the case worth -- seeing: replaced while its last word was broken. SELECT name, last_state_before, what_happened FROM renegotiated ORDER BY name; name | last_state_before | what_happened -----------+-------------------+----------------------- false_one | broken | REPLACED WHILE BROKEN (1 row) -- ---------------------------------------------------------------- stale -- -- "Old" and "never" both mean you do not know, and only one is fixed by -- waiting -- so they are reported together and labelled apart. SELECT name, (checked_at IS NULL) AS never_checked FROM stale('0 seconds') ORDER BY name; name | never_checked -------------+--------------- bad_column | f false_one | f five_rows | f never_run | t no_data | f the_world | f typo | f with_detail | f writes | f (9 rows) SELECT name, why FROM stale('100 years') ORDER BY name; name | why -----------+--------------- never_run | never checked (1 row) DROP EXTENSION pg_living_assertions CASCADE;