gongxiangp2/lib/material-purge-reference-query.ts

69 lines
4.2 KiB
TypeScript

import { createHash } from 'node:crypto'
import path from 'node:path'
import type { Prisma } from '@prisma/client'
import { getUploadRootDir } from '@/lib/upload-paths'
import type { PurgeFile } from '@/lib/material-purge-files'
type ReferenceRow = { sources: string[]; derived: boolean }
const hash = (value: string) => createHash('md5').update(value).digest('hex')
/** 索引只找候选;存储归属、历史引用及共用文件仍由原生路径规则核对。 */
export function purgeReferenceLookupKeys(files: readonly PurgeFile[]): string[] {
const keys = new Set<string>(['url:resolve'])
const root = path.resolve(getUploadRootDir())
const windows = process.platform === 'win32'
if (windows) keys.add('windows:unicode')
for (const file of files) {
const aliases = file.storage === 's3'
? [`/${file.key}`, `/${file.bucket}/${file.key}`]
: [file.key, `uploads/${file.key}`, `/uploads/${file.key}`, `./uploads/${file.key}`,
path.join(root, file.key), path.relative(process.cwd(), path.join(root, file.key)),
`./${path.relative(process.cwd(), path.join(root, file.key))}`]
for (const alias of aliases) {
let normalized = alias.replace(/\\/g, '/').replace(/\/{2,}/g, '/')
while (/(^|\/)\.(\/|$)/.test(normalized)) normalized = normalized.replace(/(^|\/)\.(\/|$)/g, '$1')
keys.add(windows
? `ascii:${hash(normalized.replace(/[A-Z]/g, char => char.toLowerCase()))}`
: `path:${hash(normalized)}`)
}
if (file.storage === 's3') {
const marker = 'public-display/'
const index = file.key.indexOf(marker)
if (index >= 0) keys.add(`derived:${hash(file.key.slice(index + marker.length))}`)
}
}
return Array.from(keys)
}
export async function findIndexedPurgeReferenceSources(db: Prisma.TransactionClient, files: readonly PurgeFile[]): Promise<ReferenceRow[]> {
const keys = purgeReferenceLookupKeys(files)
// MATERIALIZED 与 OFFSET 0 保留参数化查找边界,避免 custom plan
// 把大量检索键误估为命中全库;同一批键仍只访问每个引用索引一次。
return db.$queryRaw<ReferenceRow[]>`
WITH lookup AS MATERIALIZED (SELECT ${keys}::text[] AS keys)
SELECT hits.sources, hits.derived
FROM lookup
CROSS JOIN LATERAL (
SELECT ARRAY["previewUrl", "zipFilePath", "videoSourceUrl", "videoPreview540Url", "videoPreviewClipUrl", "videoCoverUrl"] || "displayImages" AS sources, false AS derived
FROM materials WHERE purge_reference_keys(ARRAY["previewUrl", "zipFilePath", "videoSourceUrl", "videoPreview540Url", "videoPreviewClipUrl", "videoCoverUrl"] || "displayImages", false) && lookup.keys
UNION ALL
SELECT ARRAY[file_url], true FROM material_assets WHERE purge_reference_keys(ARRAY[file_url], true) && lookup.keys
UNION ALL
SELECT ARRAY[file_path, preview_url], false FROM material_skus WHERE purge_reference_keys(ARRAY[file_path, preview_url], false) && lookup.keys
UNION ALL
SELECT ARRAY[file_path, preview_image_path] || preview_image_slices, false FROM brand_knowledge WHERE purge_reference_keys(ARRAY[file_path, preview_image_path] || preview_image_slices, false) && lookup.keys
UNION ALL
SELECT ARRAY["headerLogoUrl", "loginLogoUrl"], false FROM system_settings WHERE purge_reference_keys(ARRAY["headerLogoUrl", "loginLogoUrl"], false) && lookup.keys
UNION ALL
SELECT purge_json_reference_strings(image_urls), false FROM performance_survey_work_items WHERE purge_reference_keys(purge_json_reference_strings(image_urls), false) && lookup.keys
UNION ALL
SELECT purge_json_reference_strings(image_urls), false FROM work_review_versions WHERE purge_reference_keys(purge_json_reference_strings(image_urls), false) && lookup.keys
UNION ALL
SELECT purge_json_reference_strings(assets), false FROM product_package_upload_jobs WHERE status IN ('queued', 'processing') AND purge_reference_keys(purge_json_reference_strings(assets), false) && lookup.keys
UNION ALL
SELECT purge_json_reference_strings(operations), false FROM task_detail_change_requests WHERE status NOT IN ('applied', 'rejected', 'expired', 'cancelled') AND purge_reference_keys(purge_json_reference_strings(operations), false) && lookup.keys
OFFSET 0
) AS hits
`
}