const { Pool } = require('pg'); require('dotenv').config({ path: '.env.local' }); const SYSTEM_USER_ID = '00000000-0000-0000-0000-000000000000'; function short(value, length = 160) { if (value == null) return value; const text = typeof value === 'string' ? value : JSON.stringify(value); return text.length > length ? `${text.slice(0, length)}...` : text; } async function main() { const pool = new Pool({ connectionString: process.env.LOCAL_DB_URL }); const client = await pool.connect(); try { const tableColumns = await client.query(` SELECT table_schema, table_name, column_name, data_type FROM information_schema.columns WHERE (table_schema = 'public' AND table_name IN ('profiles', 'works', 'user_api_keys', 'orders', 'credit_transactions')) OR (table_schema = 'auth' AND table_name = 'users') ORDER BY table_schema, table_name, ordinal_position `); const userSummary = await client.query(` SELECT (SELECT COUNT(*)::int FROM auth.users) AS auth_users, (SELECT COUNT(*)::int FROM profiles) AS profiles, (SELECT COUNT(*)::int FROM profiles WHERE role = 'admin') AS admins, (SELECT COUNT(*)::int FROM profiles WHERE role <> 'admin') AS non_admin_profiles, (SELECT COUNT(*)::int FROM auth.users u LEFT JOIN profiles p ON p.id = u.id WHERE p.id IS NULL) AS auth_without_profile, (SELECT COUNT(*)::int FROM profiles p LEFT JOIN auth.users u ON u.id = p.id WHERE u.id IS NULL) AS profile_without_auth, (SELECT COUNT(*)::int FROM auth.users WHERE password_hash IS NULL OR password_hash = '') AS auth_without_password_hash, (SELECT COUNT(*)::int FROM auth.users WHERE password_hash IS NOT NULL AND password_hash <> '') AS auth_with_password_hash `); const userSamples = await client.query(` SELECT p.id, p.email, p.nickname, p.role, p.membership_tier, p.is_active, u.id IS NOT NULL AS has_auth, (u.password_hash IS NOT NULL AND u.password_hash <> '') AS has_password_hash, p.created_at FROM profiles p LEFT JOIN auth.users u ON u.id = p.id ORDER BY p.created_at DESC NULLS LAST LIMIT 80 `); const workSummary = await client.query(` SELECT COUNT(*)::int AS total, COUNT(*) FILTER (WHERE status = 'completed')::int AS completed, COUNT(*) FILTER (WHERE is_public = true AND status = 'completed')::int AS public_completed, COUNT(*) FILTER (WHERE is_public = false AND status = 'completed')::int AS private_completed, COUNT(*) FILTER (WHERE user_id IS NULL)::int AS null_user_id, COUNT(*) FILTER (WHERE user_id = $1)::int AS system_user_id, COUNT(*) FILTER (WHERE p.id IS NULL)::int AS missing_profile, COUNT(*) FILTER (WHERE p.id IS NOT NULL)::int AS linked_profile FROM works w LEFT JOIN profiles p ON p.id = w.user_id `, [SYSTEM_USER_ID]); const publicWorkSummary = await client.query(` SELECT COUNT(*)::int AS public_total, COUNT(*) FILTER (WHERE w.user_id IS NULL)::int AS null_user_id, COUNT(*) FILTER (WHERE w.user_id = $1)::int AS system_user_id, COUNT(*) FILTER (WHERE p.id IS NULL)::int AS missing_profile, COUNT(*) FILTER (WHERE p.id IS NOT NULL)::int AS linked_profile FROM works w LEFT JOIN profiles p ON p.id = w.user_id WHERE w.is_public = true AND w.status = 'completed' `, [SYSTEM_USER_ID]); const workByUser = await client.query(` SELECT COALESCE(p.email, '[missing-profile]') AS email, COALESCE(p.nickname, '') AS nickname, COALESCE(p.role, '') AS role, w.user_id, COUNT(*)::int AS total_works, COUNT(*) FILTER (WHERE w.status = 'completed')::int AS completed_works, COUNT(*) FILTER (WHERE w.is_public = true AND w.status = 'completed')::int AS public_works, COUNT(*) FILTER (WHERE w.is_public = false AND w.status = 'completed')::int AS history_works FROM works w LEFT JOIN profiles p ON p.id = w.user_id GROUP BY w.user_id, p.email, p.nickname, p.role ORDER BY total_works DESC LIMIT 120 `); const orphanSamples = await client.query(` SELECT w.id, w.user_id, w.type, w.status, w.is_public, w.result_url, LEFT(COALESCE(w.prompt, ''), 140) AS prompt, w.params, w.created_at FROM works w LEFT JOIN profiles p ON p.id = w.user_id WHERE p.id IS NULL OR w.user_id = $1 OR w.user_id IS NULL ORDER BY w.created_at DESC NULLS LAST LIMIT 80 `, [SYSTEM_USER_ID]); const paramKeys = await client.query(` SELECT key, COUNT(*)::int AS count FROM works w CROSS JOIN LATERAL jsonb_object_keys(COALESCE(w.params, '{}'::jsonb)) AS key LEFT JOIN profiles p ON p.id = w.user_id WHERE w.is_public = true AND w.status = 'completed' AND (p.id IS NULL OR w.user_id = $1 OR w.user_id IS NULL) GROUP BY key ORDER BY count DESC, key LIMIT 80 `, [SYSTEM_USER_ID]); const possibleOwnerFields = await client.query(` SELECT id, user_id, params->>'user_id' AS params_user_id, params->>'userId' AS params_user_id_camel, params->>'publisher_id' AS publisher_id, params->>'publisherId' AS publisher_id_camel, params->>'owner_id' AS owner_id, params->>'ownerId' AS owner_id_camel, params->>'created_by' AS created_by, params->>'createdBy' AS created_by_camel, params->>'email' AS params_email, params->>'userEmail' AS params_user_email, params->>'publisherEmail' AS params_publisher_email, params->>'nickname' AS params_nickname, params->>'userName' AS params_user_name, LEFT(COALESCE(prompt, ''), 120) AS prompt FROM works WHERE is_public = true AND status = 'completed' AND (user_id IS NULL OR user_id = $1 OR NOT EXISTS (SELECT 1 FROM profiles p WHERE p.id = works.user_id)) ORDER BY created_at DESC NULLS LAST LIMIT 80 `, [SYSTEM_USER_ID]); const duplicateCandidates = await client.query(` SELECT public.id AS orphan_id, public.user_id AS orphan_user_id, private.id AS owned_id, private.user_id AS owner_user_id, p.email, p.nickname, CASE WHEN private.result_url = public.result_url THEN 'result_url' WHEN COALESCE(private.thumbnail_url, '') <> '' AND private.thumbnail_url = public.thumbnail_url THEN 'thumbnail_url' WHEN COALESCE(private.prompt, '') <> '' AND private.prompt = public.prompt THEN 'prompt_time' ELSE 'unknown' END AS match_type, ABS(EXTRACT(EPOCH FROM (private.created_at - public.created_at)))::int AS seconds_apart, LEFT(COALESCE(public.prompt, ''), 120) AS prompt FROM works public JOIN works private ON private.id <> public.id AND private.user_id IS NOT NULL AND private.user_id <> $1 AND ( private.result_url = public.result_url OR ( COALESCE(public.thumbnail_url, '') <> '' AND private.thumbnail_url = public.thumbnail_url ) OR ( COALESCE(private.prompt, '') <> '' AND private.prompt = public.prompt AND private.created_at BETWEEN public.created_at - INTERVAL '30 minutes' AND public.created_at + INTERVAL '30 minutes' ) ) JOIN profiles p ON p.id = private.user_id LEFT JOIN profiles public_profile ON public_profile.id = public.user_id WHERE public.is_public = true AND public.status = 'completed' AND (public_profile.id IS NULL OR public.user_id = $1 OR public.user_id IS NULL) ORDER BY public.created_at DESC NULLS LAST, match_type, seconds_apart LIMIT 100 `, [SYSTEM_USER_ID]); const output = { columns: tableColumns.rows, userSummary: userSummary.rows[0], userSamples: userSamples.rows, workSummary: workSummary.rows[0], publicWorkSummary: publicWorkSummary.rows[0], workByUser: workByUser.rows, orphanSamples: orphanSamples.rows.map(row => ({ ...row, result_url: short(row.result_url, 120), params: short(row.params, 300) })), anonymousParamKeys: paramKeys.rows, possibleOwnerFields: possibleOwnerFields.rows, duplicateCandidates: duplicateCandidates.rows, }; console.log(JSON.stringify(output, null, 2)); } finally { client.release(); await pool.end(); } } main().catch(error => { console.error(error); process.exit(1); });