@coffre/db 0.14.5 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/README.md +2 -1
  2. package/dist/change-records-_fKvahvs.js +55 -0
  3. package/dist/connect.d.ts +22 -10
  4. package/dist/connect.js +74 -12
  5. package/dist/database-CVteLhp1.d.ts +410 -0
  6. package/dist/{database-BadNeeN-.js → database-D55ENPX-.js} +46 -25
  7. package/dist/dialect.d.ts +4 -3
  8. package/dist/dialect.js +14 -5
  9. package/dist/grants.d.ts +4 -4
  10. package/dist/grants.js +4 -4
  11. package/dist/hyperdrive.d.ts +48 -23
  12. package/dist/hyperdrive.js +168 -91
  13. package/dist/index.d.ts +3 -3
  14. package/dist/index.js +1 -1
  15. package/dist/log-DaD7fyUq.d.ts +722 -0
  16. package/dist/log.d.ts +2 -622
  17. package/dist/log.js +178 -13
  18. package/dist/migrate.d.ts +43 -3
  19. package/dist/migrate.js +149 -29
  20. package/dist/migrations/postgres/0022_log_015.sql +672 -0
  21. package/dist/migrations/postgres/0023_change_path.sql +153 -0
  22. package/dist/migrations/postgres/0024_use_015.sql +556 -0
  23. package/dist/migrations/postgres/0025_app_change.sql +221 -0
  24. package/dist/migrations/postgres/0026_issue_lock.sql +106 -0
  25. package/dist/migrations/postgres/0027_retention.sql +118 -0
  26. package/dist/migrations/postgres/0028_use_stalls.sql +594 -0
  27. package/dist/migrations/postgres/0029_use_objects.sql +887 -0
  28. package/dist/migrations/postgres/0030_unarchive_restores.sql +259 -0
  29. package/dist/migrations/postgres/0031_app_read.sql +65 -0
  30. package/dist/migrations/postgres/0032_secrets_live_index.sql +4 -0
  31. package/dist/migrations/postgres/0033_use_generations.sql +582 -0
  32. package/dist/migrations/postgres/0034_vault_credentials.sql +153 -0
  33. package/dist/migrations/postgres/0035_first_days.sql +3 -0
  34. package/dist/migrations/postgres/0036_vault_issues.sql +91 -0
  35. package/dist/migrations/postgres/0037_vault_bindings.sql +271 -0
  36. package/dist/migrations/postgres/0040_binding_origin.sql +159 -0
  37. package/dist/migrations/postgres/0041_cutover.sql +132 -0
  38. package/dist/migrations/postgres/0042_sign_in_index.sql +7 -0
  39. package/dist/migrations/postgres/0043_issue_revoke_index.sql +4 -0
  40. package/dist/migrations/postgres/0044_use_by_name.sql +705 -0
  41. package/dist/migrations/postgres/0045_decide_first.sql +878 -0
  42. package/dist/migrations/postgres/0046_way_in_uses.sql +1149 -0
  43. package/dist/migrations/postgres/0047_cutover_finish.sql +268 -0
  44. package/dist/migrations/postgres/0048_use_batch.sql +59 -0
  45. package/dist/migrations/postgres/0049_reads.sql +171 -0
  46. package/dist/migrations/postgres/meta/0022_snapshot.json +3831 -0
  47. package/dist/migrations/postgres/meta/0023_snapshot.json +3831 -0
  48. package/dist/migrations/postgres/meta/0024_snapshot.json +3831 -0
  49. package/dist/migrations/postgres/meta/0025_snapshot.json +3831 -0
  50. package/dist/migrations/postgres/meta/0026_snapshot.json +3831 -0
  51. package/dist/migrations/postgres/meta/0027_snapshot.json +3853 -0
  52. package/dist/migrations/postgres/meta/0028_snapshot.json +3853 -0
  53. package/dist/migrations/postgres/meta/0029_snapshot.json +3859 -0
  54. package/dist/migrations/postgres/meta/0030_snapshot.json +3859 -0
  55. package/dist/migrations/postgres/meta/0031_snapshot.json +3859 -0
  56. package/dist/migrations/postgres/meta/0032_snapshot.json +3875 -0
  57. package/dist/migrations/postgres/meta/0033_snapshot.json +3875 -0
  58. package/dist/migrations/postgres/meta/0034_snapshot.json +3875 -0
  59. package/dist/migrations/postgres/meta/0035_snapshot.json +3875 -0
  60. package/dist/migrations/postgres/meta/0036_snapshot.json +3875 -0
  61. package/dist/migrations/postgres/meta/0037_snapshot.json +3875 -0
  62. package/dist/migrations/postgres/meta/0038_snapshot.json +3881 -0
  63. package/dist/migrations/postgres/meta/0039_snapshot.json +3881 -0
  64. package/dist/migrations/postgres/meta/0040_snapshot.json +3931 -0
  65. package/dist/migrations/postgres/meta/0041_snapshot.json +3981 -0
  66. package/dist/migrations/postgres/meta/0042_snapshot.json +3981 -0
  67. package/dist/migrations/postgres/meta/0043_snapshot.json +3981 -0
  68. package/dist/migrations/postgres/meta/0044_snapshot.json +3991 -0
  69. package/dist/migrations/postgres/meta/0045_snapshot.json +3997 -0
  70. package/dist/migrations/postgres/meta/0046_snapshot.json +3997 -0
  71. package/dist/migrations/postgres/meta/0047_snapshot.json +4234 -0
  72. package/dist/migrations/postgres/meta/_journal.json +182 -0
  73. package/dist/migrations/sqlite/0018_log_015.sql +84 -0
  74. package/dist/migrations/sqlite/0019_retention.sql +2 -0
  75. package/dist/migrations/sqlite/0020_ended_references.sql +1 -0
  76. package/dist/migrations/sqlite/0021_secrets_live_index.sql +1 -0
  77. package/dist/migrations/sqlite/0022_binding_origin.sql +1 -0
  78. package/dist/migrations/sqlite/0023_sign_in_index.sql +2 -0
  79. package/dist/migrations/sqlite/0024_issue_revoke_index.sql +2 -0
  80. package/dist/migrations/sqlite/0025_way_in_uses.sql +42 -0
  81. package/dist/migrations/sqlite/0026_cutover_finish.sql +11 -0
  82. package/dist/migrations/sqlite/0027_reads.sql +38 -0
  83. package/dist/migrations/sqlite/meta/0018_snapshot.json +3529 -0
  84. package/dist/migrations/sqlite/meta/0019_snapshot.json +3538 -0
  85. package/dist/migrations/sqlite/meta/0020_snapshot.json +3545 -0
  86. package/dist/migrations/sqlite/meta/0021_snapshot.json +3553 -0
  87. package/dist/migrations/sqlite/meta/0022_snapshot.json +3560 -0
  88. package/dist/migrations/sqlite/meta/0023_snapshot.json +3579 -0
  89. package/dist/migrations/sqlite/meta/0024_snapshot.json +3598 -0
  90. package/dist/migrations/sqlite/meta/0025_snapshot.json +3609 -0
  91. package/dist/migrations/sqlite/meta/0026_snapshot.json +3616 -0
  92. package/dist/migrations/sqlite/meta/0027_snapshot.json +3834 -0
  93. package/dist/migrations/sqlite/meta/_journal.json +70 -0
  94. package/dist/platform-Y3gICK6n.d.ts +58 -0
  95. package/dist/portable.d.ts +2 -2
  96. package/dist/postgres-BI3xTnyw.js +1718 -0
  97. package/dist/postgres-platform-node.d.ts +14 -0
  98. package/dist/postgres-platform-node.js +21 -0
  99. package/dist/postgres-platform-workerd.d.ts +61 -0
  100. package/dist/postgres-platform-workerd.js +163 -0
  101. package/dist/{schema-BfQEBqFv.d.ts → schema-DgoPqqsI.d.ts} +1326 -7
  102. package/dist/schema-sqlite.d.ts +1306 -6
  103. package/dist/schema-sqlite.js +154 -18
  104. package/dist/schema-version.d.ts +1 -1
  105. package/dist/schema-version.js +254 -2
  106. package/dist/schema.d.ts +2 -2
  107. package/dist/schema.js +226 -20
  108. package/package.json +13 -5
  109. package/src/active-subject.ts +9 -0
  110. package/src/change-records.ts +38 -0
  111. package/src/connect.ts +67 -13
  112. package/src/database.ts +67 -51
  113. package/src/dialect.ts +17 -8
  114. package/src/grants.ts +3 -3
  115. package/src/hyperdrive.ts +181 -104
  116. package/src/log.ts +196 -15
  117. package/src/migrate.ts +157 -30
  118. package/src/migrations/postgres/0022_log_015.sql +672 -0
  119. package/src/migrations/postgres/0023_change_path.sql +153 -0
  120. package/src/migrations/postgres/0024_use_015.sql +556 -0
  121. package/src/migrations/postgres/0025_app_change.sql +221 -0
  122. package/src/migrations/postgres/0026_issue_lock.sql +106 -0
  123. package/src/migrations/postgres/0027_retention.sql +118 -0
  124. package/src/migrations/postgres/0028_use_stalls.sql +594 -0
  125. package/src/migrations/postgres/0029_use_objects.sql +887 -0
  126. package/src/migrations/postgres/0030_unarchive_restores.sql +259 -0
  127. package/src/migrations/postgres/0031_app_read.sql +65 -0
  128. package/src/migrations/postgres/0032_secrets_live_index.sql +4 -0
  129. package/src/migrations/postgres/0033_use_generations.sql +582 -0
  130. package/src/migrations/postgres/0034_vault_credentials.sql +153 -0
  131. package/src/migrations/postgres/0035_first_days.sql +3 -0
  132. package/src/migrations/postgres/0036_vault_issues.sql +91 -0
  133. package/src/migrations/postgres/0037_vault_bindings.sql +271 -0
  134. package/src/migrations/postgres/0040_binding_origin.sql +159 -0
  135. package/src/migrations/postgres/0041_cutover.sql +132 -0
  136. package/src/migrations/postgres/0042_sign_in_index.sql +7 -0
  137. package/src/migrations/postgres/0043_issue_revoke_index.sql +4 -0
  138. package/src/migrations/postgres/0044_use_by_name.sql +705 -0
  139. package/src/migrations/postgres/0045_decide_first.sql +878 -0
  140. package/src/migrations/postgres/0046_way_in_uses.sql +1149 -0
  141. package/src/migrations/postgres/0047_cutover_finish.sql +268 -0
  142. package/src/migrations/postgres/0048_use_batch.sql +59 -0
  143. package/src/migrations/postgres/0049_reads.sql +171 -0
  144. package/src/migrations/postgres/meta/0022_snapshot.json +3831 -0
  145. package/src/migrations/postgres/meta/0023_snapshot.json +3831 -0
  146. package/src/migrations/postgres/meta/0024_snapshot.json +3831 -0
  147. package/src/migrations/postgres/meta/0025_snapshot.json +3831 -0
  148. package/src/migrations/postgres/meta/0026_snapshot.json +3831 -0
  149. package/src/migrations/postgres/meta/0027_snapshot.json +3853 -0
  150. package/src/migrations/postgres/meta/0028_snapshot.json +3853 -0
  151. package/src/migrations/postgres/meta/0029_snapshot.json +3859 -0
  152. package/src/migrations/postgres/meta/0030_snapshot.json +3859 -0
  153. package/src/migrations/postgres/meta/0031_snapshot.json +3859 -0
  154. package/src/migrations/postgres/meta/0032_snapshot.json +3875 -0
  155. package/src/migrations/postgres/meta/0033_snapshot.json +3875 -0
  156. package/src/migrations/postgres/meta/0034_snapshot.json +3875 -0
  157. package/src/migrations/postgres/meta/0035_snapshot.json +3875 -0
  158. package/src/migrations/postgres/meta/0036_snapshot.json +3875 -0
  159. package/src/migrations/postgres/meta/0037_snapshot.json +3875 -0
  160. package/src/migrations/postgres/meta/0038_snapshot.json +3881 -0
  161. package/src/migrations/postgres/meta/0039_snapshot.json +3881 -0
  162. package/src/migrations/postgres/meta/0040_snapshot.json +3931 -0
  163. package/src/migrations/postgres/meta/0041_snapshot.json +3981 -0
  164. package/src/migrations/postgres/meta/0042_snapshot.json +3981 -0
  165. package/src/migrations/postgres/meta/0043_snapshot.json +3981 -0
  166. package/src/migrations/postgres/meta/0044_snapshot.json +3991 -0
  167. package/src/migrations/postgres/meta/0045_snapshot.json +3997 -0
  168. package/src/migrations/postgres/meta/0046_snapshot.json +3997 -0
  169. package/src/migrations/postgres/meta/0047_snapshot.json +4234 -0
  170. package/src/migrations/postgres/meta/_journal.json +182 -0
  171. package/src/migrations/sqlite/0018_log_015.sql +84 -0
  172. package/src/migrations/sqlite/0019_retention.sql +2 -0
  173. package/src/migrations/sqlite/0020_ended_references.sql +1 -0
  174. package/src/migrations/sqlite/0021_secrets_live_index.sql +1 -0
  175. package/src/migrations/sqlite/0022_binding_origin.sql +1 -0
  176. package/src/migrations/sqlite/0023_sign_in_index.sql +2 -0
  177. package/src/migrations/sqlite/0024_issue_revoke_index.sql +2 -0
  178. package/src/migrations/sqlite/0025_way_in_uses.sql +42 -0
  179. package/src/migrations/sqlite/0026_cutover_finish.sql +11 -0
  180. package/src/migrations/sqlite/0027_reads.sql +38 -0
  181. package/src/migrations/sqlite/meta/0018_snapshot.json +3529 -0
  182. package/src/migrations/sqlite/meta/0019_snapshot.json +3538 -0
  183. package/src/migrations/sqlite/meta/0020_snapshot.json +3545 -0
  184. package/src/migrations/sqlite/meta/0021_snapshot.json +3553 -0
  185. package/src/migrations/sqlite/meta/0022_snapshot.json +3560 -0
  186. package/src/migrations/sqlite/meta/0023_snapshot.json +3579 -0
  187. package/src/migrations/sqlite/meta/0024_snapshot.json +3598 -0
  188. package/src/migrations/sqlite/meta/0025_snapshot.json +3609 -0
  189. package/src/migrations/sqlite/meta/0026_snapshot.json +3616 -0
  190. package/src/migrations/sqlite/meta/0027_snapshot.json +3834 -0
  191. package/src/migrations/sqlite/meta/_journal.json +70 -0
  192. package/src/postgres-js/README.md +183 -0
  193. package/src/postgres-js/UNLICENSE +24 -0
  194. package/src/postgres-js/bytes.ts +89 -0
  195. package/src/postgres-js/connection.ts +882 -0
  196. package/src/postgres-js/errors.ts +84 -0
  197. package/src/postgres-js/index.ts +516 -0
  198. package/src/postgres-js/platform.node.ts +29 -0
  199. package/src/postgres-js/platform.ts +60 -0
  200. package/src/postgres-js/platform.workerd.ts +225 -0
  201. package/src/postgres-js/query.ts +123 -0
  202. package/src/postgres-js/queue.ts +42 -0
  203. package/src/postgres-js/result.ts +46 -0
  204. package/src/postgres-js/types.ts +52 -0
  205. package/src/postgres.ts +375 -26
  206. package/src/schema-version.ts +2 -2
  207. package/src/schema.sqlite.ts +198 -7
  208. package/src/schema.ts +291 -6
  209. package/src/verify-runtime.ts +18 -29
  210. package/LICENSE +0 -21
  211. package/dist/database-wq1AVRAP.d.ts +0 -73
  212. package/dist/postgres-Bmar8y0q.js +0 -29
  213. package/dist/relations-BfR3JjT1.js +0 -38
  214. package/src/relations.ts +0 -40
@@ -0,0 +1,221 @@
1
+ -- 0.15's change path for the app's own rows (docs/design/0.15-plan.md, "As built: PR 4b"; the spec: formal/HotPathOne.tla's change actions, formal/Migration015.tla's AppObject): `app_change` logs the changes the app makes to the objects its rows hold (credentials, keys, environments) in the app's own transaction, beside the rows it writes. The vault sealed each change before the transaction (`prepare`); this function checks it under the fence and numbers it. It also fixes `change_input`'s environment branch for a deleted environment, whose object holds its slug before the tombstone (packages/vault/src/objects.ts). Nothing calls either before 0.15's switch: 0.14's code serves on it as before.
2
+ --
3
+ -- A change of the app's (hot-path-one.md, "A change"), called in its transaction after its rows and its 0.14 entries are written:
4
+ -- - Locks: 0.14's head, which the app's transaction took before any of its rows (`audited()`), so taken again here at no cost; then the fence exclusive (ChangeLocks). Head, row, fence: Migration015.tla's AppHeadFirst. No member's lock: taken after the head, it would wait for a 0.14 decision that holds the member and waits for the head (RowFirst's order is the vault's, which takes the member first).
5
+ -- - A retry finds its entry and answers its number; another change under its request id is refused.
6
+ -- - ChangeChecksSeal: the row's seal is its object's newest change's (one probe, as a use makes), else `stale`.
7
+ -- - ChangeCAS: the row's seal moves only from the one the vault read; else nothing is done (null), and the app reads again.
8
+ -- - The entry, numbered now (ChangeNumberUnderLock); the locks go at the commit (LockToCommit), and the app answers once it is sealed (R51, CommitBeforeAnswer).
9
+ -- The app writes its rows itself, as 0.14 does, and this function only their seals: what the seal covers is the vault's (prepare), and a row written otherwise than sealed is `tampered` to every use.
10
+ --
11
+ -- A row the vault never sealed, before the cutover (plan PR 9), is 0.14's: its call (no request id) logs nothing and only checks that its object has no change either, else `stale`, as `change_input` refuses one.
12
+ CREATE FUNCTION app_change(
13
+ p_kind text, p_id text, p_request text, p_at bigint, p_actor text, p_payload text, p_seal_from bytea, p_seal bytea, p_mac bytea
14
+ ) RETURNS bigint
15
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp AS $$
16
+ DECLARE
17
+ op jsonb := p_payload::jsonb;
18
+ change text := op ->> 'op';
19
+ target uuid := public.log_object(p_kind, p_id);
20
+ n bigint;
21
+ done integer;
22
+ held bytea;
23
+ BEGIN
24
+ PERFORM 1 FROM public.audit_chain_head WHERE only_row FOR UPDATE;
25
+ PERFORM pg_advisory_xact_lock(1668244070, 0);
26
+ held := CASE p_kind
27
+ WHEN 'credential' THEN (SELECT c.seal FROM public.credentials c WHERE c.id = p_id::uuid)
28
+ WHEN 'secret' THEN (SELECT x.seal FROM public.secrets x WHERE x.id = p_id::uuid)
29
+ WHEN 'environment' THEN (SELECT e.seal FROM public.environments e WHERE e.id = p_id::uuid)
30
+ END;
31
+ IF p_request IS NULL THEN
32
+ IF held IS NOT NULL OR EXISTS (SELECT 1 FROM public.log_changes c WHERE c.object = target) THEN
33
+ RAISE EXCEPTION 'the % object''s row is not its newest change''s: stale', p_kind USING ERRCODE = 'data_corrupted';
34
+ END IF;
35
+ RETURN 0;
36
+ END IF;
37
+ SELECT c.number INTO n FROM public.log_changes c WHERE c.request_id = p_request;
38
+ IF FOUND THEN
39
+ IF NOT EXISTS (
40
+ SELECT 1 FROM public.log_changes c
41
+ WHERE c.request_id = p_request AND c.object = target AND c.payload = p_payload AND c.seal IS NOT DISTINCT FROM p_seal
42
+ ) THEN
43
+ RAISE EXCEPTION 'request % names another change', p_request USING ERRCODE = 'unique_violation';
44
+ END IF;
45
+ RETURN n;
46
+ END IF;
47
+ IF NOT ((p_kind = 'credential' AND change IN ('credential.issue', 'credential.revoke', 'credential.renew', 'credential.scope'))
48
+ OR (p_kind = 'secret' AND change IN ('secret.write', 'secret.delete'))
49
+ OR (p_kind = 'environment' AND change IN ('environment.create', 'environment.rename', 'environment.archive', 'environment.delete'))) THEN
50
+ RAISE EXCEPTION 'the app makes no change % to a %', coalesce(change, 'without an op'), p_kind USING ERRCODE = 'invalid_parameter_value';
51
+ END IF;
52
+ IF op -> 'ref' ->> 'kind' IS DISTINCT FROM p_kind OR op -> 'ref' ->> 'id' IS DISTINCT FROM p_id THEN
53
+ RAISE EXCEPTION 'a change names the object it is to: %, %', p_kind, p_id USING ERRCODE = 'invalid_parameter_value';
54
+ END IF;
55
+ -- ChangeChecksSeal.
56
+ IF held IS DISTINCT FROM (SELECT c.seal FROM public.log_changes c WHERE c.object = target ORDER BY c.number DESC LIMIT 1) THEN
57
+ RAISE EXCEPTION 'the % object''s row is not its newest change''s: stale', p_kind USING ERRCODE = 'data_corrupted';
58
+ END IF;
59
+ -- ChangeCAS.
60
+ CASE p_kind
61
+ WHEN 'credential' THEN
62
+ UPDATE public.credentials SET seal = p_seal WHERE id = p_id::uuid AND seal IS NOT DISTINCT FROM p_seal_from;
63
+ WHEN 'secret' THEN
64
+ UPDATE public.secrets SET seal = p_seal WHERE id = p_id::uuid AND seal IS NOT DISTINCT FROM p_seal_from;
65
+ WHEN 'environment' THEN
66
+ UPDATE public.environments SET seal = p_seal WHERE id = p_id::uuid AND seal IS NOT DISTINCT FROM p_seal_from;
67
+ END CASE;
68
+ GET DIAGNOSTICS done = ROW_COUNT;
69
+ IF done = 0 THEN RETURN NULL; END IF;
70
+ INSERT INTO public.log_changes (number, at, request_id, object, actor, payload, seal, mac)
71
+ VALUES (nextval('public.log_number'), p_at, p_request, target, p_actor, p_payload, p_seal, p_mac)
72
+ RETURNING number INTO n;
73
+ RETURN n;
74
+ END
75
+ $$;
76
+ --> statement-breakpoint
77
+ REVOKE ALL ON FUNCTION app_change(text, text, text, bigint, text, text, bytea, bytea, bytea) FROM PUBLIC;
78
+ --> statement-breakpoint
79
+ GRANT EXECUTE ON FUNCTION app_change(text, text, text, bigint, text, text, bytea, bytea, bytea) TO coffre_app;
80
+ --> statement-breakpoint
81
+ -- `change_input` as 0023 made it, but for a deleted environment (W218's follow-up on #377): its object's slug is the row's before the tombstone, as every reader takes it, where 0023 required the value's to hold the tombstone, so that the cutover's genesis of a deleted environment, and any change sealing one, would have left a row its object's seal does not cover (`tampered`). A deleted row's slug is never rewritten.
82
+ CREATE OR REPLACE FUNCTION change_input(
83
+ p_kind text, p_id text, p_request text, p_at bigint, p_actor text, p_payload text, p_value text, p_seal_from bytea, p_seal bytea, p_mac bytea
84
+ ) RETURNS bigint
85
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp AS $$
86
+ DECLARE
87
+ op jsonb := p_payload::jsonb;
88
+ v jsonb := p_value::jsonb;
89
+ change text := op ->> 'op';
90
+ target uuid := public.log_object(p_kind, p_id);
91
+ n bigint;
92
+ done integer;
93
+ member text;
94
+ held bytea;
95
+ newest bytea;
96
+ config jsonb;
97
+ BEGIN
98
+ -- Its locks in the order 0.14's writers take theirs, so that none waits on the other in a circle until 0.16 (row,
99
+ -- head, fence: Migration015.tla's RowFirst, ChangeHeadFirst): the member's queue (MEMBER_QUEUE, 0x636f666d) and row
100
+ -- first, a principal's own or a credential's (0.14's vault locks them before the head, lockMembers; a change made
101
+ -- inside a 0.14 decision holds them already), then 0.14's head (its app takes it before its rows), then the fence
102
+ -- (0.14's appends take it after the head, audit_log_fence). The head held, no 0.14 write lands between this change
103
+ -- and its entry.
104
+ member := CASE p_kind
105
+ WHEN 'principal' THEN p_id
106
+ WHEN 'credential' THEN coalesce(
107
+ (SELECT c.principal FROM public.credentials c WHERE c.id = p_id::uuid),
108
+ (SELECT b.principal FROM public.service_bindings b WHERE b.id = p_id::uuid))
109
+ END;
110
+ IF member IS NOT NULL THEN
111
+ PERFORM pg_advisory_xact_lock(1668245101, hashtext(m.principal)) FROM public.vault_members m WHERE m.principal = member FOR NO KEY UPDATE;
112
+ END IF;
113
+ PERFORM 1 FROM public.audit_chain_head WHERE only_row FOR UPDATE;
114
+ -- ChangeLocks: exclusive, so that it waits for every use in flight, and every use after it waits for its commit.
115
+ PERFORM pg_advisory_xact_lock(1668244070, 0);
116
+ SELECT c.number INTO n FROM public.log_changes c WHERE c.request_id = p_request;
117
+ IF FOUND THEN
118
+ IF NOT EXISTS (
119
+ SELECT 1 FROM public.log_changes c
120
+ WHERE c.request_id = p_request AND c.object = target AND c.payload = p_payload AND c.seal IS NOT DISTINCT FROM p_seal
121
+ ) THEN
122
+ RAISE EXCEPTION 'request % names another change', p_request USING ERRCODE = 'unique_violation';
123
+ END IF;
124
+ RETURN n;
125
+ END IF;
126
+ IF NOT (change = 'genesis'
127
+ OR (p_kind = 'principal' AND change IN ('member.add', 'member.role', 'member.remove', 'member.restore', 'access.grant', 'access.revoke'))
128
+ OR (p_kind = 'environment' AND change IN ('environment.protect', 'environment.rename', 'environment.archive'))
129
+ OR (p_kind = 'roles' AND change = 'role.define')
130
+ OR (p_kind = 'policy' AND change = 'bulk.limit')) THEN
131
+ RAISE EXCEPTION 'this schema knows no change % to a %', coalesce(change, 'without an op'), p_kind USING ERRCODE = 'invalid_parameter_value';
132
+ END IF;
133
+ -- The payload names its object (`ref`), so that a replay knows what each change is to: the column holds only its
134
+ -- key. The cutover's genesis may leave it out, its value naming it.
135
+ IF (change <> 'genesis' OR op ? 'ref') AND (op -> 'ref' ->> 'kind' IS DISTINCT FROM p_kind OR op -> 'ref' ->> 'id' IS DISTINCT FROM p_id) THEN
136
+ RAISE EXCEPTION 'a change names the object it is to: %, %', p_kind, p_id USING ERRCODE = 'invalid_parameter_value';
137
+ END IF;
138
+ -- A value is the object as this function writes it: an environment's, which a genesis may leave out to seal the
139
+ -- row as it is; the roles' and the policy's, but at their genesis, whose payload holds it. The others' rows are
140
+ -- written by whoever keeps them, and carry none.
141
+ IF (CASE
142
+ WHEN p_kind = 'environment' THEN change <> 'genesis' AND v IS NULL
143
+ WHEN p_kind IN ('roles', 'policy') THEN (change = 'genesis') = (v IS NOT NULL)
144
+ ELSE v IS NOT NULL
145
+ END) THEN
146
+ RAISE EXCEPTION 'a % change % carries the object it leaves exactly when this function writes it', p_kind, change USING ERRCODE = 'invalid_parameter_value';
147
+ END IF;
148
+ -- ChangeChecksSeal: the row's seal is its object's newest change's, or the object has neither yet (one probe, as
149
+ -- a use makes). A row deleted outright, or a seal no change wrote, is stale too.
150
+ held := CASE p_kind
151
+ WHEN 'principal' THEN (SELECT m.seal FROM public.vault_members m WHERE m.principal = p_id)
152
+ WHEN 'credential' THEN coalesce(
153
+ (SELECT c.seal FROM public.credentials c WHERE c.id = p_id::uuid),
154
+ (SELECT b.seal FROM public.service_bindings b WHERE b.id = p_id::uuid))
155
+ WHEN 'environment' THEN (SELECT e.seal FROM public.environments e WHERE e.id = p_id::uuid)
156
+ WHEN 'secret' THEN (SELECT x.seal FROM public.secrets x WHERE x.id = p_id::uuid)
157
+ WHEN 'policy' THEN (SELECT g.seal FROM public.log_config g WHERE g.name = 'policy')
158
+ WHEN 'roles' THEN (SELECT g.seal FROM public.log_config g WHERE g.name = 'roles')
159
+ END;
160
+ newest := (SELECT c.seal FROM public.log_changes c WHERE c.object = target ORDER BY c.number DESC LIMIT 1);
161
+ IF held IS DISTINCT FROM newest THEN
162
+ RAISE EXCEPTION 'the % object''s row is not its newest change''s: stale', p_kind USING ERRCODE = 'data_corrupted';
163
+ END IF;
164
+ -- ChangeCAS: each write moves the row only from the seal the vault read.
165
+ CASE p_kind
166
+ WHEN 'principal' THEN
167
+ UPDATE public.vault_members SET seal = p_seal WHERE principal = p_id AND seal IS NOT DISTINCT FROM p_seal_from;
168
+ WHEN 'credential' THEN
169
+ -- A token's row, or a trust binding's: one kind of object, ids that never meet.
170
+ UPDATE public.credentials SET seal = p_seal WHERE id = p_id::uuid AND seal IS NOT DISTINCT FROM p_seal_from;
171
+ GET DIAGNOSTICS done = ROW_COUNT;
172
+ IF done = 0 THEN
173
+ UPDATE public.service_bindings SET seal = p_seal WHERE id = p_id::uuid AND seal IS NOT DISTINCT FROM p_seal_from;
174
+ END IF;
175
+ WHEN 'environment' THEN
176
+ IF v IS NULL THEN
177
+ UPDATE public.environments SET seal = p_seal WHERE id = p_id::uuid AND seal IS NOT DISTINCT FROM p_seal_from;
178
+ ELSE
179
+ -- A deleted environment's row keeps its tombstone, `dev~deleted-…` (@coffre/core/schemas's isTombstone),
180
+ -- and its object the slug before it (objects.ts, environmentObject): an environment deleted stays so,
181
+ -- its slug is never written again, and none is deleted here; an object's slug holds no tombstone.
182
+ UPDATE public.environments e
183
+ SET slug = CASE WHEN strpos(e.slug, '~') > 0 THEN e.slug ELSE v ->> 'slug' END,
184
+ protected = (v ->> 'protected')::boolean,
185
+ archived_at = CASE WHEN (v ->> 'archived')::boolean THEN coalesce(e.archived_at, to_timestamp(p_at / 1000.0)) END,
186
+ seal = p_seal
187
+ WHERE e.id = p_id::uuid AND e.seal IS NOT DISTINCT FROM p_seal_from
188
+ AND v ->> 'kind' = 'environment' AND v ->> 'id' = p_id
189
+ AND e.project_id = (v ->> 'projectId')::uuid
190
+ AND e.parent_id IS NOT DISTINCT FROM (v ->> 'parentId')::uuid
191
+ AND e.owner IS NOT DISTINCT FROM v ->> 'owner'
192
+ AND (strpos(e.slug, '~') > 0) = (v ->> 'deleted')::boolean
193
+ AND strpos(v ->> 'slug', '~') = 0
194
+ AND (NOT (v ->> 'deleted')::boolean OR split_part(e.slug, '~', 1) = v ->> 'slug');
195
+ END IF;
196
+ WHEN 'secret' THEN
197
+ UPDATE public.secrets SET seal = p_seal WHERE id = p_id::uuid AND seal IS NOT DISTINCT FROM p_seal_from;
198
+ WHEN 'policy', 'roles' THEN
199
+ config := CASE change WHEN 'genesis' THEN op -> 'value' ELSE v END;
200
+ IF p_id <> 'instance' OR config IS NULL OR jsonb_typeof(config) <> 'object' OR (change = 'genesis' AND p_seal_from IS NOT NULL) THEN
201
+ RAISE EXCEPTION 'the % object is one, made once from its value and changed from its seal', p_kind USING ERRCODE = 'invalid_parameter_value';
202
+ END IF;
203
+ -- Made by its first change (genesis, or the first role defined, the first cap set); moved after from its seal.
204
+ IF p_seal_from IS NULL THEN
205
+ INSERT INTO public.log_config (name, value, seal) VALUES (p_kind, config::text, p_seal) ON CONFLICT (name) DO NOTHING;
206
+ ELSE
207
+ UPDATE public.log_config SET value = config::text, seal = p_seal WHERE log_config.name = p_kind AND seal = p_seal_from;
208
+ END IF;
209
+ ELSE
210
+ RAISE EXCEPTION 'no object kind %', p_kind USING ERRCODE = 'invalid_parameter_value';
211
+ END CASE;
212
+ -- A row gone, a row whose seal is no longer the one read, an environment that is not what the value says it
213
+ -- stays, or an object made already: nothing was done.
214
+ GET DIAGNOSTICS done = ROW_COUNT;
215
+ IF done = 0 THEN RETURN NULL; END IF;
216
+ INSERT INTO public.log_changes (number, at, request_id, object, actor, payload, seal, mac)
217
+ VALUES (nextval('public.log_number'), p_at, p_request, target, p_actor, p_payload, p_seal, p_mac)
218
+ RETURNING number INTO n;
219
+ RETURN n;
220
+ END
221
+ $$;
@@ -0,0 +1,106 @@
1
+ -- Sign-ins never hold up a use (T123, R58; docs/design/0.15-plan.md, "Uses never wait on sign-ins"; the spec: formal/HotPathOne.tla's issues, HotPathOneIssue, and formal/Migration015.tla's, Migration015Issue). A sign-in issues a credential: one change of the log, `credential.issue`, which 0025's `app_change` made under the fence exclusive, as every change, while 0022's trigger fenced 0.14's `token.exchange` too. Each sign-in then held every use up for its last round trips, and a burst of them, serialized on 0.14's head, held them up over and over (measured: a fifth of the uses waited, 1,000 a second beside 10 sign-ins at once). Nobody can name a credential before its issue commits (its token is handed out after), and an issue changes no input any other use reads: no use can overlap it, so it needs no fence. It needs only that the checkpointer's barrier wait for it, every number handed out before the barrier being final when it closes. So an issue takes a lock of its own, the issue lock (1668244070, 1), shared, and its number under it; the barrier takes that lock exclusive, then the fence: it waits for the issues in flight without holding the fence, so no use queues behind it meanwhile (Postgres queues a shared request behind a waiting exclusive one). The order, everywhere: a member's row, 0.14's head, the issue lock, the fence. Whatever takes the fence in a transaction that may also issue takes the issue lock shared first (`app_change`, 0.14's trigger): a sign-in that revokes its account's earlier sessions fences first, then issues, and would otherwise hold the fence while it waits for the issue lock, which the barrier holds while it waits for the fence (TLC finds the cycle without). The vault's `change_input` never issues, and takes no issue lock. 0.14's code calls none of the functions; its `token.exchange`, which writes the new credential, the spent token and its binding's last use and no decision input, no longer takes the fence.
2
+ --
3
+ -- `app_change`, as 0025 made it, but for its locks: the issue lock shared, then, but for an issue of a credential its transaction wrote, the fence. The app issues only a row it inserts (`prepare` makes an issue only of no row before); an issue of a row its transaction did not write (one committed before, which a use may name; or a savepoint's) takes the fence too, as before.
4
+ CREATE OR REPLACE FUNCTION app_change(
5
+ p_kind text, p_id text, p_request text, p_at bigint, p_actor text, p_payload text, p_seal_from bytea, p_seal bytea, p_mac bytea
6
+ ) RETURNS bigint
7
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp AS $$
8
+ DECLARE
9
+ op jsonb := p_payload::jsonb;
10
+ change text := op ->> 'op';
11
+ target uuid := public.log_object(p_kind, p_id);
12
+ n bigint;
13
+ done integer;
14
+ held bytea;
15
+ -- An issue of a credential this very transaction wrote (its row's xmin is ours: not a savepoint's, nor a row another committed).
16
+ -- Never null, whatever is missing (a 0.14 row's call carries no payload, so no op): else the fence.
17
+ fresh boolean := coalesce(change = 'credential.issue' AND p_kind = 'credential'
18
+ AND EXISTS (SELECT 1 FROM public.credentials c WHERE c.id = p_id::uuid AND c.xmin = pg_current_xact_id()::xid), false);
19
+ BEGIN
20
+ PERFORM 1 FROM public.audit_chain_head WHERE only_row FOR UPDATE;
21
+ -- The issue lock shared (IssueOwnLock); and, but for a fresh issue, the fence exclusive after it (IssueBeforeFence).
22
+ PERFORM pg_advisory_xact_lock_shared(1668244070, 1);
23
+ IF NOT fresh THEN PERFORM pg_advisory_xact_lock(1668244070, 0); END IF;
24
+ held := CASE p_kind
25
+ WHEN 'credential' THEN (SELECT c.seal FROM public.credentials c WHERE c.id = p_id::uuid)
26
+ WHEN 'secret' THEN (SELECT x.seal FROM public.secrets x WHERE x.id = p_id::uuid)
27
+ WHEN 'environment' THEN (SELECT e.seal FROM public.environments e WHERE e.id = p_id::uuid)
28
+ END;
29
+ IF p_request IS NULL THEN
30
+ IF held IS NOT NULL OR EXISTS (SELECT 1 FROM public.log_changes c WHERE c.object = target) THEN
31
+ RAISE EXCEPTION 'the % object''s row is not its newest change''s: stale', p_kind USING ERRCODE = 'data_corrupted';
32
+ END IF;
33
+ RETURN 0;
34
+ END IF;
35
+ SELECT c.number INTO n FROM public.log_changes c WHERE c.request_id = p_request;
36
+ IF FOUND THEN
37
+ IF NOT EXISTS (
38
+ SELECT 1 FROM public.log_changes c
39
+ WHERE c.request_id = p_request AND c.object = target AND c.payload = p_payload AND c.seal IS NOT DISTINCT FROM p_seal
40
+ ) THEN
41
+ RAISE EXCEPTION 'request % names another change', p_request USING ERRCODE = 'unique_violation';
42
+ END IF;
43
+ RETURN n;
44
+ END IF;
45
+ IF NOT ((p_kind = 'credential' AND change IN ('credential.issue', 'credential.revoke', 'credential.renew', 'credential.scope'))
46
+ OR (p_kind = 'secret' AND change IN ('secret.write', 'secret.delete'))
47
+ OR (p_kind = 'environment' AND change IN ('environment.create', 'environment.rename', 'environment.archive', 'environment.delete'))) THEN
48
+ RAISE EXCEPTION 'the app makes no change % to a %', coalesce(change, 'without an op'), p_kind USING ERRCODE = 'invalid_parameter_value';
49
+ END IF;
50
+ IF op -> 'ref' ->> 'kind' IS DISTINCT FROM p_kind OR op -> 'ref' ->> 'id' IS DISTINCT FROM p_id THEN
51
+ RAISE EXCEPTION 'a change names the object it is to: %, %', p_kind, p_id USING ERRCODE = 'invalid_parameter_value';
52
+ END IF;
53
+ -- ChangeChecksSeal.
54
+ IF held IS DISTINCT FROM (SELECT c.seal FROM public.log_changes c WHERE c.object = target ORDER BY c.number DESC LIMIT 1) THEN
55
+ RAISE EXCEPTION 'the % object''s row is not its newest change''s: stale', p_kind USING ERRCODE = 'data_corrupted';
56
+ END IF;
57
+ -- ChangeCAS.
58
+ CASE p_kind
59
+ WHEN 'credential' THEN
60
+ UPDATE public.credentials SET seal = p_seal WHERE id = p_id::uuid AND seal IS NOT DISTINCT FROM p_seal_from;
61
+ WHEN 'secret' THEN
62
+ UPDATE public.secrets SET seal = p_seal WHERE id = p_id::uuid AND seal IS NOT DISTINCT FROM p_seal_from;
63
+ WHEN 'environment' THEN
64
+ UPDATE public.environments SET seal = p_seal WHERE id = p_id::uuid AND seal IS NOT DISTINCT FROM p_seal_from;
65
+ END CASE;
66
+ GET DIAGNOSTICS done = ROW_COUNT;
67
+ IF done = 0 THEN RETURN NULL; END IF;
68
+ INSERT INTO public.log_changes (number, at, request_id, object, actor, payload, seal, mac)
69
+ VALUES (nextval('public.log_number'), p_at, p_request, target, p_actor, p_payload, p_seal, p_mac)
70
+ RETURNING number INTO n;
71
+ RETURN n;
72
+ END
73
+ $$;
74
+ --> statement-breakpoint
75
+ -- 0.14's trigger (0022, OldWritesFence): the issue lock shared, then the fence, as `app_change` takes them; and `token.exchange`, a sign-in, among the entries it lets by.
76
+ CREATE OR REPLACE FUNCTION log_fence_old_write() RETURNS trigger
77
+ LANGUAGE plpgsql AS $$
78
+ BEGIN
79
+ PERFORM pg_catalog.pg_advisory_xact_lock_shared(1668244070, 1);
80
+ PERFORM pg_catalog.pg_advisory_xact_lock(1668244070, 0);
81
+ RETURN NULL;
82
+ END
83
+ $$;
84
+ --> statement-breakpoint
85
+ CREATE OR REPLACE TRIGGER audit_log_fence AFTER INSERT ON audit_log FOR EACH ROW
86
+ WHEN (NEW.decision = 'allow' AND NEW.action NOT IN (
87
+ 'credential.use', 'token.use', 'secret.read', 'secret.use', 'secret.list', 'secret.history', 'secret.version', 'missing.list', 'personal.list',
88
+ 'roles.list', 'roles.preview', 'mcp.read', 'mcp.view', 'audit.read',
89
+ 'sign_in', 'token.exchange', 'device.approve', 'device.deny',
90
+ 'audit.heartbeat', 'audit.checkpoint', 'audit.finding', 'audit.finding_cleared', 'key.check', 'key.intent', 'workload.alert', 'vault.tampered'
91
+ ))
92
+ EXECUTE FUNCTION log_fence_old_write();
93
+ --> statement-breakpoint
94
+ -- The barrier (0022): the issue lock exclusive, which waits for every issue in flight and holds no use up, then the fence.
95
+ CREATE OR REPLACE FUNCTION close_one() RETURNS TABLE (upto bigint, at_floor bigint, floor_found bigint)
96
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp AS $$
97
+ DECLARE
98
+ found_floor bigint;
99
+ BEGIN
100
+ PERFORM pg_advisory_xact_lock(1668244070, 1);
101
+ PERFORM pg_advisory_xact_lock(1668244070, 0);
102
+ SELECT f.at_floor INTO found_floor FROM public.log_fence f WHERE f.only_row;
103
+ UPDATE public.log_fence f SET at_floor = greatest(f.at_floor, (extract(epoch FROM clock_timestamp()) * 1000)::bigint - public.deadline_max()) WHERE f.only_row;
104
+ RETURN QUERY SELECT pg_sequence_last_value('public.log_number'), f.at_floor, found_floor FROM public.log_fence f WHERE f.only_row;
105
+ END
106
+ $$;
@@ -0,0 +1,118 @@
1
+ -- 0.15's retention (docs/design/0.15-plan.md, PR 7; the spec: formal/Seal.tla's retention, SealRetention.cfg): the checkpointer drops a day of uses once a sealed retention change cuts the uses tree above it, and the uses tree's tiles below the cut, keeping the tile that holds it at each level. Changes are kept for good. It only adds functions, and nothing calls them until 0.15's log is switched on (plan PR 10): 0.14's code serves on it as before.
2
+ --
3
+ -- Each function checks, from the database alone, what it can of the vault's reasons, so that the vault's login, which owns nothing, drops only what a retention change it can see has cut. The signer's checks (the change sealed in its pair, the pair fresh past the cut) are the vault's: the database cannot verify a signature.
4
+ --
5
+ -- The retention change (Seal.tla's RetAppend): an entry of the changes tree, its object log_object('log', 'retention'), naming the cut, the pair at or above the day's last use, by its upto, time and uses tree: {"op":"retention","day":"YYYY-MM-DD","cut":{"upto":"<n>","at":<ms>,"uses":<size>,"root":"<hex>"}}. Numbered under the fence shared, as a use is: it changes no decision input, so no use waits on it. A request ID names one: the same one again answers the number it has.
6
+ CREATE FUNCTION log_retention(p_request text, p_at bigint, p_payload text, p_mac bytea) RETURNS bigint
7
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp AS $$
8
+ DECLARE
9
+ op jsonb := p_payload::jsonb;
10
+ n bigint;
11
+ BEGIN
12
+ IF op ->> 'op' IS DISTINCT FROM 'retention' OR (op ->> 'day') IS NULL OR (op #>> '{cut,upto}') IS NULL OR (op #>> '{cut,uses}') IS NULL
13
+ OR (op #>> '{cut,at}') IS NULL OR (op #>> '{cut,root}') IS NULL THEN
14
+ RAISE EXCEPTION 'not a retention change: %', p_payload USING ERRCODE = 'invalid_parameter_value';
15
+ END IF;
16
+ -- Its cut is the first checkpoint at or past the end of its day by its floor of at (every use of the day took its number at or below it), its time and uses head the change's.
17
+ IF (op #>> '{cut,root}') !~ '^[0-9a-f]{64}$' OR NOT EXISTS (
18
+ SELECT 1 FROM (
19
+ SELECT k.upto, k.at, k.uses_size, k.uses_root FROM public.log_checkpoints k
20
+ WHERE k.at_floor >= ((op ->> 'day')::date + 1 - date '1970-01-01')::bigint * 86400000 ORDER BY k.at_floor, k.upto LIMIT 1
21
+ ) k
22
+ WHERE k.upto = (op #>> '{cut,upto}')::bigint AND k.at = (op #>> '{cut,at}')::bigint
23
+ AND k.uses_size = (op #>> '{cut,uses}')::bigint AND k.uses_root = decode(op #>> '{cut,root}', 'hex')
24
+ ) THEN
25
+ RAISE EXCEPTION 'the cut of % is not the first checkpoint past its end', op ->> 'day' USING ERRCODE = 'invalid_parameter_value';
26
+ END IF;
27
+ PERFORM pg_advisory_xact_lock_shared(1668244070, 0);
28
+ SELECT c.number INTO n FROM public.log_changes c WHERE c.request_id = p_request;
29
+ IF n IS NOT NULL THEN RETURN n; END IF;
30
+ INSERT INTO public.log_changes (number, at, request_id, object, actor, payload, seal, mac)
31
+ VALUES (nextval('public.log_number'), p_at, p_request, public.log_object('log', 'retention'), 'system:retention', p_payload, NULL, p_mac)
32
+ RETURNING number INTO n;
33
+ RETURN n;
34
+ END
35
+ $$;
36
+ --> statement-breakpoint
37
+ -- The newest cut the database holds as sealed: the newest retention change at or below the newest checkpoint, its uses tree's size and upto. The signer holds it to its own pair before it asks for anything below.
38
+ CREATE FUNCTION log_cut() RETURNS TABLE (upto bigint, uses bigint)
39
+ LANGUAGE sql STABLE SECURITY DEFINER SET search_path = pg_catalog, pg_temp AS $$
40
+ SELECT (c.payload::jsonb #>> '{cut,upto}')::bigint, (c.payload::jsonb #>> '{cut,uses}')::bigint
41
+ FROM public.log_changes c
42
+ WHERE c.object = public.log_object('log', 'retention') AND c.number <= (SELECT max(k.upto) FROM public.log_checkpoints k)
43
+ ORDER BY c.number DESC LIMIT 1
44
+ $$;
45
+ --> statement-breakpoint
46
+ -- A day of uses dropped, a step a call, each its own short transaction: 'detached', then 'dropped', then 'gone'. Only a day before yesterday, and only below the newest cut: the first checkpoint whose floor of at is past the day's end, which every use of the day took its number at or below (the floor holds a use's at to it), must be at or below log_cut()'s.
47
+ --
48
+ -- DETACH takes log_uses ACCESS EXCLUSIVE for its own few milliseconds: uses queue behind it meanwhile, and lock_timeout bounds how long it waits for a reader before it gives up and tries again later. (DETACH … CONCURRENTLY cannot run in a function, and the vault's login owns no table; measured, it buys nothing here: docs/design/0.15-plan.md, "As built: PR 7".) The DROP of the detached table locks nothing a use reads; the vault asks for it a run later, never right after the DETACH, whose invalidation every backend's plan of log_uses is still replanning from.
49
+ CREATE FUNCTION drop_day(p_day date) RETURNS text
50
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET lock_timeout = '100ms' AS $$
51
+ DECLARE
52
+ name text := 'log_uses_' || to_char(p_day, 'YYYYMMDD');
53
+ hi bigint := (p_day + 1 - date '1970-01-01')::bigint * 86400000;
54
+ relation regclass := to_regclass('public.' || quote_ident(name));
55
+ attached boolean;
56
+ BEGIN
57
+ IF p_day >= (clock_timestamp() AT TIME ZONE 'UTC')::date - 1 THEN RAISE EXCEPTION 'the uses of % are not past', p_day USING ERRCODE = 'invalid_parameter_value'; END IF;
58
+ IF relation IS NULL THEN RETURN 'gone'; END IF;
59
+ IF NOT coalesce((SELECT k.upto FROM public.log_checkpoints k WHERE k.at_floor >= hi ORDER BY k.at_floor, k.upto LIMIT 1) <= (SELECT c.upto FROM public.log_cut() c), false) THEN
60
+ RAISE EXCEPTION 'no sealed retention change cuts the uses of %', p_day USING ERRCODE = 'invalid_parameter_value';
61
+ END IF;
62
+ attached := EXISTS (SELECT 1 FROM pg_inherits i WHERE i.inhrelid = relation AND i.inhparent = 'public.log_uses'::regclass);
63
+ IF attached THEN
64
+ -- The day as make_day made it, by its bound: a table of that name holding anything else is not dropped.
65
+ IF pg_get_expr((SELECT c.relpartbound FROM pg_class c WHERE c.oid = relation), relation) IS DISTINCT FROM format('FOR VALUES FROM (%L) TO (%L)', hi - 86400000, hi) THEN
66
+ RAISE EXCEPTION '% is not the uses of %', name, p_day;
67
+ END IF;
68
+ EXECUTE format('ALTER TABLE public.log_uses DETACH PARTITION public.%I', name);
69
+ RETURN 'detached';
70
+ END IF;
71
+ -- Detached: only one this function detached is dropped, a table with log_uses's columns and no parent (make_day's, by name).
72
+ IF EXISTS (SELECT 1 FROM pg_inherits i WHERE i.inhrelid = relation) OR (SELECT c.relkind FROM pg_class c WHERE c.oid = relation) <> 'r' THEN
73
+ RAISE EXCEPTION '% is not a detached day of uses', name;
74
+ END IF;
75
+ EXECUTE format('DROP TABLE public.%I', name);
76
+ RETURN 'dropped';
77
+ END
78
+ $$;
79
+ --> statement-breakpoint
80
+ -- The uses tree's tiles below a cut (W212): every tile whose leaves all lie below p_below, the cut's uses size, at most log_cut()'s, at every level; the tile that holds the cut, at each level, stays, and so does every proof of a leaf at or above it and every consistency proof from a size at or above it. The leaf tiles go a range at a time (make_tiles's partitions, those wholly below), detached then dropped, a step a call, the tiles above in one DELETE. Unlike a day's, a range is dropped right after its DETACH: no use's plan reads log_tiles, only the checkpointer's own statements and proofs, so no replan stalls a use. Answers what it did: 'detached <range>', 'dropped <range>', or 'done <rows deleted>'.
81
+ CREATE FUNCTION drop_tiles(p_below bigint) RETURNS text
82
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET lock_timeout = '1s' AS $$
83
+ DECLARE
84
+ part record;
85
+ deleted integer;
86
+ BEGIN
87
+ IF p_below IS NULL OR p_below < 0 OR p_below > coalesce((SELECT c.uses FROM public.log_cut() c), 0) THEN
88
+ RAISE EXCEPTION 'no sealed retention change cuts the uses tree at %', p_below USING ERRCODE = 'invalid_parameter_value';
89
+ END IF;
90
+ -- A detached range first (a call that stopped between the two), then the next attached one wholly below.
91
+ FOR part IN
92
+ SELECT c.relname, (regexp_match(pg_get_expr(c.relpartbound, c.oid), '^FOR VALUES FROM \(''?(\d+)''?\) TO \(''?(\d+)''?\)$'))[2]::bigint AS hi, i.inhparent IS NOT NULL AS attached
93
+ FROM pg_class c LEFT JOIN pg_inherits i ON i.inhrelid = c.oid
94
+ WHERE c.relnamespace = 'public'::regnamespace AND c.relname ~ '^log_tiles_uses_\d+$' AND c.relkind = 'r'
95
+ ORDER BY i.inhparent IS NOT NULL, c.relname
96
+ LOOP
97
+ IF NOT part.attached THEN
98
+ -- Detached by this function: no other way leaves one of make_tiles's names without its parent.
99
+ EXECUTE format('DROP TABLE public.%I', part.relname);
100
+ RETURN 'dropped ' || part.relname;
101
+ END IF;
102
+ IF part.hi IS NOT NULL AND part.hi <= p_below THEN
103
+ EXECUTE format('ALTER TABLE public.log_tiles_uses_leaves DETACH PARTITION public.%I', part.relname);
104
+ RETURN 'detached ' || part.relname;
105
+ END IF;
106
+ END LOOP;
107
+ DELETE FROM public.log_tiles_uses_upper t WHERE t.leaf + (256::numeric ^ (t.level + 1)) <= p_below;
108
+ GET DIAGNOSTICS deleted = ROW_COUNT;
109
+ RETURN 'done ' || deleted;
110
+ END
111
+ $$;
112
+ --> statement-breakpoint
113
+ -- A day's cut, the first checkpoint whose floor is past its end, by an index: floors only rise with upto (the barrier raises the floor, never lowers it, and the signer refuses a pair whose floor runs back), so the lowest floor past the end, then the lowest upto, is that checkpoint, however many there are.
114
+ CREATE INDEX log_checkpoints_floor_idx ON log_checkpoints USING btree (at_floor, upto);
115
+ --> statement-breakpoint
116
+ REVOKE ALL ON FUNCTION log_retention(text, bigint, text, bytea), log_cut(), drop_day(date), drop_tiles(bigint) FROM PUBLIC;
117
+ --> statement-breakpoint
118
+ GRANT EXECUTE ON FUNCTION log_retention(text, bigint, text, bytea), log_cut(), drop_day(date), drop_tiles(bigint) TO coffre_vault;