140 lines
6.8 KiB
PL/PgSQL
140 lines
6.8 KiB
PL/PgSQL
BEGIN;
|
|
|
|
-- 只建立引用检索索引,不改动业务记录。索引由 PostgreSQL 随每次写入维护,
|
|
-- 因而覆盖 ORM、历史导入及直接 SQL 写入,不依赖应用层双写或定时回填。
|
|
CREATE FUNCTION purge_decode_reference(value text) RETURNS text
|
|
LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL UNSAFE AS $$
|
|
DECLARE result bytea := ''::bytea; position integer := 1; piece text;
|
|
BEGIN
|
|
IF strpos(value, '%') = 0 THEN RETURN value; END IF;
|
|
WHILE position <= length(value) LOOP
|
|
piece := substr(value, position, 1);
|
|
IF piece = '%' THEN
|
|
piece := substr(value, position + 1, 2);
|
|
IF piece !~ '^[0-9a-fA-F]{2}$' THEN RETURN value; END IF;
|
|
result := result || decode(piece, 'hex'); position := position + 3;
|
|
ELSE
|
|
result := result || convert_to(piece, 'UTF8'); position := position + 1;
|
|
END IF;
|
|
END LOOP;
|
|
RETURN convert_from(result, 'UTF8');
|
|
EXCEPTION WHEN character_not_in_repertoire OR untranslatable_character THEN RETURN value;
|
|
END;
|
|
$$;
|
|
|
|
CREATE FUNCTION purge_json_reference_strings(value jsonb) RETURNS text[]
|
|
LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL UNSAFE AS $$
|
|
DECLARE result text[] := '{}'; child jsonb;
|
|
BEGIN
|
|
CASE jsonb_typeof(value)
|
|
WHEN 'string' THEN RETURN ARRAY[value #>> '{}'];
|
|
WHEN 'array' THEN
|
|
FOR child IN SELECT jsonb_array_elements(value) LOOP
|
|
result := result || purge_json_reference_strings(child);
|
|
END LOOP;
|
|
WHEN 'object' THEN
|
|
FOR child IN SELECT v FROM jsonb_each(value) AS entry(k, v) LOOP
|
|
result := result || purge_json_reference_strings(child);
|
|
END LOOP;
|
|
ELSE NULL;
|
|
END CASE;
|
|
RETURN result;
|
|
END;
|
|
$$;
|
|
|
|
-- 哈希仅用于索引候选匹配,最终删除授权仍使用应用的完整 storage/bucket/key。
|
|
-- 哈希冲突只会增加候选,不会授权删除;内嵌 data:image 不进入文件索引。
|
|
CREATE FUNCTION purge_reference_keys(sources text[], derive_white_background boolean) RETURNS text[]
|
|
LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL UNSAFE AS $$
|
|
DECLARE
|
|
result text[] := '{}'; source text; value text; local_source text; decoded text; derived text;
|
|
segments text[]; tail text[]; wb integer; round integer; is_http boolean;
|
|
whitespace text := U&'\0009\000A\000B\000C\000D\0020\00A0\1680\2000\2001\2002\2003\2004\2005\2006\2007\2008\2009\200A\2028\2029\202F\205F\3000\FEFF';
|
|
BEGIN
|
|
FOREACH source IN ARRAY sources LOOP
|
|
value := btrim(source, whitespace);
|
|
IF value IS NULL OR value = '' OR value LIKE 'data:image/%' THEN CONTINUE; END IF;
|
|
is_http := value ~* '^https?://';
|
|
value := replace(value, chr(92), '/');
|
|
IF is_http THEN
|
|
-- URL 的点段及内嵌制表/换行由 WHATWG URL 处理。保留稀少的这类候选,
|
|
-- 不在 SQL 中另写一套 URL 授权规则;常规地址及编码地址走精确索引。
|
|
IF value ~ E'[\\t\\r\\n]' OR value ~* '/(\.|%2e)' THEN
|
|
result := array_append(result, 'url:resolve');
|
|
END IF;
|
|
value := regexp_replace(value, '[?#].*$', '');
|
|
value := regexp_replace(value, '^https?://[^/]*', '', 'i');
|
|
END IF;
|
|
IF NOT is_http THEN
|
|
value := regexp_replace(value, '/+', '/', 'g');
|
|
WHILE value ~ '(^|/)\.(\/|$)' LOOP
|
|
value := regexp_replace(value, '(^|/)\.(\/|$)', '\1', 'g');
|
|
END LOOP;
|
|
END IF;
|
|
local_source := value;
|
|
IF value ~ '[^\x01-\x7f]' THEN result := array_append(result, 'windows:unicode'); END IF;
|
|
FOR round IN 0..6 LOOP
|
|
IF is_http THEN value := regexp_replace(value, '^/+', '/'); END IF;
|
|
result := result || ARRAY['path:' || md5(value),
|
|
'ascii:' || md5(translate(value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'))];
|
|
decoded := purge_decode_reference(value);
|
|
EXIT WHEN decoded = value;
|
|
value := decoded;
|
|
END LOOP;
|
|
IF derive_white_background AND NOT is_http THEN
|
|
segments := array_remove(string_to_array(local_source, '/'), '');
|
|
wb := array_position(segments, 'white-background');
|
|
IF wb >= 3 AND array_length(segments, 1) >= wb + 2 THEN
|
|
tail := segments[wb + 2:array_length(segments, 1)];
|
|
IF tail[1] = 'preview' THEN tail := tail[2:array_length(tail, 1)]; END IF;
|
|
IF cardinality(tail) > 0 THEN
|
|
derived := 'white-background/' || segments[wb - 1] || '/' || segments[wb + 1] || '/' || array_to_string(tail, '/');
|
|
IF derived ~ E'[\\t\\r\\n]' OR derived ~* '/(\.|%2e)' THEN
|
|
result := array_append(result, 'url:resolve');
|
|
END IF;
|
|
-- 派生地址会经过 URL 解析;原始 ?/# 是查询/片段,编码形式仍是文件名。
|
|
derived := regexp_replace(derived, '[?#].*$', '');
|
|
FOR round IN 0..6 LOOP
|
|
result := array_append(result, 'derived:' || md5(derived));
|
|
decoded := purge_decode_reference(derived);
|
|
EXIT WHEN decoded = derived;
|
|
derived := decoded;
|
|
END LOOP;
|
|
END IF;
|
|
END IF;
|
|
END IF;
|
|
END LOOP;
|
|
RETURN ARRAY(SELECT DISTINCT token FROM unnest(result) token);
|
|
END;
|
|
$$;
|
|
|
|
CREATE INDEX materials_purge_references_idx ON materials USING gin
|
|
(purge_reference_keys(ARRAY["previewUrl", "zipFilePath", "videoSourceUrl", "videoPreview540Url", "videoPreviewClipUrl", "videoCoverUrl"] || "displayImages", false));
|
|
CREATE INDEX material_assets_purge_references_idx ON material_assets USING gin
|
|
(purge_reference_keys(ARRAY[file_url], true));
|
|
CREATE INDEX material_skus_purge_references_idx ON material_skus USING gin
|
|
(purge_reference_keys(ARRAY[file_path, preview_url], false));
|
|
CREATE INDEX brand_knowledge_purge_references_idx ON brand_knowledge USING gin
|
|
(purge_reference_keys(ARRAY[file_path, preview_image_path] || preview_image_slices, false));
|
|
CREATE INDEX system_settings_purge_references_idx ON system_settings USING gin
|
|
(purge_reference_keys(ARRAY["headerLogoUrl", "loginLogoUrl"], false));
|
|
CREATE INDEX performance_work_items_purge_references_idx ON performance_survey_work_items USING gin
|
|
(purge_reference_keys(purge_json_reference_strings(image_urls), false));
|
|
CREATE INDEX work_review_versions_purge_references_idx ON work_review_versions USING gin
|
|
(purge_reference_keys(purge_json_reference_strings(image_urls), false));
|
|
CREATE INDEX package_upload_jobs_purge_references_idx ON product_package_upload_jobs USING gin
|
|
(purge_reference_keys(purge_json_reference_strings(assets), false)) WHERE status IN ('queued', 'processing');
|
|
CREATE INDEX task_detail_changes_purge_references_idx ON task_detail_change_requests USING gin
|
|
(purge_reference_keys(purge_json_reference_strings(operations), false)) WHERE status NOT IN ('applied', 'rejected', 'expired', 'cancelled');
|
|
|
|
ANALYZE materials;
|
|
ANALYZE material_assets;
|
|
ANALYZE material_skus;
|
|
ANALYZE brand_knowledge;
|
|
ANALYZE system_settings;
|
|
ANALYZE performance_survey_work_items;
|
|
ANALYZE work_review_versions;
|
|
ANALYZE product_package_upload_jobs;
|
|
ANALYZE task_detail_change_requests;
|
|
COMMIT;
|