-- ============================================================================ -- پیش‌بررسی مهاجرت — فقط شمارش، صفر داده -- ============================================================================ -- -- روی دیتابیس MySQL قدیمی (اسکیمای `oapi`) اجرا شود. -- -- ⚠️ این فایل فقط SELECT COUNT / SUM / MIN / MAX دارد. -- هیچ INSERT / UPDATE / DELETE / DROP / ALTER در آن نیست. -- هیچ ستون متنی (نام، موبایل، کد ملی، آدرس) در خروجی ظاهر نمی‌شود. -- -- اجرا: -- mysql -u USER -p -h HOST -P 3305 oapi < migration-precheck.sql > out.txt -- -- سپس محتوای out.txt کپی شود. خروجی فقط عدد است. -- -- مرجع: docs/DATA-MIGRATION-MAP.md §۳ · §۵.۳ · §۵.۵ · §۵.۶ -- ============================================================================ -- --------------------------------------------------------------------------- -- بخش ۱ — حجم کار: تعداد ردیف زنده و حذف‌شده‌ی نرم -- مرجع: §۵.۶ — ردیف حذف‌شده نباید منتقل شود -- --------------------------------------------------------------------------- SELECT '=== 1. ROW COUNTS ===' AS section; SELECT 'customers' AS tbl, COUNT(*) AS live, SUM(deleted_at IS NOT NULL) AS soft_deleted FROM customers UNION ALL SELECT 'users', COUNT(*), SUM(deleted_at IS NOT NULL) FROM users UNION ALL SELECT 'orders', COUNT(*), SUM(deleted_at IS NOT NULL) FROM orders UNION ALL SELECT 'order_items', COUNT(*), SUM(deleted_at IS NOT NULL) FROM order_items UNION ALL SELECT 'cashes', COUNT(*), SUM(deleted_at IS NOT NULL) FROM cashes UNION ALL SELECT 'product', COUNT(*), SUM(deleted_at IS NOT NULL) FROM product UNION ALL SELECT 'variant', COUNT(*), SUM(deleted_at IS NOT NULL) FROM products_productvarient UNION ALL SELECT 'prod_image', COUNT(*), SUM(deleted_at IS NOT NULL) FROM products_productimage UNION ALL SELECT 'category', COUNT(*), SUM(deleted_at IS NOT NULL) FROM category_collection UNION ALL SELECT 'image', COUNT(*), SUM(deleted_at IS NOT NULL) FROM image UNION ALL SELECT 'cities', COUNT(*), SUM(deleted_at IS NOT NULL) FROM cities UNION ALL SELECT 'branches', COUNT(*), SUM(deleted_at IS NOT NULL) FROM branches UNION ALL SELECT 'workshops', COUNT(*), SUM(deleted_at IS NOT NULL) FROM workshops UNION ALL SELECT 'custom_orders', COUNT(*), SUM(deleted_at IS NOT NULL) FROM custom_orders UNION ALL SELECT 'comments', COUNT(*), SUM(deleted_at IS NOT NULL) FROM comments UNION ALL SELECT 'messages', COUNT(*), 0 FROM messages UNION ALL SELECT 'notify', COUNT(*), SUM(deleted_at IS NOT NULL) FROM notify UNION ALL SELECT 'fav', COUNT(*), 0 FROM fav UNION ALL SELECT 'permission', COUNT(*), 0 FROM permission UNION ALL SELECT 'config_statics', COUNT(*), SUM(deleted_at IS NOT NULL) FROM config_statics; -- --------------------------------------------------------------------------- -- بخش ۲ — 🔴 یکتایی: آیا @unique در Prisma می‌شکند؟ -- مرجع: §۳ — مبدأ هیچ unique index ندارد -- هر عدد غیرصفر یعنی مهاجرت در میانه متوقف می‌شود -- --------------------------------------------------------------------------- SELECT '=== 2. UNIQUENESS (any nonzero = migration breaks) ===' AS section; -- چند *گروه* موبایل تکراری، و چند ردیف درگیرند SELECT 'dup_mobile_groups' AS metric, COUNT(*) AS n FROM ( SELECT mobile FROM customers WHERE deleted_at IS NULL AND mobile IS NOT NULL AND mobile <> '' GROUP BY mobile HAVING COUNT(*) > 1 ) d UNION ALL SELECT 'dup_mobile_rows', COALESCE(SUM(c),0) FROM ( SELECT COUNT(*) AS c FROM customers WHERE deleted_at IS NULL AND mobile IS NOT NULL AND mobile <> '' GROUP BY mobile HAVING COUNT(*) > 1 ) d2 UNION ALL SELECT 'dup_ncode_groups', COUNT(*) FROM ( SELECT ncode FROM customers WHERE deleted_at IS NULL AND ncode IS NOT NULL AND ncode <> '' GROUP BY ncode HAVING COUNT(*) > 1 ) d3 UNION ALL SELECT 'dup_ncode_rows', COALESCE(SUM(c),0) FROM ( SELECT COUNT(*) AS c FROM customers WHERE deleted_at IS NULL AND ncode IS NOT NULL AND ncode <> '' GROUP BY ncode HAVING COUNT(*) > 1 ) d4; -- تهی‌بودن: Prisma هر دو را NOT NULL می‌خواهد SELECT 'empty_mobile' AS metric, SUM(mobile IS NULL OR mobile = '') AS n FROM customers WHERE deleted_at IS NULL UNION ALL SELECT 'empty_ncode', SUM(ncode IS NULL OR ncode = '') FROM customers WHERE deleted_at IS NULL; -- تداخل شماره بین مشتری و کاربر ادمین (D21: «کارشناس برنده است») SELECT 'mobile_collision_customer_vs_user' AS metric, COUNT(*) AS n FROM customers c JOIN users u ON u.mobile = c.mobile WHERE c.deleted_at IS NULL AND u.deleted_at IS NULL AND c.mobile IS NOT NULL AND c.mobile <> ''; -- یکتایی در جدول users هم بررسی شود SELECT 'dup_user_mobile_groups' AS metric, COUNT(*) AS n FROM ( SELECT mobile FROM users WHERE deleted_at IS NULL AND mobile IS NOT NULL AND mobile <> '' GROUP BY mobile HAVING COUNT(*) > 1 ) d; -- --------------------------------------------------------------------------- -- بخش ۳ — 🔴 نام: آیا D0 درست است؟ -- مرجع: §۲ — fname/lname از قبل وجود دارند -- --------------------------------------------------------------------------- SELECT '=== 3. NAME PARTS (does `name` agree with fname+lname?) ===' AS section; SELECT SUM(fname IS NULL OR fname = '') AS empty_fname, SUM(lname IS NULL OR lname = '') AS empty_lname, SUM(name IS NULL OR name = '') AS empty_name, -- ردیف‌هایی که name با اجزا نمی‌خواند → trigger مقصد name را بازنویسی می‌کند SUM(TRIM(COALESCE(name,'')) <> TRIM(CONCAT(COALESCE(fname,''),' ',COALESCE(lname,'')))) AS name_mismatch, -- اجزا تهی ولی name پر → تنها حالتی که تجزیه لازم است SUM((fname IS NULL OR fname = '') AND (lname IS NULL OR lname = '') AND name IS NOT NULL AND name <> '') AS parts_empty_name_set FROM customers WHERE deleted_at IS NULL; -- --------------------------------------------------------------------------- -- بخش ۴ — enum ها: آیا مقداری بی‌نگاشت می‌ماند؟ -- مرجع: §۵.۳ (type) · §۵.۵ (verify) · §۷.۳ (sts) -- --------------------------------------------------------------------------- SELECT '=== 4. ENUMS ===' AS section; -- type: 1..5 نگاشت دارند. NULL یا خارج از بازه → بی‌صدا APP_SHOPKEEPER می‌شود SELECT 'customers.type' AS col, CAST(type AS CHAR) AS val, COUNT(*) AS n FROM customers WHERE deleted_at IS NULL GROUP BY type ORDER BY type; -- sts و active: دو سوییچ مجزا SELECT 'customers.sts' AS col, CAST(sts AS CHAR) AS val, COUNT(*) AS n FROM customers WHERE deleted_at IS NULL GROUP BY sts UNION ALL SELECT 'customers.active', CAST(active AS CHAR), COUNT(*) FROM customers WHERE deleted_at IS NULL GROUP BY active UNION ALL SELECT 'customers.is_archive', CAST(is_archive AS CHAR), COUNT(*) FROM customers WHERE deleted_at IS NULL GROUP BY is_archive; -- دو ستون تأیید → یک enum چهارحالته. حالت (0,1) در مقصد نماینده ندارد SELECT 'verify_step_combo' AS col, CONCAT(COALESCE(CAST(sts_verify_step_one AS CHAR),'NULL'), ',', COALESCE(CAST(sts_verify_step_two AS CHAR),'NULL')) AS val, COUNT(*) AS n FROM customers WHERE deleted_at IS NULL GROUP BY sts_verify_step_one, sts_verify_step_two; -- وضعیت سفارش عمده: 0..6 نگاشت دارند SELECT 'orders.sts' AS col, CAST(sts AS CHAR) AS val, COUNT(*) AS n FROM orders WHERE deleted_at IS NULL GROUP BY sts ORDER BY sts; -- اسناد مالی: type → LedgerSourceType (9,1,2,3,4)، confirmed حیاتی است (D24) SELECT 'cashes.type' AS col, CAST(type AS CHAR) AS val, COUNT(*) AS n FROM cashes WHERE deleted_at IS NULL GROUP BY type ORDER BY type; SELECT 'cashes.confirmed' AS col, CAST(confirmed AS CHAR) AS val, COUNT(*) AS n FROM cashes WHERE deleted_at IS NULL GROUP BY confirmed; -- --------------------------------------------------------------------------- -- بخش ۵ — 🔴 پول و وزن: مبنای تطبیق + تأیید اینکه آستانه لازم است -- مرجع: §۴ — ۱۱۰ ستون double، «صفر اختلاف» دست‌نیافتنی است -- --------------------------------------------------------------------------- SELECT '=== 5. MONEY / WEIGHT TOTALS (migration must match these) ===' AS section; SELECT COUNT(*) AS n_customers, CAST(SUM(remain) AS CHAR) AS sum_gold_debt, CAST(SUM(rial_remain) AS CHAR) AS sum_rial_debt, CAST(SUM(gram_level) AS CHAR) AS sum_credit_limit, SUM(remain <> 0) AS n_nonzero_gold, SUM(rial_remain <> 0) AS n_nonzero_rial, SUM(remain < 0) AS n_negative_gold, SUM(rial_remain < 0) AS n_negative_rial FROM customers WHERE deleted_at IS NULL; -- 🔴 شاهدِ مشکل double: چند ردیف مقداری دارند که در Decimal(14,4) جا نمی‌شود؟ -- عدد غیرصفر = گِردکردن رخ می‌دهد = «صفر اختلاف» ناممکن است SELECT 'gold_needs_rounding' AS metric, SUM(remain <> ROUND(remain, 4)) AS n FROM customers WHERE deleted_at IS NULL UNION ALL SELECT 'credit_needs_rounding', SUM(gram_level <> ROUND(gram_level, 4)) FROM customers WHERE deleted_at IS NULL UNION ALL SELECT 'order_weight_needs_rounding', SUM(w <> ROUND(w, 4)) FROM orders WHERE deleted_at IS NULL UNION ALL SELECT 'cash_amount_needs_rounding', SUM(remain <> ROUND(remain, 4)) FROM cashes WHERE deleted_at IS NULL; -- فقط اسناد تأییدشده در موجودی حساب می‌شوند (D24) SELECT 'confirmed_only' AS scope, CAST(SUM(remain) AS CHAR) AS sum_gold, CAST(SUM(rial_remain) AS CHAR) AS sum_rial, COUNT(*) AS n FROM cashes WHERE deleted_at IS NULL AND confirmed = 1; -- --------------------------------------------------------------------------- -- بخش ۶ — یکپارچگی ارجاعی (مبدأ صفر FK دارد — §۱۱) -- ارجاع یتیم = ردیفی که مهاجرت نمی‌تواند وصل کند -- --------------------------------------------------------------------------- SELECT '=== 6. ORPHAN REFERENCES (no FKs exist in source) ===' AS section; SELECT 'customer.city_id -> cities' AS ref, COUNT(*) AS orphans FROM customers c LEFT JOIN cities ci ON ci.id = c.city_id WHERE c.deleted_at IS NULL AND c.city_id IS NOT NULL AND ci.id IS NULL UNION ALL SELECT 'customer.reagent_id -> users', COUNT(*) FROM customers c LEFT JOIN users u ON u.id = c.reagent_id WHERE c.deleted_at IS NULL AND c.reagent_id IS NOT NULL AND u.id IS NULL UNION ALL SELECT 'customer.image_id -> image', COUNT(*) FROM customers c LEFT JOIN image i ON i.id = c.image_id WHERE c.deleted_at IS NULL AND c.image_id IS NOT NULL AND i.id IS NULL UNION ALL SELECT 'order.customer_id -> customers', COUNT(*) FROM orders o LEFT JOIN customers c ON c.id = o.customer_id WHERE o.deleted_at IS NULL AND o.customer_id IS NOT NULL AND c.id IS NULL UNION ALL SELECT 'order_items.order_id -> orders', COUNT(*) FROM order_items oi LEFT JOIN orders o ON o.id = oi.order_id WHERE oi.deleted_at IS NULL AND oi.order_id IS NOT NULL AND o.id IS NULL UNION ALL SELECT 'order_items.variant_id -> variant', COUNT(*) FROM order_items oi LEFT JOIN products_productvarient v ON v.id = oi.variant_id WHERE oi.deleted_at IS NULL AND oi.variant_id IS NOT NULL AND v.id IS NULL UNION ALL SELECT 'cashes.customer_id -> customers', COUNT(*) FROM cashes ca LEFT JOIN customers c ON c.id = ca.customer_id WHERE ca.deleted_at IS NULL AND ca.customer_id IS NOT NULL AND c.id IS NULL UNION ALL SELECT 'variant.product_id_id -> product', COUNT(*) FROM products_productvarient v LEFT JOIN product p ON p.id = v.product_id_id WHERE v.deleted_at IS NULL AND v.product_id_id IS NOT NULL AND p.id IS NULL UNION ALL SELECT 'fav.customer_id -> customers', COUNT(*) FROM fav f LEFT JOIN customers c ON c.id = f.customer_id WHERE c.id IS NULL UNION ALL SELECT 'fav.product_id -> product', COUNT(*) FROM fav f LEFT JOIN product p ON p.id = f.product_id WHERE p.id IS NULL; -- 🔴 fav هیچ PK ندارد → ردیف تکراری ممکن است (§۱۰) SELECT 'fav_duplicate_pairs' AS metric, COUNT(*) AS n FROM ( SELECT customer_id, product_id FROM fav GROUP BY customer_id, product_id HAVING COUNT(*) > 1 ) d; -- permission هم PK ندارد SELECT 'permission_duplicate_rows' AS metric, COUNT(*) AS n FROM ( SELECT rid, ut FROM permission GROUP BY rid, ut HAVING COUNT(*) > 1 ) d; -- --------------------------------------------------------------------------- -- بخش ۷ — ستون‌های مبهم: نوع واقعی داده -- مرجع: §۸ — ayar متنی یا عددی؟ تاریخ جلالی یا میلادی؟ -- --------------------------------------------------------------------------- SELECT '=== 7. AMBIGUOUS COLUMNS ===' AS section; -- ayar در مبدأ varchar(20) است ولی مقصد Int? — مقدار غیرعددی می‌شکند SELECT 'ayar_total' AS metric, COUNT(*) AS n FROM cashes WHERE deleted_at IS NULL AND ayar IS NOT NULL AND ayar <> '' UNION ALL SELECT 'ayar_non_numeric', COUNT(*) FROM cashes WHERE deleted_at IS NULL AND ayar IS NOT NULL AND ayar <> '' AND ayar NOT REGEXP '^[0-9]+$'; -- recive_date / proceed_at در مبدأ varchar هستند — جلالی یا میلادی؟ -- طول رشته و اینکه با 13 یا 14 شروع می‌شود، نوع تقویم را لو می‌دهد SELECT 'recive_date_set' AS metric, COUNT(*) AS n FROM cashes WHERE deleted_at IS NULL AND recive_date IS NOT NULL AND recive_date <> '' UNION ALL SELECT 'recive_date_starts_13x', COUNT(*) FROM cashes WHERE deleted_at IS NULL AND recive_date LIKE '13%' UNION ALL SELECT 'recive_date_starts_14x', COUNT(*) FROM cashes WHERE deleted_at IS NULL AND recive_date LIKE '14%' UNION ALL SELECT 'recive_date_starts_20x', COUNT(*) FROM cashes WHERE deleted_at IS NULL AND recive_date LIKE '20%' UNION ALL SELECT 'proceed_at_set', COUNT(*) FROM cashes WHERE deleted_at IS NULL AND proceed_at IS NOT NULL AND proceed_at <> ''; -- ستون‌های داده‌ی غیرقابل‌بازسازی (§۵.۲ گروه ج) — چند ردیف واقعاً مقدار دارند؟ SELECT 'has_birth_date' AS metric, COUNT(*) AS n FROM customers WHERE deleted_at IS NULL AND birth_date IS NOT NULL UNION ALL SELECT 'has_marriage_date', COUNT(*) FROM customers WHERE deleted_at IS NULL AND marriage_date IS NOT NULL UNION ALL SELECT 'has_token_mobile', COUNT(*) FROM customers WHERE deleted_at IS NULL AND token_mobile IS NOT NULL AND token_mobile <> '' UNION ALL SELECT 'has_new_city', COUNT(*) FROM customers WHERE deleted_at IS NULL AND new_city IS NOT NULL AND new_city <> ''; -- فرمت رمز عبور: bcrypt با $2y$ شروع می‌شود. اگر نه، hash دیگری است SELECT 'pw_bcrypt' AS metric, SUM(password LIKE '$2%') AS n FROM customers WHERE deleted_at IS NULL UNION ALL SELECT 'pw_other', SUM(password IS NOT NULL AND password <> '' AND password NOT LIKE '$2%') FROM customers WHERE deleted_at IS NULL UNION ALL SELECT 'pw_empty', SUM(password IS NULL OR password = '') FROM customers WHERE deleted_at IS NULL; -- --------------------------------------------------------------------------- -- بخش ۸ — جداول پشتیبان: آیا ردیفی دارند که `customers` ندارد؟ -- مرجع: §۱.۱ — قبل از نادیده‌گرفتن باید شمرده شوند -- --------------------------------------------------------------------------- SELECT '=== 8. BACKUP TABLES (safe to ignore?) ===' AS section; SELECT 'customers' AS tbl, COUNT(*) AS n, CAST(MAX(id) AS CHAR) AS max_id FROM customers UNION ALL SELECT 'customers_copy1', COUNT(*), CAST(MAX(id) AS CHAR) FROM customers_copy1 UNION ALL SELECT 'customers_copy2', COUNT(*), CAST(MAX(id) AS CHAR) FROM customers_copy2 UNION ALL SELECT 'customers_test', COUNT(*), CAST(MAX(id) AS CHAR) FROM customers_test; -- شناسه‌هایی که در کپی هستند ولی در جدول اصلی نیستند → یعنی ردیف حذف شده SELECT 'ids_only_in_copy1' AS metric, COUNT(*) AS n FROM customers_copy1 k LEFT JOIN customers c ON c.id = k.id WHERE c.id IS NULL UNION ALL SELECT 'ids_only_in_copy2', COUNT(*) FROM customers_copy2 k LEFT JOIN customers c ON c.id = k.id WHERE c.id IS NULL; -- --------------------------------------------------------------------------- -- بخش ۹ — تک‌فروشی (به تأخیر افتاده، §۷.۲) — فقط حجم، برای برنامه‌ریزی آینده -- --------------------------------------------------------------------------- SELECT '=== 9. RETAIL (deferred — size only) ===' AS section; SELECT 'orders_orders' AS tbl, COUNT(*) AS live, SUM(deleted_at IS NOT NULL) AS soft_deleted FROM orders_orders UNION ALL SELECT 'orders_orderitem', COUNT(*), SUM(deleted_at IS NOT NULL) FROM orders_orderitem UNION ALL SELECT 'login', COUNT(*), SUM(deleted_at IS NOT NULL) FROM login; -- 🔴 sts=7 در خرده «تحویل مشتری» است ولی در عمده «کنسل». چند ردیف درگیرند؟ SELECT 'retail_sts' AS col, CAST(sts AS CHAR) AS val, COUNT(*) AS n FROM orders_orders WHERE deleted_at IS NULL GROUP BY sts ORDER BY sts; SELECT '=== END ===' AS section;