#!/usr/bin/env python3 """ JTL Shop MariaDB Index-Optimierung ================================== Erstellt fehlende Indizes und aktiviert FULLTEXT-Suche auf allen JTL-Shops. Features: - Backup aller Sucheinstellungen vor Änderungen - Erstellt 5 Index-Typen auf allen betroffenen Shops - Aktiviert FULLTEXT-Suche - Generiert Rollback-Skript für Notfälle Autor: Claude Datum: 2025-12-12 """ import subprocess import json import sys from datetime import datetime from pathlib import Path # Konfiguration TIMESTAMP = datetime.now().strftime("%Y%m%d_%H%M%S") BACKUP_FILE = f"/root/jtl_search_backup_{TIMESTAMP}.json" ROLLBACK_FILE = f"/root/jtl_search_rollback_{TIMESTAMP}.py" LOG_FILE = f"/root/jtl_index_log_{TIMESTAMP}.txt" # Index-Definitionen INDEXES = [ { "name": "idx_kLieferscheinPos", "table": "tlieferscheinposinfo", "columns": "kLieferscheinPos", "check_column": "kLieferscheinPos" }, { "name": "idx_kLieferschein", "table": "tlieferscheinpos", "columns": "kLieferschein", "check_column": "kLieferschein" }, { "name": "idx_kKampagneDef_kKey", "table": "tkampagnevorgang", "columns": "kKampagneDef, kKey", "check_column": "kKampagneDef" }, { "name": "idx_kXSellArtikel", "table": "txsellkauf", "columns": "kXSellArtikel", "check_column": "kXSellArtikel" } ] FULLTEXT_INDEX = { "name": "idx_tartikel_fulltext", "table": "tartikel", "columns": "cName, cSeo, cSuchbegriffe, cArtNr, cKurzBeschreibung, cBeschreibung, cBarcode, cISBN, cHAN" } class Logger: def __init__(self, log_file): self.log_file = log_file self.messages = [] def log(self, message, level="INFO"): timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") formatted = f"[{timestamp}] [{level}] {message}" print(formatted) self.messages.append(formatted) def save(self): with open(self.log_file, 'w') as f: f.write("\n".join(self.messages)) print(f"\nLog gespeichert: {self.log_file}") def run_mysql(query, database=None): """Führt MySQL-Query aus und gibt Ergebnis zurück.""" cmd = ["mysql", "-N", "-e", query] if database: cmd.extend(["-D", database]) result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: return None, result.stderr return result.stdout.strip(), None def get_all_shops(): """Ermittelt alle JTL-Shop-Datenbanken.""" query = """ SELECT DISTINCT table_schema FROM information_schema.tables WHERE table_name = 'tartikel' AND table_schema NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys') ORDER BY table_schema; """ result, err = run_mysql(query) if err: return [] return [line.strip() for line in result.split('\n') if line.strip()] def check_index_exists(shop, table, check_column): """Prüft ob ein Index für die Spalte bereits existiert.""" query = f""" SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = '{shop}' AND table_name = '{table}' AND column_name = '{check_column}' AND index_name != 'PRIMARY'; """ result, _ = run_mysql(query) return result and int(result) > 0 def check_fulltext_exists(shop): """Prüft ob FULLTEXT-Index bereits existiert.""" query = f""" SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = '{shop}' AND table_name = 'tartikel' AND index_type = 'FULLTEXT'; """ result, _ = run_mysql(query) return result and int(result) > 0 def check_table_exists(shop, table): """Prüft ob Tabelle existiert.""" query = f""" SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = '{shop}' AND table_name = '{table}'; """ result, _ = run_mysql(query) return result and int(result) > 0 def get_search_setting(shop): """Liest aktuelle suche_fulltext Einstellung.""" query = f"SELECT cWert FROM `{shop}`.teinstellungen WHERE cName = 'suche_fulltext' LIMIT 1;" result, _ = run_mysql(query) return result if result else "NICHT_GESETZT" def set_search_setting(shop, value): """Setzt suche_fulltext Einstellung.""" query = f"UPDATE `{shop}`.teinstellungen SET cWert = '{value}' WHERE cName = 'suche_fulltext';" _, err = run_mysql(query) return err is None def create_index(shop, index_name, table, columns, is_fulltext=False): """Erstellt einen Index.""" if is_fulltext: query = f"CREATE FULLTEXT INDEX `{index_name}` ON `{shop}`.`{table}` ({columns});" else: query = f"CREATE INDEX `{index_name}` ON `{shop}`.`{table}` ({columns});" _, err = run_mysql(query) return err def generate_rollback_script(backup_data): """Generiert ein Rollback-Skript.""" script = f'''#!/usr/bin/env python3 """ JTL Search Settings Rollback Script ==================================== Generiert am: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} Dieses Skript stellt die ursprünglichen suche_fulltext Einstellungen wieder her. """ import subprocess BACKUP_DATA = {json.dumps(backup_data, indent=4)} def run_mysql(query): result = subprocess.run(["mysql", "-N", "-e", query], capture_output=True, text=True) return result.returncode == 0 def main(): print("=== JTL Search Settings Rollback ===") print(f"Stelle {{len(BACKUP_DATA)}} Einstellungen wieder her...\\n") success = 0 failed = 0 for shop, value in BACKUP_DATA.items(): if value == "NICHT_GESETZT": print(f" [SKIP] {{shop}}: Keine Einstellung vorhanden") continue query = f"UPDATE `{{shop}}`.teinstellungen SET cWert = '{{value}}' WHERE cName = 'suche_fulltext';" if run_mysql(query): print(f" [OK] {{shop}}: suche_fulltext = '{{value}}'") success += 1 else: print(f" [FEHLER] {{shop}}") failed += 1 print(f"\\n=== Rollback abgeschlossen ===") print(f"Erfolgreich: {{success}}") print(f"Fehlgeschlagen: {{failed}}") if __name__ == "__main__": main() ''' with open(ROLLBACK_FILE, 'w') as f: f.write(script) # Ausführbar machen subprocess.run(["chmod", "+x", ROLLBACK_FILE]) def main(): logger = Logger(LOG_FILE) print("=" * 60) print("JTL Shop MariaDB Index-Optimierung") print("=" * 60) print() # Phase 0: Shops ermitteln logger.log("Ermittle alle JTL-Shop-Datenbanken...") shops = get_all_shops() logger.log(f"Gefunden: {len(shops)} Shops") print() if not shops: logger.log("Keine Shops gefunden. Abbruch.", "ERROR") return # Phase 1: Backup der Sucheinstellungen print("=" * 60) print("PHASE 1: Backup der Sucheinstellungen") print("=" * 60) backup_data = {} for shop in shops: setting = get_search_setting(shop) backup_data[shop] = setting logger.log(f" {shop}: suche_fulltext = '{setting}'") # Backup speichern with open(BACKUP_FILE, 'w') as f: json.dump(backup_data, f, indent=2) logger.log(f"Backup gespeichert: {BACKUP_FILE}") # Rollback-Skript generieren generate_rollback_script(backup_data) logger.log(f"Rollback-Skript generiert: {ROLLBACK_FILE}") print() # Phase 2: Standard-Indizes erstellen print("=" * 60) print("PHASE 2: Standard-Indizes erstellen") print("=" * 60) index_stats = {"created": 0, "skipped": 0, "failed": 0} for idx in INDEXES: logger.log(f"\n--- Index: {idx['name']} auf {idx['table']} ---") for shop in shops: # Prüfen ob Tabelle existiert if not check_table_exists(shop, idx['table']): continue # Prüfen ob Index existiert if check_index_exists(shop, idx['table'], idx['check_column']): logger.log(f" [SKIP] {shop}: Index existiert bereits") index_stats["skipped"] += 1 continue # Index erstellen err = create_index(shop, idx['name'], idx['table'], idx['columns']) if err: logger.log(f" [FEHLER] {shop}: {err}", "ERROR") index_stats["failed"] += 1 else: logger.log(f" [OK] {shop}: Index erstellt") index_stats["created"] += 1 print() # Phase 3: FULLTEXT-Index erstellen print("=" * 60) print("PHASE 3: FULLTEXT-Index erstellen") print("=" * 60) fulltext_stats = {"created": 0, "skipped": 0, "failed": 0} for shop in shops: # Prüfen ob FULLTEXT bereits existiert if check_fulltext_exists(shop): logger.log(f" [SKIP] {shop}: FULLTEXT existiert bereits") fulltext_stats["skipped"] += 1 continue # FULLTEXT-Index erstellen logger.log(f" [WORK] {shop}: Erstelle FULLTEXT-Index (kann dauern)...") err = create_index( shop, FULLTEXT_INDEX['name'], FULLTEXT_INDEX['table'], FULLTEXT_INDEX['columns'], is_fulltext=True ) if err: logger.log(f" [FEHLER] {shop}: {err}", "ERROR") fulltext_stats["failed"] += 1 else: logger.log(f" [OK] {shop}: FULLTEXT-Index erstellt") fulltext_stats["created"] += 1 print() # Phase 4: FULLTEXT-Suche aktivieren print("=" * 60) print("PHASE 4: FULLTEXT-Suche aktivieren") print("=" * 60) search_stats = {"updated": 0, "skipped": 0, "failed": 0} for shop in shops: current = backup_data.get(shop, "NICHT_GESETZT") if current == "Y": logger.log(f" [SKIP] {shop}: Bereits aktiviert") search_stats["skipped"] += 1 continue if current == "NICHT_GESETZT": logger.log(f" [SKIP] {shop}: Keine Einstellung vorhanden") search_stats["skipped"] += 1 continue # Einstellung ändern if set_search_setting(shop, "Y"): logger.log(f" [OK] {shop}: suche_fulltext = 'Y' (war: '{current}')") search_stats["updated"] += 1 else: logger.log(f" [FEHLER] {shop}", "ERROR") search_stats["failed"] += 1 print() # Zusammenfassung print("=" * 60) print("ZUSAMMENFASSUNG") print("=" * 60) print() print("Standard-Indizes:") print(f" Erstellt: {index_stats['created']}") print(f" Übersprungen: {index_stats['skipped']}") print(f" Fehlgeschlagen: {index_stats['failed']}") print() print("FULLTEXT-Index:") print(f" Erstellt: {fulltext_stats['created']}") print(f" Übersprungen: {fulltext_stats['skipped']}") print(f" Fehlgeschlagen: {fulltext_stats['failed']}") print() print("Sucheinstellung (suche_fulltext = 'Y'):") print(f" Aktualisiert: {search_stats['updated']}") print(f" Übersprungen: {search_stats['skipped']}") print(f" Fehlgeschlagen: {search_stats['failed']}") print() print("=" * 60) print("WICHTIGE DATEIEN") print("=" * 60) print(f" Backup: {BACKUP_FILE}") print(f" Rollback: {ROLLBACK_FILE}") print(f" Log: {LOG_FILE}") print() print("Bei Problemen Rollback ausführen mit:") print(f" python3 {ROLLBACK_FILE}") print() # Log speichern logger.save() if __name__ == "__main__": main()