1
+ -- Drop old prefix-related triggers that conflict with new GC system
2
+ DROP TRIGGER IF EXISTS prefixes_delete_hierarchy ON storage .prefixes ;
3
+ DROP TRIGGER IF EXISTS objects_delete_delete_prefix ON storage .objects ;
4
+ DROP TRIGGER IF EXISTS objects_update_create_prefix ON storage .objects ;
5
+
6
+ -- Helper: Acquire statement-scoped advisory locks for the top-level path
7
+ -- for each \[bucket_id, name] pair to serialize operations per "bucket/top_level_prefix".
8
+ CREATE OR REPLACE FUNCTION storage .lock_top_prefixes(bucket_ids text [], names text [])
9
+ RETURNS void
10
+ LANGUAGE plpgsql
11
+ SECURITY DEFINER
12
+ AS $$
13
+ DECLARE
14
+ v_bucket text ;
15
+ v_top text ;
16
+ BEGIN
17
+ FOR v_bucket, v_top IN
18
+ SELECT DISTINCT t .bucket_id ,
19
+ split_part(t .name , ' /' , 1 ) AS top
20
+ FROM unnest(bucket_ids, names) AS t(bucket_id, name)
21
+ WHERE t .name <> ' '
22
+ ORDER BY 1 , 2
23
+ LOOP
24
+ PERFORM pg_advisory_xact_lock(hashtextextended(v_bucket || ' /' || v_top, 0 ));
25
+ END LOOP;
26
+ END;
27
+ $$;
28
+
29
+ -- Helper: Given arrays of bucket_ids and names, compute all ancestor
30
+ -- prefixes and delete those that are leaves (no children objects or prefixes).
31
+ -- Repeats bottom-up until no more rows are removed.
32
+ CREATE OR REPLACE FUNCTION storage .delete_leaf_prefixes(bucket_ids text [], names text [])
33
+ RETURNS void
34
+ LANGUAGE plpgsql
35
+ SECURITY DEFINER
36
+ AS $$
37
+ DECLARE
38
+ v_rows_deleted integer ;
39
+ BEGIN
40
+ LOOP
41
+ WITH candidates AS (
42
+ SELECT DISTINCT t .bucket_id ,
43
+ unnest(storage .get_prefixes (t .name )) AS name
44
+ FROM unnest(bucket_ids, names) AS t(bucket_id, name)
45
+ ),
46
+ uniq AS (
47
+ SELECT bucket_id,
48
+ name,
49
+ storage .get_level (name) AS level
50
+ FROM candidates
51
+ WHERE name <> ' '
52
+ GROUP BY bucket_id, name
53
+ ),
54
+ leaf AS (
55
+ SELECT p .bucket_id , p .name , p .level
56
+ FROM storage .prefixes AS p
57
+ JOIN uniq AS u
58
+ ON u .bucket_id = p .bucket_id
59
+ AND u .name = p .name
60
+ AND u .level = p .level
61
+ WHERE NOT EXISTS (
62
+ SELECT 1
63
+ FROM storage .objects AS o
64
+ WHERE o .bucket_id = p .bucket_id
65
+ AND storage .get_level (o .name ) = p .level + 1
66
+ AND o .name COLLATE " C" LIKE p .name || ' /%'
67
+ )
68
+ AND NOT EXISTS (
69
+ SELECT 1
70
+ FROM storage .prefixes AS c
71
+ WHERE c .bucket_id = p .bucket_id
72
+ AND c .level = p .level + 1
73
+ AND c .name COLLATE " C" LIKE p .name || ' /%'
74
+ )
75
+ )
76
+ DELETE FROM storage .prefixes AS p
77
+ USING leaf AS l
78
+ WHERE p .bucket_id = l .bucket_id
79
+ AND p .name = l .name
80
+ AND p .level = l .level ;
81
+
82
+ GET DIAGNOSTICS v_rows_deleted = ROW_COUNT;
83
+ EXIT WHEN v_rows_deleted = 0 ;
84
+ END LOOP;
85
+ END;
86
+ $$;
87
+
88
+ -- After DELETE on storage.objects
89
+ -- - Guards with `gc.prefixes`
90
+ -- - Locks top-level prefixes for touched objects
91
+ -- - Deletes leaf prefixes derived from deleted object names and their ancestors
92
+ CREATE OR REPLACE FUNCTION storage .objects_delete_cleanup()
93
+ RETURNS trigger
94
+ LANGUAGE plpgsql
95
+ SECURITY DEFINER
96
+ AS $$
97
+ DECLARE
98
+ v_bucket_ids text [];
99
+ v_names text [];
100
+ BEGIN
101
+ IF current_setting(' storage.gc.prefixes' , true) = ' 1' THEN
102
+ RETURN NULL ;
103
+ END IF;
104
+
105
+ PERFORM set_config(' storage.gc.prefixes' , ' 1' , true);
106
+
107
+ SELECT COALESCE(array_agg(d .bucket_id ), ' {}' ),
108
+ COALESCE(array_agg(d .name ), ' {}' )
109
+ INTO v_bucket_ids, v_names
110
+ FROM deleted AS d
111
+ WHERE d .name <> ' ' ;
112
+
113
+ PERFORM storage .lock_top_prefixes (v_bucket_ids, v_names);
114
+ PERFORM storage .delete_leaf_prefixes (v_bucket_ids, v_names);
115
+
116
+ RETURN NULL ;
117
+ END;
118
+ $$;
119
+
120
+ -- After UPDATE on storage.objects
121
+ -- - Only OLD names matter for cleanup; NEW prefixes are created elsewhere
122
+ -- - Guards with `gc.prefixes`, locks, then prunes leaves derived from OLD names
123
+ CREATE OR REPLACE FUNCTION storage .objects_update_cleanup()
124
+ RETURNS trigger
125
+ LANGUAGE plpgsql
126
+ SECURITY DEFINER
127
+ AS $$
128
+ DECLARE
129
+ -- NEW - OLD (destinations to create prefixes for)
130
+ v_add_bucket_ids text [];
131
+ v_add_names text [];
132
+
133
+ -- OLD - NEW (sources to prune)
134
+ v_src_bucket_ids text [];
135
+ v_src_names text [];
136
+ BEGIN
137
+ IF TG_OP <> ' UPDATE' THEN
138
+ RETURN NULL ;
139
+ END IF;
140
+
141
+ -- 1) Compute NEW−OLD (added paths) and OLD−NEW (moved-away paths)
142
+ WITH added AS (
143
+ SELECT n .bucket_id , n .name
144
+ FROM new_rows n
145
+ WHERE n .name <> ' ' AND position(' /' in n .name ) > 0
146
+ EXCEPT
147
+ SELECT o .bucket_id , o .name FROM old_rows o WHERE o .name <> ' '
148
+ ),
149
+ moved AS (
150
+ SELECT o .bucket_id , o .name
151
+ FROM old_rows o
152
+ WHERE o .name <> ' '
153
+ EXCEPT
154
+ SELECT n .bucket_id , n .name FROM new_rows n WHERE n .name <> ' '
155
+ )
156
+ SELECT
157
+ -- arrays for ADDED (dest) in stable order
158
+ COALESCE( (SELECT array_agg(a .bucket_id ORDER BY a .bucket_id , a .name ) FROM added a), ' {}' ),
159
+ COALESCE( (SELECT array_agg(a .name ORDER BY a .bucket_id , a .name ) FROM added a), ' {}' ),
160
+ -- arrays for MOVED (src) in stable order
161
+ COALESCE( (SELECT array_agg(m .bucket_id ORDER BY m .bucket_id , m .name ) FROM moved m), ' {}' ),
162
+ COALESCE( (SELECT array_agg(m .name ORDER BY m .bucket_id , m .name ) FROM moved m), ' {}' )
163
+ INTO v_add_bucket_ids, v_add_names, v_src_bucket_ids, v_src_names;
164
+
165
+ -- Nothing to do?
166
+ IF (array_length(v_add_bucket_ids, 1 ) IS NULL ) AND (array_length(v_src_bucket_ids, 1 ) IS NULL ) THEN
167
+ RETURN NULL ;
168
+ END IF;
169
+
170
+ -- 2) Take per-(bucket, top) locks: ALL prefixes in consistent global order to prevent deadlocks
171
+ DECLARE
172
+ v_all_bucket_ids text [];
173
+ v_all_names text [];
174
+ BEGIN
175
+ -- Combine source and destination arrays for consistent lock ordering
176
+ v_all_bucket_ids := COALESCE(v_src_bucket_ids, ' {}' ) || COALESCE(v_add_bucket_ids, ' {}' );
177
+ v_all_names := COALESCE(v_src_names, ' {}' ) || COALESCE(v_add_names, ' {}' );
178
+
179
+ -- Single lock call ensures consistent global ordering across all transactions
180
+ IF array_length(v_all_bucket_ids, 1 ) IS NOT NULL THEN
181
+ PERFORM storage .lock_top_prefixes (v_all_bucket_ids, v_all_names);
182
+ END IF;
183
+ END;
184
+
185
+ -- 3) Create destination prefixes (NEW−OLD) BEFORE pruning sources
186
+ IF array_length(v_add_bucket_ids, 1 ) IS NOT NULL THEN
187
+ WITH candidates AS (
188
+ SELECT DISTINCT t .bucket_id , unnest(storage .get_prefixes (t .name )) AS name
189
+ FROM unnest(v_add_bucket_ids, v_add_names) AS t(bucket_id, name)
190
+ WHERE name <> ' '
191
+ )
192
+ INSERT INTO storage .prefixes (bucket_id, name)
193
+ SELECT c .bucket_id , c .name
194
+ FROM candidates c
195
+ ON CONFLICT DO NOTHING;
196
+ END IF;
197
+
198
+ -- 4) Prune source prefixes bottom-up for OLD−NEW
199
+ IF array_length(v_src_bucket_ids, 1 ) IS NOT NULL THEN
200
+ -- re-entrancy guard so DELETE on prefixes won't recurse
201
+ IF current_setting(' storage.gc.prefixes' , true) <> ' 1' THEN
202
+ PERFORM set_config(' storage.gc.prefixes' , ' 1' , true);
203
+ END IF;
204
+
205
+ PERFORM storage .delete_leaf_prefixes (v_src_bucket_ids, v_src_names);
206
+ END IF;
207
+
208
+ RETURN NULL ;
209
+ END;
210
+ $$;
211
+
212
+ -- After DELETE on storage.prefixes
213
+ -- - When prefixes are deleted, remove now-empty ancestor prefixes
214
+ -- - Guards with `gc.prefixes`, locks, then prunes leaves derived from deleted prefixes
215
+ CREATE OR REPLACE FUNCTION storage .prefixes_delete_cleanup()
216
+ RETURNS trigger
217
+ LANGUAGE plpgsql
218
+ SECURITY DEFINER
219
+ AS $$
220
+ DECLARE
221
+ v_bucket_ids text [];
222
+ v_names text [];
223
+ BEGIN
224
+ IF current_setting(' storage.gc.prefixes' , true) = ' 1' THEN
225
+ RETURN NULL ;
226
+ END IF;
227
+
228
+ PERFORM set_config(' storage.gc.prefixes' , ' 1' , true);
229
+
230
+ SELECT COALESCE(array_agg(d .bucket_id ), ' {}' ),
231
+ COALESCE(array_agg(d .name ), ' {}' )
232
+ INTO v_bucket_ids, v_names
233
+ FROM deleted AS d
234
+ WHERE d .name <> ' ' ;
235
+
236
+ PERFORM storage .lock_top_prefixes (v_bucket_ids, v_names);
237
+ PERFORM storage .delete_leaf_prefixes (v_bucket_ids, v_names);
238
+
239
+ RETURN NULL ;
240
+ END;
241
+ $$;
242
+
243
+ -- Trigger bindings
244
+ CREATE TRIGGER objects_delete_cleanup
245
+ AFTER DELETE ON storage .objects
246
+ REFERENCING OLD TABLE AS deleted
247
+ FOR EACH STATEMENT
248
+ EXECUTE FUNCTION storage .objects_delete_cleanup ();
249
+
250
+ CREATE TRIGGER prefixes_delete_cleanup
251
+ AFTER DELETE ON storage .prefixes
252
+ REFERENCING OLD TABLE AS deleted
253
+ FOR EACH STATEMENT
254
+ EXECUTE FUNCTION storage .prefixes_delete_cleanup ();
255
+
256
+ CREATE TRIGGER objects_update_cleanup
257
+ AFTER UPDATE ON storage .objects
258
+ REFERENCING OLD TABLE AS old_rows NEW TABLE AS new_rows
259
+ FOR EACH STATEMENT
260
+ EXECUTE FUNCTION storage .objects_update_cleanup ();
0 commit comments