Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified docs/FOX_Checklist.pdf
Binary file not shown.
1 change: 1 addition & 0 deletions docs/FOX_Feature_Implemented.html
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ <h2 class="sec">Change log</h2>
<p class="sub">Newest first. Every change to FoxByte adds a row here.</p>
<table>
<tr><th style="width:17%">Date</th><th style="width:22%">Feature(s)</th><th>What changed</th><th style="width:14%">Commit / PR</th></tr>
<tr><td>11 Nov 2026</td><td>H1</td><td><b>Fixed: every branch start wrote 82 entries into the branch&rsquo;s Blackbox.</b> Since #221, each start of a branch takes dblink away from clients again (a REVOKE of EXECUTE on each of its 41 functions, from PUBLIC and from <code>db_client</code>), and the Blackbox records every REVOKE, held or not: a restarted branch gained 82 entries with no actor, and a branch made from just before an entry held 82 entries made after it. Each REVOKE is now issued only while the grant it removes is there, so a start that changes nothing records nothing, and a grant someone added is still taken away, and recorded, on the next start. Tests: integration-v2 403/403 (it was 401/403 on main), integration 936/936, Go unit tests in both editions (the REVOKEs are guarded by a look at the grant), vet for macOS, Linux and Windows; on the test VM a branch&rsquo;s restart added 0 entries, and one given two dblink grants lost both on restart and recorded those two REVOKEs.</td><td><code>fix/v2-branch-ledger</code></td></tr>
<tr><td>11 Nov 2026</td><td>L4</td><td><b>Table dialogs (point 12.3, second part, pgAdmin&rsquo;s basics).</b> <b>+ New table&hellip;</b> in the Console&rsquo;s schema rail makes a table with its columns and primary key, and opens on them. A table&rsquo;s <b>Structure</b> tab lists its columns from the catalog with <b>+ Add column&hellip;</b>, <b>Edit&hellip;</b> (name, type with a USING conversion, NOT NULL, default; only what changed, in one transaction, the rename last) and <b>Drop&hellip;</b>, and renames and drops the table; its <b>Constraints</b> tab adds, edits, renames and drops primary, unique, foreign and check keys: columns in the order picked, a foreign key&rsquo;s referenced table with its primary key filled in, ON DELETE and ON UPDATE, deferrable; an edit replaces the key in one statement under its name and keeps MATCH FULL, NOT VALID and NO INHERIT; a key with parts the dialog cannot write again (INCLUDE, NULLS NOT DISTINCT, an exclusion, an inherited one) is renamed and dropped but not edited. Each dialog shows its SQL and runs it as the person; DROP TABLE meets the guardrail, and <i>Put it in the editor</i> is the way to override it. The engine gives each constraint&rsquo;s parts (<code>key</code>) on <code>GET &hellip;/schema/table/{oid}</code>. Tests: integration 936/936 (&sect;11aq new: what each dialog writes run; a failed rename leaving a column edit undone; each key&rsquo;s parts, in the key&rsquo;s order; a key replaced under its name, a failed replacement keeping it, one depended on refused; keys not offered as edits; DROP TABLE refused by the guardrail, then dropped with the override), UI 61/61 plus the upgrade test (a table made, its columns added, edited and dropped, renamed, its drop refused and put in the editor; its four kinds of key added, a foreign key edited, a check renamed and dropped; a refusal shown in the dialog), integration-f5 57/57, f1 70/70, f6 71/71, web unit 294/294, Go unit tests in both editions, vet for macOS, Linux and Windows. v2 401/403: its two failures (a point-in-time branch&rsquo;s Blackbox holding 82 entries from after the entry it was made before, and a branch&rsquo;s restart adding them again) are the same on main without this change.</td><td><code>feat/p12-tables</code></td></tr>
<tr><td>10 Nov 2026</td><td>L4</td><td><b>Function and trigger dialogs (point 12.3, first part, pgAdmin&rsquo;s basics).</b> <b>+ New function&hellip;</b> in the Console&rsquo;s schema rail makes a function, a procedure or a trigger function; <b>Edit&hellip;</b> changes a body, language, volatility, strict and security definer and keeps what the dialog has no field for (LEAKPROOF, PARALLEL, COST, ROWS, each SET clause); <b>Rename&hellip;</b> and <b>Drop&hellip;</b> use its identity arguments. A table&rsquo;s Triggers tab has <b>+ New trigger&hellip;</b>, Edit, Rename and Drop: timing, events, columns for UPDATE, row or statement, WHEN, the function and its arguments; an edit keeps REFERENCING, and a disabled trigger stays disabled. Each dialog shows the SQL as it is built and runs it as the person, so their rights, the guardrails and the Blackbox apply; what a guardrail refuses can be put in the editor. FoxByte&rsquo;s own functions and triggers are not offered. The engine gives each function&rsquo;s attributes (<code>routine</code>) and each trigger&rsquo;s parts (<code>trigger</code>) on <code>GET &hellip;/schema/{kind}/{oid}</code> and marks trigger functions in the list; <code>fox schema</code>&rsquo;s output is unchanged. Fixed on the way: a slow reply about the branch just left could replace the Console&rsquo;s catalog of the branch switched to, and a table then said it was not in the catalog; each load now ignores replies to an earlier one. Tests: integration 916/916 (&sect;11ap new: what the dialogs write run, an edit keeping every attribute, a BEGIN ATOMIC function told apart, a disabled trigger staying so, a procedure&rsquo;s and a trigger&rsquo;s drop in the Blackbox by the catalog scan, what the role does not own refused), v2 403/403, UI 59/59 plus the upgrade test (a function made, edited, renamed and dropped from its dialogs; a trigger likewise; a late reply about the branch left held back and arriving), integration-f5 57/57, f1 70/70, f6 71/71, web unit 280/280, Go unit tests in both editions, vet for macOS, Linux and Windows.</td><td><code>feat/p12-routines</code></td></tr>
<tr><td>10 Nov 2026</td><td>L4</td><td><b>Run, toggle and plan (point 12.4, pgAdmin&rsquo;s basics).</b> A function or a procedure opened in the Console&rsquo;s object browser has a <b>Run</b> form: one field per argument with its type, a NULL box and, where declared, its default; the call is shown as it is built (named arguments when every one has a name, so any default may be left out, otherwise only the last), each value a literal cast to its argument&rsquo;s type, a procedure&rsquo;s OUT arguments as NULL; Run puts it in the editor and runs it as the person. The engine now gives each argument&rsquo;s name, type, mode and default (<code>params</code> on <code>GET …/schema/{kind}/{oid}</code>). A table&rsquo;s Triggers tab <b>disables</b> (after asking) and <b>enables</b> each of its own triggers, an <code>ALTER TABLE</code> run as the person and recorded in the Blackbox; FoxByte&rsquo;s own are marked and not offered. <b>Explain</b>, beside Run, shows the selected statement&rsquo;s plan, or the editor&rsquo;s only one&rsquo;s, as a table: each step under the one that uses it, what it reads, its conditions, the estimate and cost, and a bar for its share of the whole, the most expensive marked; with <b>Analyse</b> it runs inside a transaction that is rolled back, adding actual rows, loops, each step&rsquo;s own time and a warning where the estimate was ten times off. Fixed on the way: a json or jsonb array reached the console with each object inside it as text of its own (<code>["{\"a\":1}"]</code>), as an EXPLAIN plan did; it is now JSON whole. The f6 suite&rsquo;s measured forensic round wrote its 3.6 GB a minute of audit log to the test VM&rsquo;s 2 GB in-memory <code>/tmp</code>, and stopped without a word; it now writes to disk. Tests: integration 906/906 (&sect;11ao new: each argument of a function, a set-returning, an argumentless and a variadic one, and a procedure; what the form writes run, with a default left out, NULL, a variadic and a procedure&rsquo;s INOUT and OUT; a trigger disabled and enabled, firing or not, in the Blackbox, FoxByte&rsquo;s own marked; a jsonb array whole; a plan read, and with ANALYZE rolled back), v2 403/403, UI 56/56 plus the upgrade test (a function run from its form; a trigger disabled and enabled; a statement explained, and with Analyse measured and rolled back), integration-f5 57/57, f1 70/70, f6 71/71, web unit 269/269, Go unit tests in both editions, vet for macOS, Linux and Windows.</td><td><code>feat/p12-run-plan</code></td></tr>
Expand Down
Binary file modified docs/FOX_Feature_Implemented.pdf
Binary file not shown.
18 changes: 15 additions & 3 deletions internal/branch/dblink.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,14 @@ package branch
// connections from inside the database to anything the container can reach
// (audit GW-1). healDblink runs this on every start, so a branch made before
// the change loses the grant the next time it starts.
//
// Each REVOKE is issued only while the grant it takes away is there. The
// Blackbox records every GRANT and REVOKE, held or not, so revoking all 41
// functions' grants again on each start wrote 82 unattributed entries into the
// branch's Blackbox every time it started, and into a branch made from just
// before an entry, after that entry.
const ensureDblinkSQL = `DO $do$
DECLARE s name; f regprocedure;
DECLARE s name; f regprocedure; c oid;
BEGIN
SELECT n.nspname INTO s FROM pg_extension e JOIN pg_namespace n ON n.oid = e.extnamespace
WHERE e.extname = 'dblink';
Expand All @@ -40,8 +46,14 @@ BEGIN
JOIN pg_proc p ON p.oid = d.objid
WHERE d.classid = 'pg_proc'::regclass AND d.refclassid = 'pg_extension'::regclass
AND d.deptype = 'e' AND e.extname = 'dblink' LOOP
EXECUTE format('REVOKE EXECUTE ON FUNCTION %s FROM PUBLIC', f);
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'db_client') THEN
-- PUBLIC is grantee 0; a function with no ACL has the default one, which grants it.
IF EXISTS (SELECT 1 FROM pg_proc p, aclexplode(coalesce(p.proacl, acldefault('f', p.proowner))) a
WHERE p.oid = f AND a.grantee = 0 AND a.privilege_type = 'EXECUTE') THEN
EXECUTE format('REVOKE EXECUTE ON FUNCTION %s FROM PUBLIC', f);
END IF;
SELECT oid INTO c FROM pg_roles WHERE rolname = 'db_client';
IF c IS NOT NULL AND EXISTS (SELECT 1 FROM pg_proc p, aclexplode(coalesce(p.proacl, acldefault('f', p.proowner))) a
WHERE p.oid = f AND a.grantee = c AND a.privilege_type = 'EXECUTE') THEN
EXECUTE format('REVOKE EXECUTE ON FUNCTION %s FROM db_client', f);
END IF;
END LOOP;
Expand Down
11 changes: 11 additions & 0 deletions internal/branch/import_as_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,14 @@ func TestEnsureDblinkPutsItWhereTheTriggersLook(t *testing.T) {
}
}
}

// Every start runs ensureDblinkSQL, and the Blackbox records each REVOKE, held
// or not: each is issued only while the grant it removes is there, so a start
// that changes nothing records nothing.
func TestEnsureDblinkRevokesOnlyWhatIsHeld(t *testing.T) {
revokes := strings.Count(ensureDblinkSQL, "EXECUTE format('REVOKE")
guarded := strings.Count(ensureDblinkSQL, "aclexplode(coalesce(p.proacl, acldefault('f', p.proowner)))")
if revokes != 2 || guarded != revokes {
t.Errorf("%d REVOKEs, %d of them guarded by a look at the grant", revokes, guarded)
}
}
Loading