#!/usr/bin/env python3 """ USLaP Schema Consolidation — 185 tables → ~80 بِسْمِ اللَّهِ الرَّحْمَٰنِ الرَّحِيمِ Phase 1: RU mirrors → EN parents (13 eliminated) Phase 2: Body 46 → 5 tables (41 eliminated) Phase 3: Bitig 15 → 10 (5 eliminated) Phase 4: Foundation/Mechanism consolidation Usage: python3 consolidate_schema_v5.py [--dry-run] """ import sqlite3 import json import os import sys from datetime import datetime BASE = os.path.dirname(os.path.abspath(__file__)) DB = os.path.join(BASE, "uslap_database_v3.db") DRY_RUN = '--dry-run' in sys.argv def run(): conn = sqlite3.connect(DB) conn.execute("PRAGMA foreign_keys = OFF") conn.execute("PRAGMA journal_mode = WAL") print("=" * 70) print(f"USLaP Schema Consolidation {'(DRY RUN)' if DRY_RUN else ''}") print(f"Started: {datetime.now().isoformat()}") print("=" * 70) # Save triggers print("\n[PREP] Saving triggers...") all_triggers = conn.execute("SELECT name, sql FROM sqlite_master WHERE type='trigger'").fetchall() print(f" {len(all_triggers)} triggers saved") # Drop ALL triggers for migration (NOT in dry run) if not DRY_RUN: for tname, _ in all_triggers: conn.execute(f'DROP TRIGGER IF EXISTS "{tname}"') conn.commit() print(" All triggers dropped") else: print(" (dry run — triggers preserved)") # Drop existing compatibility views that conflict existing_views = set() for view_name in ['a1_записи', 'a1_entries']: vtype = conn.execute("SELECT type FROM sqlite_master WHERE name=?", (view_name,)).fetchone() if vtype and vtype[0] == 'view': existing_views.add(view_name) if not DRY_RUN: conn.execute(f'DROP VIEW IF EXISTS "{view_name}"') print(f" {'Would drop' if DRY_RUN else 'Dropped'} view: {view_name}") # ================================================================ # PHASE 1: RU MIRRORS → EN PARENTS # ================================================================ print("\n" + "=" * 70) print("PHASE 1: RU Mirrors → EN Parents") print("=" * 70) # Add lang column to parent tables (skip views) lang_tables = ['entries', 'names_of_allah', 'a3_quran_refs', 'a4_derivatives', 'a5_cross_refs', 'a6_country_names'] for tbl in lang_tables: ttype = conn.execute("SELECT type FROM sqlite_master WHERE name=?", (tbl,)).fetchone() if not ttype or ttype[0] == 'view': print(f" Skipping {tbl} (view or missing)") continue cols = [r[1] for r in conn.execute(f"PRAGMA table_info('{tbl}')").fetchall()] if 'lang' not in cols: if not DRY_RUN: conn.execute(f'ALTER TABLE "{tbl}" ADD COLUMN lang TEXT DEFAULT "EN"') conn.execute(f'UPDATE "{tbl}" SET lang="EN" WHERE lang IS NULL') print(f" {tbl}: lang column {'would be added' if DRY_RUN else 'added'}") else: print(f" {tbl}: lang column exists") if not DRY_RUN: conn.commit() # Merge a1_записи → entries (SKIP if it's already a view) a1_type = conn.execute("SELECT type FROM sqlite_master WHERE name='a1_записи'").fetchone() if a1_type and a1_type[0] == 'view': print(" a1_записи is already a VIEW (data in entries) — skipping merge") ru_rows = [] else: ru_cols = {r[1]: r for r in conn.execute("PRAGMA table_info('a1_записи')").fetchall()} col_map = { 'запись_id': 'entry_id', 'балл': 'score', 'рус_термин': 'ru_term', 'ар_слово': 'ar_word', 'корень_id': 'root_id', 'корневые_буквы': 'root_letters', 'коранич_значение': 'qur_meaning', 'паттерн': 'pattern', 'тип_инверсии': 'inversion_type', 'сеть_id': 'network_id', 'имя_аллаха_id': 'allah_name_id', 'фонетическая_цепь': 'phonetic_chain', 'исходная_форма': 'source_form', 'основание': 'foundation_refs', } ru_rows = conn.execute('SELECT * FROM "a1_записи"').fetchall() ru_col_names = [r[1] for r in conn.execute("PRAGMA table_info('a1_записи')").fetchall()] migrated = 0 for row in ru_rows: row_dict = dict(zip(ru_col_names, row)) # Map to EN column names en_vals = {} for ru_name, value in row_dict.items(): if ru_name.startswith('quf_'): en_vals[ru_name] = value elif ru_name in col_map: en_vals[col_map[ru_name]] = value en_vals['lang'] = 'RU' en_vals['en_term'] = row_dict.get('рус_термин', '') # RU term goes in en_term for now # Insert cols_str = ', '.join(f'"{k}"' for k in en_vals.keys()) placeholders = ', '.join('?' * len(en_vals)) if not DRY_RUN: try: conn.execute(f'INSERT INTO entries ({cols_str}) VALUES ({placeholders})', list(en_vals.values())) migrated += 1 except Exception as e: if migrated == 0: print(f" WARN: {e}") conn.commit() print(f" a1_записи → entries: {migrated}/{len(ru_rows)} rows migrated") # Merge a2_имена_аллаха → names_of_allah a2_map = {'имя_id': 'allah_id', 'арабское_имя': 'arabic_name', 'транслитерация': 'transliteration', 'значение': 'meaning', 'коран_ссылка': 'qur_ref', 'записи_id': 'entry_ids', 'корень_id': 'root_id'} _migrate_ru_table(conn, 'a2_имена_аллаха', 'names_of_allah', a2_map) # Merge a3 a3_map = {'ссылка_id': 'ref_id', 'сура': 'surah', 'аят': 'ayah', 'арабский_текст': 'arabic_text', 'кв_id': 'qv_id', 'слой': 'layer_ref', 'записи_id': 'entry_ids', 'сеть_id': 'network_id', 'релевантность': 'relevance'} _migrate_ru_table(conn, 'a3_коранические_ссылки', 'a3_quran_refs', a3_map) # Merge a4 a4_map = {'произв_id': 'deriv_id', 'запись_id': 'entry_id', 'рус_термин': 'en_term', 'производное': 'derivative', 'тип_связи': 'link_type'} _migrate_ru_table(conn, 'a4_производные', 'a4_derivatives', a4_map) # Merge a5 a5_map = {'перекр_id': 'xref_id', 'от_id': 'from_id', 'к_id': 'to_id', 'тип_связи': 'link_type', 'описание': 'description', 'слой': 'layer_ref'} _migrate_ru_table(conn, 'a5_перекрёстные_ссылки', 'a5_cross_refs', a5_map) # Merge a6 a6_map = {'страна_id': 'country_id', 'название_страны': 'country_name', 'корень_id': 'root_id', 'слово_al': 'al_word', 'корень_al': 'al_root', 'коранич_значение': 'qur_meaning', 'фонетическая_цепь': 'phonetic_chain', 'основание_названия': 'naming_basis', 'запись_ids': 'entry_ids', 'примечания': 'notes'} _migrate_ru_table(conn, 'a6_названия_стран', 'a6_country_names', a6_map) # Foundation RU mirrors → add lang column to EN tables for ru_tbl, en_tbl in [ ('f1_один_источник_два_коридора', 'f1_two_originals'), ('f2_нисходящий_поток', 'f2_script_downstream'), ('f2_критерии_деградации', 'f2_decay_criteria'), ('f3_модель_застывших_стадий', 'f3_frozen_stage_model'), ('f4_градиент_деградации', 'f4_decay_gradient'), ('f5_хронология_уничтожения', 'f5_destruction_timeline'), ('f6_рукописные_свидетельства', 'f6_manuscript_evidence'), ('f7_направление_потока', 'f7_direction_of_flow'), ]: _merge_generic_ru(conn, ru_tbl, en_tbl) # Mechanism RU → new mechanism_data table conn.execute(""" CREATE TABLE IF NOT EXISTS mechanism_data ( mech_id INTEGER PRIMARY KEY AUTOINCREMENT, layer TEXT NOT NULL, lang TEXT DEFAULT 'RU', data TEXT, quf_q TEXT, quf_u TEXT, quf_f TEXT, quf_pass TEXT, quf_date TEXT, quf_token TEXT ) """) for ru_tbl, layer in [ ('m1_фонетические_сдвиги', 'm1'), ('m2_паттерны_обнаружения', 'm2'), ('m3_учёные', 'm3'), ('m4_сети', 'm4'), ('m5_коранич_верификация', 'm5'), ]: try: rows = conn.execute(f'SELECT * FROM "{ru_tbl}"').fetchall() cols = [r[1] for r in conn.execute(f"PRAGMA table_info('{ru_tbl}')").fetchall()] for row in rows: row_dict = {cols[i]: row[i] for i in range(len(cols)) if not cols[i].startswith('quf_')} if not DRY_RUN: conn.execute("INSERT INTO mechanism_data (layer, lang, data) VALUES (?, 'RU', ?)", (layer, json.dumps(row_dict, ensure_ascii=False))) print(f" {ru_tbl} → mechanism_data: {len(rows)} rows") except Exception as e: print(f" WARN {ru_tbl}: {e}") conn.commit() # Retire all RU tables ru_retire = [ 'a1_записи', 'a2_имена_аллаха', 'a3_коранические_ссылки', 'a4_производные', 'a5_перекрёстные_ссылки', 'a6_названия_стран', 'f1_один_источник_два_коридора', 'f2_нисходящий_поток', 'f2_критерии_деградации', 'f3_модель_застывших_стадий', 'f4_градиент_деградации', 'f5_хронология_уничтожения', 'f6_рукописные_свидетельства', 'f7_направление_потока', 'm1_фонетические_сдвиги', 'm2_паттерны_обнаружения', 'm3_учёные', 'm4_сети', 'm5_коранич_верификация', ] retired = 0 for tbl in ru_retire: try: if not DRY_RUN: conn.execute(f'ALTER TABLE "{tbl}" RENAME TO "_retired_consol_{tbl}"') retired += 1 except: pass conn.commit() print(f" Retired {retired} RU tables") # Create compatibility views if not DRY_RUN: conn.execute('CREATE VIEW IF NOT EXISTS "a1_записи" AS SELECT * FROM entries WHERE lang="RU"') print(" Compatibility view created: a1_записи") # ================================================================ # PHASE 2: BODY → 5 TABLES # ================================================================ print("\n" + "=" * 70) print("PHASE 2: Body 46 → 5 tables") print("=" * 70) # Create body_data conn.execute(""" CREATE TABLE IF NOT EXISTS body_data ( body_id INTEGER PRIMARY KEY AUTOINCREMENT, subsystem TEXT NOT NULL, subtable TEXT NOT NULL, heptad INTEGER, category TEXT, arabic TEXT, transliteration TEXT, english TEXT, description TEXT, root_letters TEXT, aa_root_id TEXT, quranic_ref TEXT, score REAL, status TEXT, specific_data TEXT, quf_q TEXT, quf_u TEXT, quf_f TEXT, quf_pass TEXT, quf_date TEXT, quf_token TEXT ) """) # Create body_cross_refs conn.execute(""" CREATE TABLE IF NOT EXISTS body_cross_refs_unified ( xref_id INTEGER PRIMARY KEY AUTOINCREMENT, subsystem TEXT NOT NULL, subtable TEXT NOT NULL, source_ref TEXT, target_ref TEXT, relationship TEXT, notes TEXT, quf_q TEXT, quf_u TEXT, quf_f TEXT, quf_pass TEXT, quf_date TEXT, quf_token TEXT ) """) # Create body_prayer_map conn.execute(""" CREATE TABLE IF NOT EXISTS body_prayer_map ( prayer_id INTEGER PRIMARY KEY AUTOINCREMENT, subsystem TEXT NOT NULL, subtable TEXT NOT NULL, prayer TEXT, state TEXT, arabic TEXT, quranic_ref TEXT, specific_data TEXT, quf_q TEXT, quf_u TEXT, quf_f TEXT, quf_pass TEXT, quf_date TEXT, quf_token TEXT ) """) conn.commit() # Migrate body data tables COMMON_COLS = ['category', 'arabic', 'transliteration', 'english', 'description', 'root_letters', 'aa_root_id', 'quranic_ref', 'score', 'status'] body_data_tables = [] body_xref_tables = [] body_prayer_tables = [] for (tbl,) in conn.execute("""SELECT name FROM sqlite_master WHERE type='table' AND (name LIKE 'body_%' OR name LIKE 'sensory_%' OR name LIKE 'nutrition_%' OR name LIKE 'healing_%' OR name LIKE 'nafs_%' OR name LIKE 'lifecycle_%' OR name LIKE 'heart_%' OR name LIKE 'prayer_%' OR name LIKE 'emotional_%' OR name LIKE 'social_%' OR name LIKE 'death_%' OR name LIKE 'perception_%' OR name LIKE 'pelvis_%' OR name LIKE 'spirit_%' OR name LIKE 'qalb_%' OR name LIKE 'transition_%' OR name LIKE 'therapy_%' OR name LIKE 'food_%' OR name LIKE 'agricultural_%') AND name NOT LIKE '_retired%' AND name != 'body_data' AND name != 'body_cross_refs_unified' AND name != 'body_prayer_map' AND name != 'body_heptad_meta' AND name != 'body_extraction_intel' """).fetchall(): if 'cross_ref' in tbl or 'edges' in tbl: body_xref_tables.append(tbl) elif 'prayer' in tbl and ('map' in tbl or 'state' in tbl or 'transition' in tbl): body_prayer_tables.append(tbl) else: body_data_tables.append(tbl) # Determine subsystem from table name def get_subsystem(tbl): for prefix in ['body_', 'sensory_', 'nutrition_', 'healing_', 'nafs_', 'lifecycle_', 'heart_', 'emotional_', 'social_', 'death_', 'perception_', 'pelvis_', 'spirit_', 'qalb_', 'transition_', 'therapy_', 'food_', 'agricultural_']: if tbl.startswith(prefix): return prefix.rstrip('_') return tbl.split('_')[0] # Migrate data tables total_migrated = 0 for tbl in body_data_tables: try: cols = [r[1] for r in conn.execute(f"PRAGMA table_info('{tbl}')").fetchall()] rows = conn.execute(f'SELECT * FROM "{tbl}"').fetchall() subsystem = get_subsystem(tbl) for row in rows: row_dict = dict(zip(cols, row)) # Extract common columns common_vals = {c: row_dict.get(c) for c in COMMON_COLS} # Everything else goes to specific_data specific = {k: v for k, v in row_dict.items() if k not in COMMON_COLS and not k.startswith('quf_') and k not in ('rowid_pk',) and v is not None} if not DRY_RUN: conn.execute(""" INSERT INTO body_data (subsystem, subtable, category, arabic, transliteration, english, description, root_letters, aa_root_id, quranic_ref, score, status, specific_data) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, (subsystem, tbl, common_vals.get('category'), common_vals.get('arabic'), common_vals.get('transliteration'), common_vals.get('english'), common_vals.get('description'), common_vals.get('root_letters'), common_vals.get('aa_root_id'), common_vals.get('quranic_ref'), common_vals.get('score'), common_vals.get('status'), json.dumps(specific, ensure_ascii=False) if specific else None)) total_migrated += 1 except Exception as e: print(f" WARN {tbl}: {e}") conn.commit() print(f" body_data: {total_migrated} rows from {len(body_data_tables)} tables") # Migrate cross-ref tables xref_migrated = 0 for tbl in body_xref_tables: try: cols = [r[1] for r in conn.execute(f"PRAGMA table_info('{tbl}')").fetchall()] rows = conn.execute(f'SELECT * FROM "{tbl}"').fetchall() for row in rows: row_dict = dict(zip(cols, row)) specific = {k: v for k, v in row_dict.items() if not k.startswith('quf_') and k not in ('rowid_pk',) and v is not None} if not DRY_RUN: conn.execute(""" INSERT INTO body_cross_refs_unified (subsystem, subtable, source_ref, target_ref, notes) VALUES (?, ?, ?, ?, ?) """, (get_subsystem(tbl), tbl, json.dumps({k: v for k, v in specific.items() if 'source' in k or 'from' in k}, ensure_ascii=False), json.dumps({k: v for k, v in specific.items() if 'target' in k or 'to' in k}, ensure_ascii=False), json.dumps(specific, ensure_ascii=False))) xref_migrated += 1 except Exception as e: print(f" WARN xref {tbl}: {e}") conn.commit() print(f" body_cross_refs_unified: {xref_migrated} rows from {len(body_xref_tables)} tables") # Migrate prayer tables prayer_migrated = 0 for tbl in body_prayer_tables: try: cols = [r[1] for r in conn.execute(f"PRAGMA table_info('{tbl}')").fetchall()] rows = conn.execute(f'SELECT * FROM "{tbl}"').fetchall() for row in rows: row_dict = dict(zip(cols, row)) specific = {k: v for k, v in row_dict.items() if not k.startswith('quf_') and k not in ('rowid_pk',) and v is not None} if not DRY_RUN: conn.execute(""" INSERT INTO body_prayer_map (subsystem, subtable, specific_data) VALUES (?, ?, ?) """, (get_subsystem(tbl), tbl, json.dumps(specific, ensure_ascii=False))) prayer_migrated += 1 except Exception as e: print(f" WARN prayer {tbl}: {e}") conn.commit() print(f" body_prayer_map: {prayer_migrated} rows from {len(body_prayer_tables)} tables") # Retire body tables body_retired = 0 for tbl in body_data_tables + body_xref_tables + body_prayer_tables: try: if not DRY_RUN: conn.execute(f'ALTER TABLE "{tbl}" RENAME TO "_retired_body_{tbl}"') body_retired += 1 except: pass conn.commit() print(f" Retired {body_retired} body tables") # ================================================================ # PHASE 3: BITIG CLEANUP # ================================================================ print("\n" + "=" * 70) print("PHASE 3: Bitig Cleanup") print("=" * 70) # Drop empty tables for tbl in ['bitig_bridge_xref', 'bitig_dispersal_edges', 'bitig_sibling_propagation']: try: cnt = conn.execute(f'SELECT COUNT(*) FROM "{tbl}"').fetchone()[0] if cnt == 0 and not DRY_RUN: conn.execute(f'DROP TABLE "{tbl}"') print(f" Dropped {tbl} (empty)") except: pass # Merge corrections + investigation try: conn.execute(""" CREATE TABLE IF NOT EXISTS bitig_corrections_merged ( corr_id TEXT, source_table TEXT, word TEXT, issue TEXT, resolution TEXT, evidence TEXT, status TEXT, next_step TEXT, quf_q TEXT, quf_u TEXT, quf_f TEXT, quf_pass TEXT, quf_date TEXT, quf_token TEXT ) """) # Copy corrections for row in conn.execute('SELECT * FROM bitig_corrections').fetchall(): cols = [r[1] for r in conn.execute("PRAGMA table_info('bitig_corrections')").fetchall()] d = dict(zip(cols, row)) if not DRY_RUN: conn.execute("INSERT INTO bitig_corrections_merged (corr_id, source_table, issue, resolution, evidence, status) VALUES (?, 'corrections', ?, ?, ?, ?)", (d.get('corr_id'), d.get('what_was_wrong'), d.get('corrected_to'), d.get('evidence'), d.get('status'))) # Copy investigations for row in conn.execute('SELECT * FROM bitig_investigation').fetchall(): cols = [r[1] for r in conn.execute("PRAGMA table_info('bitig_investigation')").fetchall()] d = dict(zip(cols, row)) if not DRY_RUN: conn.execute("INSERT INTO bitig_corrections_merged (corr_id, source_table, word, issue, status, next_step) VALUES (?, 'investigation', ?, ?, ?, ?)", (d.get('inv_id'), d.get('word'), d.get('issue'), d.get('status'), d.get('next_step'))) conn.commit() if not DRY_RUN: conn.execute('ALTER TABLE bitig_corrections RENAME TO "_retired_bitig_corrections"') conn.execute('ALTER TABLE bitig_investigation RENAME TO "_retired_bitig_investigation"') conn.execute('ALTER TABLE bitig_corrections_merged RENAME TO bitig_corrections') print(f" Merged bitig_corrections + bitig_investigation → bitig_corrections") except Exception as e: print(f" WARN merge corrections: {e}") # Merge intelligence_summary + operator_profiles try: conn.execute(""" CREATE TABLE IF NOT EXISTS bitig_intelligence_merged ( intel_id TEXT, source_table TEXT, category TEXT, content TEXT, period TEXT, dp_codes TEXT, method TEXT, specific_data TEXT, quf_q TEXT, quf_u TEXT, quf_f TEXT, quf_pass TEXT, quf_date TEXT, quf_token TEXT ) """) for row in conn.execute('SELECT * FROM bitig_intelligence_summary').fetchall(): cols = [r[1] for r in conn.execute("PRAGMA table_info('bitig_intelligence_summary')").fetchall()] d = dict(zip(cols, row)) if not DRY_RUN: conn.execute("INSERT INTO bitig_intelligence_merged (intel_id, source_table, category, dp_codes, specific_data) VALUES (?, 'summary', ?, ?, ?)", (d.get('intel_id'), d.get('category'), d.get('dp_code'), json.dumps({k:v for k,v in d.items() if not k.startswith('quf_')}, ensure_ascii=False))) for row in conn.execute('SELECT * FROM bitig_operator_profiles').fetchall(): cols = [r[1] for r in conn.execute("PRAGMA table_info('bitig_operator_profiles')").fetchall()] d = dict(zip(cols, row)) if not DRY_RUN: conn.execute("INSERT INTO bitig_intelligence_merged (intel_id, source_table, category, period, method, specific_data) VALUES (?, 'profiles', ?, ?, ?, ?)", (d.get('profile_id'), d.get('role'), d.get('period'), d.get('method'), json.dumps({k:v for k,v in d.items() if not k.startswith('quf_')}, ensure_ascii=False))) conn.commit() if not DRY_RUN: conn.execute('ALTER TABLE bitig_intelligence_summary RENAME TO "_retired_bitig_intel_summary"') conn.execute('ALTER TABLE bitig_operator_profiles RENAME TO "_retired_bitig_op_profiles"') conn.execute('ALTER TABLE bitig_intelligence_merged RENAME TO bitig_intelligence') print(f" Merged bitig_intelligence_summary + bitig_operator_profiles → bitig_intelligence") except Exception as e: print(f" WARN merge intel: {e}") conn.commit() # ================================================================ # PHASE 4: FOUNDATION/MECHANISM # ================================================================ print("\n" + "=" * 70) print("PHASE 4: Foundation/Mechanism") print("=" * 70) # Add lang column to foundation tables for tbl in ['f1_two_originals', 'f2_script_downstream', 'f2_decay_criteria', 'f3_frozen_stage_model', 'f4_decay_gradient', 'f5_destruction_timeline', 'f6_manuscript_evidence', 'f7_direction_of_flow']: cols = [r[1] for r in conn.execute(f"PRAGMA table_info('{tbl}')").fetchall()] if 'lang' not in cols: try: conn.execute(f'ALTER TABLE "{tbl}" ADD COLUMN lang TEXT DEFAULT "EN"') conn.execute(f'UPDATE "{tbl}" SET lang="EN" WHERE lang IS NULL') except: pass conn.commit() print(" Foundation tables: lang column added") # ================================================================ # RECREATE TRIGGERS (on surviving tables only) # ================================================================ print("\n" + "=" * 70) print("RESTORE: Recreating triggers") print("=" * 70) # Get surviving table names surviving = set(r[0] for r in conn.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE '_retired%'" ).fetchall()) trg_ok = 0 trg_skip = 0 for tname, tsql in all_triggers: if not tsql: continue # Check if the trigger's table still exists tbl_match = None for tbl in surviving: if f'ON [{tbl}]' in tsql or f'ON "{tbl}"' in tsql: tbl_match = tbl break if tbl_match and not DRY_RUN: try: conn.execute(tsql) trg_ok += 1 except: trg_skip += 1 else: trg_skip += 1 conn.commit() print(f" Recreated: {trg_ok}, Skipped (retired tables): {trg_skip}") # ================================================================ # VERIFY # ================================================================ print("\n" + "=" * 70) print("VERIFICATION") print("=" * 70) tables_after = conn.execute( "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_retired%' AND name NOT LIKE 'entries_fts%'" ).fetchone()[0] retired_count = conn.execute( "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name LIKE '_retired%'" ).fetchone()[0] triggers_after = conn.execute("SELECT COUNT(*) FROM sqlite_master WHERE type='trigger'").fetchone()[0] # Data verification if not DRY_RUN: ru_in_entries = conn.execute("SELECT COUNT(*) FROM entries WHERE lang='RU'").fetchone()[0] body_data_count = conn.execute("SELECT COUNT(*) FROM body_data").fetchone()[0] body_subsystems = conn.execute("SELECT COUNT(DISTINCT subsystem) FROM body_data").fetchone()[0] integrity = conn.execute("PRAGMA integrity_check").fetchone()[0] else: ru_in_entries = body_data_count = body_subsystems = 0 integrity = 'DRY RUN' db_size = os.path.getsize(DB) / 1024 / 1024 print(f" Active tables: {tables_after}") print(f" Retired tables: {retired_count}") print(f" Triggers: {triggers_after}") print(f" entries WHERE lang='RU': {ru_in_entries}") print(f" body_data rows: {body_data_count}") print(f" body_data subsystems: {body_subsystems}") print(f" Integrity: {integrity}") print(f" DB size: {db_size:.1f} MB") print(f"\n{'=' * 70}") print(f"CONSOLIDATION {'COMPLETE' if not DRY_RUN else 'DRY RUN COMPLETE'}") print(f"Finished: {datetime.now().isoformat()}") print(f"{'=' * 70}") conn.close() def _migrate_ru_table(conn, ru_tbl, en_tbl, col_map): """Generic RU → EN migration with column mapping.""" try: ru_cols = [r[1] for r in conn.execute(f"PRAGMA table_info('{ru_tbl}')").fetchall()] en_cols = [r[1] for r in conn.execute(f"PRAGMA table_info('{en_tbl}')").fetchall()] rows = conn.execute(f'SELECT * FROM "{ru_tbl}"').fetchall() # Add lang column if missing if 'lang' not in en_cols: conn.execute(f'ALTER TABLE "{en_tbl}" ADD COLUMN lang TEXT DEFAULT "EN"') conn.execute(f'UPDATE "{en_tbl}" SET lang="EN" WHERE lang IS NULL') migrated = 0 for row in rows: row_dict = dict(zip(ru_cols, row)) en_vals = {'lang': 'RU'} for ru_name, value in row_dict.items(): if ru_name.startswith('quf_'): en_vals[ru_name] = value elif ru_name in col_map and col_map[ru_name] in en_cols: en_vals[col_map[ru_name]] = value if en_vals and not DRY_RUN: cols_str = ', '.join(f'"{k}"' for k in en_vals.keys()) placeholders = ', '.join('?' * len(en_vals)) try: conn.execute(f'INSERT INTO "{en_tbl}" ({cols_str}) VALUES ({placeholders})', list(en_vals.values())) migrated += 1 except: pass conn.commit() print(f" {ru_tbl} → {en_tbl}: {migrated}/{len(rows)} rows") except Exception as e: print(f" WARN {ru_tbl}: {e}") def _merge_generic_ru(conn, ru_tbl, en_tbl): """Merge generic RU table into EN parent using JSON data column.""" try: ru_cols = [r[1] for r in conn.execute(f"PRAGMA table_info('{ru_tbl}')").fetchall()] en_cols = [r[1] for r in conn.execute(f"PRAGMA table_info('{en_tbl}')").fetchall()] rows = conn.execute(f'SELECT * FROM "{ru_tbl}"').fetchall() if 'lang' not in en_cols: conn.execute(f'ALTER TABLE "{en_tbl}" ADD COLUMN lang TEXT DEFAULT "EN"') conn.execute(f'UPDATE "{en_tbl}" SET lang="EN" WHERE lang IS NULL') for row in rows: row_dict = {ru_cols[i]: row[i] for i in range(len(ru_cols)) if not ru_cols[i].startswith('quf_')} # Try to insert mapped values; store unmappable as JSON in notes if not DRY_RUN: if 'notes' not in en_cols: conn.execute(f'ALTER TABLE "{en_tbl}" ADD COLUMN notes TEXT') en_cols.append('notes') conn.execute(f'INSERT INTO "{en_tbl}" (lang, notes) VALUES ("RU", ?)', (json.dumps(row_dict, ensure_ascii=False),)) conn.commit() print(f" {ru_tbl} → {en_tbl}: {len(rows)} rows (as JSON)") except Exception as e: print(f" WARN {ru_tbl}: {e}") if __name__ == "__main__": run()