Files
boursecu_server/database/sql/generate_merge_sql.py
T

414 lines
15 KiB
Python
Raw Normal View History

2026-06-06 12:55:07 +03:30
#!/usr/bin/env python3
"""
Generate client merge SQL (settings + const) from:
2026-06-06 16:05:40 +03:30
- reference catalog template (abshode_rahimi_merge_goldali_catalog.sql UPDATE blocks)
2026-06-06 12:55:07 +03:30
- client JSON: { "settings": [...], "const": [...] }
Usage:
python generate_merge_sql.py abaeigold_client.json abaeigold
"""
from __future__ import annotations
import json
import re
import sys
from pathlib import Path
SQL_DIR = Path(__file__).resolve().parent
CATALOG_SQL = SQL_DIR / "abshode_rahimi_merge_goldali_catalog.sql"
CONST_CATALOG: list[dict] = [
{"key": "limited_price", "title_fa": "سقف قیمت محدود", "default": ""},
{"key": "is_gateway_open", "title_fa": "باز بودن درگاه پرداخت", "default": ""},
{"key": "liman", "title_fa": "توکن لیمان", "default": ""},
{"key": "header_banner", "title_fa": "بنر بالای صفحه", "default": ""},
{"key": "app_closed", "title_fa": "بسته بودن اپ", "default": "false"},
{"key": "main_product", "title_fa": "محصول آبشده پیش‌فرض", "default": "13"},
2026-06-06 16:05:40 +03:30
{"key": "gram", "title_fa": "گرم ثبت‌نام", "default": "0"},
2026-06-06 12:55:07 +03:30
{"key": "first_customer_wallet_value", "title_fa": "حداقل اولین خرید", "default": ""},
{"key": "jibit_counter", "title_fa": "شمارنده جیبیت", "default": "0"},
{"key": "price", "title_fa": "لیست قیمت TGJU (JSON)", "default": "[]"},
]
# Legacy client keys → catalog key (value only)
SETTINGS_ALIASES = {
"liman_token": "const:liman",
}
# Keys dropped from client settings (logs, legacy goldir in settings if any)
SETTINGS_SKIP = {
"montzar_order_response",
"montzar_last_product_log",
"liman_token",
2026-06-06 16:05:40 +03:30
"is_requaire_referral",
2026-06-06 12:55:07 +03:30
}
2026-06-06 16:05:40 +03:30
# metadata overrides for catalog keys (e.g. editable in admin panel)
SETTINGS_META_OVERRIDES: dict[str, dict] = {
"referral_id": {
"can_edit": 1,
"description": "required = اجباری؛ خالی = اختیاری|required,",
},
}
LEGACY_SETTINGS_DELETE = ("is_requaire_referral",)
def legacy_referral_migration_sql() -> list[str]:
"""اگر is_requaire_referral قدیمی روشن بود → referral_id = required"""
return [
"-- legacy is_requaire_referral → referral_id",
"UPDATE `settings` AS r",
"SET",
" r.`value` = 'required',",
" r.`is_active` = 1,",
" r.`updated_at` = NOW()",
"WHERE r.`key` = 'referral_id'",
" AND EXISTS (",
" SELECT 1",
" FROM (",
" SELECT `value`, `is_active`",
" FROM `settings`",
" WHERE `key` = 'is_requaire_referral'",
" LIMIT 1",
" ) AS leg",
" WHERE leg.`is_active` = 1",
" AND LOWER(TRIM(COALESCE(leg.`value`, ''))) IN ('true', '1', 'yes')",
" )",
" AND TRIM(COALESCE(r.`value`, '')) <> 'required';",
"",
]
2026-06-06 12:55:07 +03:30
CONST_SKIP_PREFIXES = ("goldir",)
CONST_SKIP_KEYS = {"log", "last_order_log"}
def normalize_key(key: str) -> str:
return key.strip().replace("\r", "").replace("\n", "")
def sql_str(value: str | None) -> str:
if value is None:
return "NULL"
return "'" + str(value).replace("\\", "\\\\").replace("'", "''") + "'"
def _parse_sql_field(line: str) -> str | None:
line = line.strip().rstrip(",")
if line.endswith("= NULL"):
return None
eq = line.index("=")
raw = line[eq + 1 :].strip()
if raw.startswith("'") and raw.endswith("'"):
return raw[1:-1].replace("''", "'")
return raw
def parse_catalog_updates(text: str) -> dict[str, dict]:
catalog: dict[str, dict] = {}
blocks = re.split(r"(?=UPDATE `settings` SET)", text)
for block in blocks:
if not block.startswith("UPDATE `settings` SET"):
continue
key_m = re.search(
r"`updated_at` = NOW\(\)\s+WHERE `key` = '([^']+)';", block
)
if not key_m:
continue
key = key_m.group(1)
fields: dict[str, str | None] = {}
for line in block.splitlines():
line = line.strip()
for col in ("title_fa", "can_edit", "type", "description"):
if line.startswith(f"`{col}` ="):
fields[col] = _parse_sql_field(line)
catalog[key] = {
"title_fa": fields.get("title_fa") or "",
"can_edit": int(fields.get("can_edit") or 0),
"type": fields.get("type"),
"description": fields.get("description"),
}
# fallback: ensure full key list from DELETE block
delete_m = re.search(
r"DELETE FROM `settings`\s+WHERE `key` NOT IN \(\s*(.*?)\s*\);",
text,
re.S,
)
if delete_m:
for key in re.findall(r"'([^']+)'", delete_m.group(1)):
catalog.setdefault(
key,
{"title_fa": "", "can_edit": 1, "type": "text", "description": None},
)
catalog.pop("price_method", None)
return catalog
def load_client_rows(path: Path) -> tuple[dict[str, str | None], dict[str, str | None]]:
data = json.loads(path.read_text(encoding="utf-8"))
settings: dict[str, str | None] = {}
const: dict[str, str | None] = {}
for row in data.get("settings", []):
key = normalize_key(row["key"])
if key in SETTINGS_SKIP:
continue
val = row.get("value")
if val is not None and isinstance(val, str):
val = val.strip()
settings[key] = val if val != "" else None
for row in data.get("const", []):
key = normalize_key(row["key"])
if key in CONST_SKIP_KEYS or key.startswith(CONST_SKIP_PREFIXES):
continue
val = row.get("value")
if val is not None and isinstance(val, str):
val = val.strip()
const[key] = val if val != "" else None
# liman_token in settings → const.liman
if "liman_token" in {normalize_key(r["key"]) for r in data.get("settings", [])}:
for row in data.get("settings", []):
if normalize_key(row["key"]) == "liman_token" and row.get("value"):
const["liman"] = row["value"]
# montzar_price → price_provider
mp = settings.get("montzar_price")
if mp and str(mp).lower() in ("true", "1"):
settings["price_provider"] = "montzar"
elif settings.get("main_product_price") and str(settings.get("main_product_price")).lower() == "true":
settings["price_provider"] = "main_product"
elif not settings.get("price_provider"):
settings.setdefault("price_provider", "montzar" if mp and str(mp).lower() in ("true", "1") else "")
# main_product from const if missing in settings
if const.get("main_product") and not settings.get("main_product"):
settings["main_product"] = const["main_product"]
return settings, const
def is_active_value(value: str | None) -> int:
if value is None or value == "":
return 0
return 1
def generate_settings_sql(
client_name: str,
client_name_fa: str,
catalog: dict[str, dict],
client_settings: dict[str, str | None],
) -> str:
lines: list[str] = [
"-- =============================================================================",
2026-06-06 16:05:40 +03:30
f"-- {client_name_fa} — ادغام settings با کاتالوگ واحد",
2026-06-06 12:55:07 +03:30
"--",
f"-- منبع مقادیر: {client_name}_client.json",
f"-- قالب کلیدها: goldali ({len(catalog)} کلید)",
"--",
"-- قوانین:",
"-- • کلید مشترک → value از کلاینت، metadata از کاتالوگ",
"-- • کلید کاتالوگ بدون مقدار در کلاینت → value=''، is_active=0",
2026-06-06 16:05:40 +03:30
"-- • هر setting با value پر → is_active=1 (الزام ConfigService)",
2026-06-06 12:55:07 +03:30
"-- • کلیدهای اضافی کلاینت → DELETE",
"--",
f"-- قبل از اجرا: USE دیتابیس {client_name_fa} + بکاپ",
"-- =============================================================================",
"",
"START TRANSACTION;",
"",
"SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;",
"",
"ALTER TABLE `settings`",
" MODIFY COLUMN `description` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,",
" MODIFY COLUMN `title_fa` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,",
" MODIFY COLUMN `type` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL;",
"",
"-- 1) INSERT",
"",
]
catalog_keys = sorted(catalog.keys())
for key in catalog_keys:
2026-06-06 16:05:40 +03:30
meta = dict(catalog[key])
if key in SETTINGS_META_OVERRIDES:
meta.update(SETTINGS_META_OVERRIDES[key])
2026-06-06 12:55:07 +03:30
val = client_settings.get(key)
if val is None:
val = ""
active = is_active_value(val if val != "" else None)
lines.append(
"INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)"
)
lines.append(
f"SELECT {sql_str(key)}, {sql_str(val)}, {sql_str(meta['title_fa'])}, "
f"{meta['can_edit']}, {active}, {sql_str(meta['description'])}, {sql_str(meta['type'])}"
)
lines.append(f"WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = {sql_str(key)} LIMIT 1);")
lines.extend(["", "-- 2) UPDATE", ""])
for key in catalog_keys:
2026-06-06 16:05:40 +03:30
meta = dict(catalog[key])
if key in SETTINGS_META_OVERRIDES:
meta.update(SETTINGS_META_OVERRIDES[key])
2026-06-06 12:55:07 +03:30
val = client_settings.get(key)
if val is None:
val = ""
active = is_active_value(val if val != "" else None)
lines.append("UPDATE `settings` SET")
lines.append(f" `title_fa` = {sql_str(meta['title_fa'])},")
lines.append(f" `can_edit` = {meta['can_edit']},")
lines.append(f" `type` = {sql_str(meta['type'])},")
lines.append(f" `description` = {sql_str(meta['description'])},")
lines.append(f" `value` = {sql_str(val)},")
lines.append(f" `is_active` = {active},")
lines.append(" `updated_at` = NOW()")
lines.append(f"WHERE `key` = {sql_str(key)};")
lines.extend([
"",
"-- 3) logo_login normalize",
"",
"DELETE FROM `settings`",
"WHERE `key` LIKE 'logo_login%' AND `key` <> 'logo_login'",
" AND EXISTS (SELECT 1 FROM (SELECT `id`, `key` FROM `settings`) AS `s` WHERE `s`.`key` = 'logo_login');",
"",
"UPDATE `settings` SET",
" `key` = 'logo_login',",
" `value` = TRIM(REPLACE(REPLACE(`value`, CHAR(13), ''), CHAR(10), '')),",
" `updated_at` = NOW()",
"WHERE `key` LIKE 'logo_login%' AND `key` <> 'logo_login';",
"",
2026-06-06 16:05:40 +03:30
"-- 3b) legacy is_requaire_referral → referral_id (قبل از DELETE)",
"",
])
lines.extend(legacy_referral_migration_sql())
lines.extend([
2026-06-06 12:55:07 +03:30
"-- 4) DELETE extras",
"",
"DELETE FROM `settings`",
"WHERE `key` NOT IN (",
])
for i, key in enumerate(catalog_keys):
comma = "," if i < len(catalog_keys) - 1 else ""
lines.append(f" {sql_str(key)}{comma}")
lines.append(");")
lines.extend([
"",
"DELETE FROM `settings` WHERE `key` = 'price_method';",
"",
2026-06-06 16:05:40 +03:30
"-- 4b) حذف کلیدهای legacy",
"",
])
for legacy_key in LEGACY_SETTINGS_DELETE:
lines.append(f"DELETE FROM `settings` WHERE `key` = {sql_str(legacy_key)};")
lines.extend([
"",
"-- 5) is_active — ConfigService فقط وقتی is_active=1 و value پر باشد می‌خواند",
"",
"UPDATE `settings` SET",
" `is_active` = 1,",
" `updated_at` = NOW()",
"WHERE TRIM(COALESCE(`value`, '')) <> '';",
"",
2026-06-06 12:55:07 +03:30
"COMMIT;",
"",
])
return "\n".join(lines)
def generate_const_sql(
client_name: str,
client_name_fa: str,
client_const: dict[str, str | None],
) -> str:
lines: list[str] = [
"-- =============================================================================",
2026-06-06 16:05:40 +03:30
f"-- {client_name_fa} — ادغام const با کاتالوگ واحد",
2026-06-06 12:55:07 +03:30
"--",
f"-- منبع مقادیر: {client_name}_client.json",
"-- قالب: 10 کلید goldali",
"-- =============================================================================",
"",
"START TRANSACTION;",
"",
"SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;",
"",
"ALTER TABLE `const`",
" MODIFY COLUMN `title_fa` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL;",
"",
"-- 1) INSERT",
"",
]
for item in CONST_CATALOG:
key = item["key"]
val = client_const.get(key)
if val is None:
val = item["default"]
lines.append("INSERT INTO `const` (`key`, `value`, `title_fa`)")
lines.append(
f"SELECT {sql_str(key)}, {sql_str(val)}, {sql_str(item['title_fa'])}"
)
lines.append(f"WHERE NOT EXISTS (SELECT 1 FROM `const` WHERE `key` = {sql_str(key)} LIMIT 1);")
lines.extend(["", "-- 2) UPDATE", ""])
for item in CONST_CATALOG:
key = item["key"]
val = client_const.get(key)
if val is None:
val = item["default"]
lines.append("UPDATE `const` SET")
lines.append(f" `title_fa` = {sql_str(item['title_fa'])},")
lines.append(f" `value` = {sql_str(val)},")
lines.append(" `updated_at` = NOW()")
lines.append(f"WHERE `key` = {sql_str(key)};")
allowed = [item["key"] for item in CONST_CATALOG]
lines.extend(["", "-- 3) DELETE extras", "", "DELETE FROM `const`", "WHERE `key` NOT IN ("])
for i, key in enumerate(allowed):
comma = "," if i < len(allowed) - 1 else ""
lines.append(f" {sql_str(key)}{comma}")
lines.extend([");", "", "COMMIT;", ""])
return "\n".join(lines)
def main() -> None:
if len(sys.argv) < 3:
print(__doc__)
sys.exit(1)
client_json = Path(sys.argv[1])
client_slug = sys.argv[2]
client_name_fa = sys.argv[3] if len(sys.argv) > 3 else client_slug
catalog_text = CATALOG_SQL.read_text(encoding="utf-8")
catalog = parse_catalog_updates(catalog_text)
if not catalog:
print("ERROR: could not parse catalog from", CATALOG_SQL)
sys.exit(1)
client_settings, client_const = load_client_rows(client_json)
settings_sql = generate_settings_sql(client_slug, client_name_fa, catalog, client_settings)
const_sql = generate_const_sql(client_slug, client_name_fa, client_const)
out_settings = SQL_DIR / f"{client_slug}_merge_goldali_catalog.sql"
out_const = SQL_DIR / f"{client_slug}_merge_goldali_const.sql"
out_settings.write_text(settings_sql, encoding="utf-8")
out_const.write_text(const_sql, encoding="utf-8")
print(f"Wrote {out_settings} ({len(catalog)} keys)")
print(f"Wrote {out_const} ({len(CONST_CATALOG)} keys)")
if __name__ == "__main__":
main()