|
| 1 | +-- ============================================================================= |
| 2 | +-- delete_package(p_package_id) |
| 3 | +-- |
| 4 | +-- Manually and completely removes a single package and every related row. |
| 5 | +-- |
| 6 | +-- WHY THIS IS NOT JUST "DELETE FROM packages WHERE id = ...": |
| 7 | +-- Several child tables ARE wired with ON DELETE CASCADE and would be removed |
| 8 | +-- automatically (package_metadata, items -> submission_reviews -> |
| 9 | +-- submission_review_entries / submission_item_notes / submissions, |
| 10 | +-- update_requests, internal_staff_documents). |
| 11 | +-- |
| 12 | +-- But these related rows are NOT cascaded and a plain delete would orphan them: |
| 13 | +-- * submitted_forms - submissions.submitted_form_id points TO it (no cascade) |
| 14 | +-- * submitted_documents - submissions.submitted_document_id points TO it (no cascade) |
| 15 | +-- * geo_data_uploads - no FK at all; linked by raw_s3_key == document url |
| 16 | +-- * activity_logs - keyed by value, not FK: |
| 17 | +-- PACKAGE -> package_versions.original_package_id |
| 18 | +-- SUBMISSION -> submission id |
| 19 | +-- * email_queue - entity_type='PACKAGE', entity_id = package id |
| 20 | +-- * package_versions - packages.version_id points TO it (no cascade) |
| 21 | +-- |
| 22 | +-- This function deletes everything in FK-safe order inside one transaction |
| 23 | +-- (the whole function body runs atomically; any error rolls it all back). |
| 24 | +-- |
| 25 | +-- NOTE: geo_data_uploads also have S3 artifacts (raw/preview/standard keys). |
| 26 | +-- This function removes the DB rows only. It RETURNS the affected S3 keys |
| 27 | +-- so the caller can clean up object storage. SQL cannot delete from S3. |
| 28 | +-- |
| 29 | +-- USAGE: |
| 30 | +-- SELECT * FROM delete_package(123); |
| 31 | +-- |
| 32 | +-- Returns one row: counts per table + the S3 keys that still need deletion. |
| 33 | +-- ============================================================================= |
| 34 | + |
| 35 | +CREATE OR REPLACE FUNCTION delete_package(p_package_id INTEGER) |
| 36 | +RETURNS TABLE ( |
| 37 | + deleted_package_id INTEGER, |
| 38 | + items_deleted INTEGER, |
| 39 | + submissions_deleted INTEGER, |
| 40 | + submitted_forms_deleted INTEGER, |
| 41 | + submitted_documents_deleted INTEGER, |
| 42 | + geo_uploads_deleted INTEGER, |
| 43 | + reviews_deleted INTEGER, |
| 44 | + review_entries_deleted INTEGER, |
| 45 | + item_notes_deleted INTEGER, |
| 46 | + update_requests_deleted INTEGER, |
| 47 | + internal_docs_deleted INTEGER, |
| 48 | + package_metadata_deleted INTEGER, |
| 49 | + activity_logs_deleted INTEGER, |
| 50 | + email_queue_deleted INTEGER, |
| 51 | + package_versions_deleted INTEGER, |
| 52 | + s3_keys_to_delete TEXT[] |
| 53 | +) |
| 54 | +LANGUAGE plpgsql |
| 55 | +AS $$ |
| 56 | +DECLARE |
| 57 | + v_item_ids INTEGER[]; |
| 58 | + v_submission_ids INTEGER[]; |
| 59 | + v_review_ids INTEGER[]; |
| 60 | + v_form_ids INTEGER[]; |
| 61 | + v_document_ids INTEGER[]; |
| 62 | + v_document_urls TEXT[]; |
| 63 | + v_version_id INTEGER; |
| 64 | + v_original_package_id INTEGER; |
| 65 | + |
| 66 | + v_items_deleted INTEGER := 0; |
| 67 | + v_submissions_deleted INTEGER := 0; |
| 68 | + v_forms_deleted INTEGER := 0; |
| 69 | + v_documents_deleted INTEGER := 0; |
| 70 | + v_geo_deleted INTEGER := 0; |
| 71 | + v_reviews_deleted INTEGER := 0; |
| 72 | + v_review_entries_deleted INTEGER := 0; |
| 73 | + v_item_notes_deleted INTEGER := 0; |
| 74 | + v_update_requests_deleted INTEGER := 0; |
| 75 | + v_internal_docs_deleted INTEGER := 0; |
| 76 | + v_package_metadata_deleted INTEGER := 0; |
| 77 | + v_activity_logs_deleted INTEGER := 0; |
| 78 | + v_email_queue_deleted INTEGER := 0; |
| 79 | + v_package_versions_deleted INTEGER := 0; |
| 80 | + v_package_logs_deleted INTEGER := 0; |
| 81 | + |
| 82 | + v_s3_keys TEXT[] := ARRAY[]::TEXT[]; |
| 83 | +BEGIN |
| 84 | + -- Guard: package must exist. |
| 85 | + IF NOT EXISTS (SELECT 1 FROM packages WHERE id = p_package_id) THEN |
| 86 | + RAISE EXCEPTION 'Package % does not exist', p_package_id; |
| 87 | + END IF; |
| 88 | + |
| 89 | + -- ------------------------------------------------------------------------- |
| 90 | + -- 1. Resolve the id graph BEFORE deleting anything. |
| 91 | + -- ------------------------------------------------------------------------- |
| 92 | + SELECT version_id INTO v_version_id FROM packages WHERE id = p_package_id; |
| 93 | + |
| 94 | + IF v_version_id IS NOT NULL THEN |
| 95 | + SELECT original_package_id INTO v_original_package_id |
| 96 | + FROM package_versions WHERE id = v_version_id; |
| 97 | + END IF; |
| 98 | + |
| 99 | + -- Items under this package. |
| 100 | + SELECT array_agg(id) INTO v_item_ids |
| 101 | + FROM items WHERE package_id = p_package_id; |
| 102 | + v_item_ids := COALESCE(v_item_ids, ARRAY[]::INTEGER[]); |
| 103 | + |
| 104 | + -- Submissions under those items. |
| 105 | + IF array_length(v_item_ids, 1) IS NOT NULL THEN |
| 106 | + SELECT array_agg(id) INTO v_submission_ids |
| 107 | + FROM submissions WHERE item_id = ANY(v_item_ids); |
| 108 | + |
| 109 | + SELECT array_agg(id) INTO v_review_ids |
| 110 | + FROM submission_reviews WHERE item_id = ANY(v_item_ids); |
| 111 | + END IF; |
| 112 | + v_submission_ids := COALESCE(v_submission_ids, ARRAY[]::INTEGER[]); |
| 113 | + v_review_ids := COALESCE(v_review_ids, ARRAY[]::INTEGER[]); |
| 114 | + |
| 115 | + -- Forms and documents referenced by those submissions. |
| 116 | + IF array_length(v_submission_ids, 1) IS NOT NULL THEN |
| 117 | + SELECT array_agg(DISTINCT submitted_form_id) |
| 118 | + INTO v_form_ids |
| 119 | + FROM submissions |
| 120 | + WHERE id = ANY(v_submission_ids) AND submitted_form_id IS NOT NULL; |
| 121 | + |
| 122 | + SELECT array_agg(DISTINCT submitted_document_id) |
| 123 | + INTO v_document_ids |
| 124 | + FROM submissions |
| 125 | + WHERE id = ANY(v_submission_ids) AND submitted_document_id IS NOT NULL; |
| 126 | + END IF; |
| 127 | + v_form_ids := COALESCE(v_form_ids, ARRAY[]::INTEGER[]); |
| 128 | + v_document_ids := COALESCE(v_document_ids, ARRAY[]::INTEGER[]); |
| 129 | + |
| 130 | + -- Document URLs (used to find geo uploads and to report S3 cleanup). |
| 131 | + IF array_length(v_document_ids, 1) IS NOT NULL THEN |
| 132 | + SELECT array_agg(url) INTO v_document_urls |
| 133 | + FROM submitted_documents WHERE id = ANY(v_document_ids); |
| 134 | + END IF; |
| 135 | + v_document_urls := COALESCE(v_document_urls, ARRAY[]::TEXT[]); |
| 136 | + |
| 137 | + -- Collect S3 keys from internal staff documents for the caller to clean up. |
| 138 | + SELECT COALESCE(array_agg(url), ARRAY[]::TEXT[]) |
| 139 | + INTO v_s3_keys |
| 140 | + FROM internal_staff_documents |
| 141 | + WHERE package_id = p_package_id AND url IS NOT NULL; |
| 142 | + |
| 143 | + -- Add document urls to the S3 cleanup report. |
| 144 | + v_s3_keys := v_s3_keys || v_document_urls; |
| 145 | + |
| 146 | + -- Add geo upload S3 tiers (raw/preview/standard) for matching documents. |
| 147 | + IF array_length(v_document_urls, 1) IS NOT NULL THEN |
| 148 | + SELECT v_s3_keys |
| 149 | + || COALESCE(array_agg(raw_s3_key) FILTER (WHERE raw_s3_key IS NOT NULL), ARRAY[]::TEXT[]) |
| 150 | + || COALESCE(array_agg(preview_s3_key) FILTER (WHERE preview_s3_key IS NOT NULL), ARRAY[]::TEXT[]) |
| 151 | + || COALESCE(array_agg(standard_s3_key) FILTER (WHERE standard_s3_key IS NOT NULL), ARRAY[]::TEXT[]) |
| 152 | + INTO v_s3_keys |
| 153 | + FROM geo_data_uploads |
| 154 | + WHERE raw_s3_key = ANY(v_document_urls); |
| 155 | + END IF; |
| 156 | + |
| 157 | + -- ------------------------------------------------------------------------- |
| 158 | + -- 2. Delete value-keyed / non-cascaded tables that reference the package |
| 159 | + -- or its children, from the leaves inward. |
| 160 | + -- ------------------------------------------------------------------------- |
| 161 | + |
| 162 | + -- 2a. geo_data_uploads (no FK; matched by document url == raw_s3_key). |
| 163 | + IF array_length(v_document_urls, 1) IS NOT NULL THEN |
| 164 | + DELETE FROM geo_data_uploads WHERE raw_s3_key = ANY(v_document_urls); |
| 165 | + GET DIAGNOSTICS v_geo_deleted = ROW_COUNT; |
| 166 | + END IF; |
| 167 | + |
| 168 | + -- 2b. activity_logs for the submissions (entity_type = 'SUBMISSION'). |
| 169 | + IF array_length(v_submission_ids, 1) IS NOT NULL THEN |
| 170 | + DELETE FROM activity_logs |
| 171 | + WHERE entity_type = 'SUBMISSION' AND entity_id = ANY(v_submission_ids); |
| 172 | + GET DIAGNOSTICS v_activity_logs_deleted = ROW_COUNT; |
| 173 | + END IF; |
| 174 | + |
| 175 | + -- 2c. activity_logs for the package. |
| 176 | + -- Historically keyed by package_versions.original_package_id; older rows |
| 177 | + -- may still be keyed by the raw package id. Clear both to be safe. |
| 178 | + DELETE FROM activity_logs |
| 179 | + WHERE entity_type = 'PACKAGE' |
| 180 | + AND entity_id IN (p_package_id, COALESCE(v_original_package_id, -1)); |
| 181 | + GET DIAGNOSTICS v_package_logs_deleted = ROW_COUNT; |
| 182 | + v_activity_logs_deleted := v_activity_logs_deleted + v_package_logs_deleted; |
| 183 | + |
| 184 | + -- 2d. email_queue for the package. |
| 185 | + DELETE FROM email_queue |
| 186 | + WHERE entity_type = 'PACKAGE' AND entity_id = p_package_id; |
| 187 | + GET DIAGNOSTICS v_email_queue_deleted = ROW_COUNT; |
| 188 | + |
| 189 | + -- ------------------------------------------------------------------------- |
| 190 | + -- 3. Delete the package. ON DELETE CASCADE removes, in order: |
| 191 | + -- package_metadata, update_requests, internal_staff_documents, |
| 192 | + -- items -> submission_reviews -> submission_review_entries, |
| 193 | + -- submission_item_notes, submissions. |
| 194 | + -- We capture counts first so the return value is accurate. |
| 195 | + -- ------------------------------------------------------------------------- |
| 196 | + SELECT count(*) INTO v_package_metadata_deleted FROM package_metadata WHERE package_id = p_package_id; |
| 197 | + SELECT count(*) INTO v_update_requests_deleted FROM update_requests WHERE submission_package_id = p_package_id; |
| 198 | + SELECT count(*) INTO v_internal_docs_deleted FROM internal_staff_documents WHERE package_id = p_package_id; |
| 199 | + v_items_deleted := COALESCE(array_length(v_item_ids, 1), 0); |
| 200 | + v_submissions_deleted := COALESCE(array_length(v_submission_ids, 1), 0); |
| 201 | + v_reviews_deleted := COALESCE(array_length(v_review_ids, 1), 0); |
| 202 | + v_forms_deleted := COALESCE(array_length(v_form_ids, 1), 0); |
| 203 | + v_documents_deleted := COALESCE(array_length(v_document_ids, 1), 0); |
| 204 | + |
| 205 | + IF array_length(v_review_ids, 1) IS NOT NULL THEN |
| 206 | + SELECT count(*) INTO v_review_entries_deleted |
| 207 | + FROM submission_review_entries WHERE review_id = ANY(v_review_ids); |
| 208 | + END IF; |
| 209 | + IF array_length(v_item_ids, 1) IS NOT NULL THEN |
| 210 | + SELECT count(*) INTO v_item_notes_deleted |
| 211 | + FROM submission_item_notes WHERE item_id = ANY(v_item_ids); |
| 212 | + END IF; |
| 213 | + |
| 214 | + DELETE FROM packages WHERE id = p_package_id; |
| 215 | + |
| 216 | + -- ------------------------------------------------------------------------- |
| 217 | + -- 4. Now that submissions are gone, remove the forms/documents they |
| 218 | + -- pointed to (these FKs point TO those tables, so they survive the |
| 219 | + -- cascade and must be deleted explicitly, after the package). |
| 220 | + -- ------------------------------------------------------------------------- |
| 221 | + IF array_length(v_document_ids, 1) IS NOT NULL THEN |
| 222 | + DELETE FROM submitted_documents WHERE id = ANY(v_document_ids); |
| 223 | + GET DIAGNOSTICS v_documents_deleted = ROW_COUNT; |
| 224 | + END IF; |
| 225 | + |
| 226 | + IF array_length(v_form_ids, 1) IS NOT NULL THEN |
| 227 | + DELETE FROM submitted_forms WHERE id = ANY(v_form_ids); |
| 228 | + GET DIAGNOSTICS v_forms_deleted = ROW_COUNT; |
| 229 | + END IF; |
| 230 | + |
| 231 | + -- ------------------------------------------------------------------------- |
| 232 | + -- 5. package_versions lineage row. |
| 233 | + -- Only delete it if no OTHER package still references this version id |
| 234 | + -- (a version row can be shared across the version lineage). |
| 235 | + -- ------------------------------------------------------------------------- |
| 236 | + IF v_version_id IS NOT NULL THEN |
| 237 | + IF NOT EXISTS (SELECT 1 FROM packages WHERE version_id = v_version_id) THEN |
| 238 | + DELETE FROM package_versions WHERE id = v_version_id; |
| 239 | + GET DIAGNOSTICS v_package_versions_deleted = ROW_COUNT; |
| 240 | + END IF; |
| 241 | + END IF; |
| 242 | + |
| 243 | + -- ------------------------------------------------------------------------- |
| 244 | + -- 6. Return the report. |
| 245 | + -- ------------------------------------------------------------------------- |
| 246 | + deleted_package_id := p_package_id; |
| 247 | + items_deleted := v_items_deleted; |
| 248 | + submissions_deleted := v_submissions_deleted; |
| 249 | + submitted_forms_deleted := v_forms_deleted; |
| 250 | + submitted_documents_deleted := v_documents_deleted; |
| 251 | + geo_uploads_deleted := v_geo_deleted; |
| 252 | + reviews_deleted := v_reviews_deleted; |
| 253 | + review_entries_deleted := v_review_entries_deleted; |
| 254 | + item_notes_deleted := v_item_notes_deleted; |
| 255 | + update_requests_deleted := v_update_requests_deleted; |
| 256 | + internal_docs_deleted := v_internal_docs_deleted; |
| 257 | + package_metadata_deleted := v_package_metadata_deleted; |
| 258 | + activity_logs_deleted := v_activity_logs_deleted; |
| 259 | + email_queue_deleted := v_email_queue_deleted; |
| 260 | + package_versions_deleted := v_package_versions_deleted; |
| 261 | + s3_keys_to_delete := v_s3_keys; |
| 262 | + |
| 263 | + RETURN NEXT; |
| 264 | +END; |
| 265 | +$$; |
0 commit comments