Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """ | |
| USLaP V4 Schema Hardening (Updated with Domain QUF) | |
| بِسْمِ اللَّهِ الرَّحْمَٰنِ الرَّحِيمِ | |
| Resolves all SWOT threats and weaknesses in one pass: | |
| T4: schema_version table | |
| T5: PK on excel_data_consolidated | |
| O1/W2: root_id FK on bitig_a1_entries | |
| O2/W3: FK on a4_derivatives.entry_id | |
| O3/W5: Indexes on entries + core tables | |
| O4/W4: UNIQUE constraints | |
| O5/W6: CHECK constraints | |
| O6: Retire xlsx_* tables | |
| O7/W1: PK on remaining NO_PK tables | |
| NEW: QUF indexes for fast filtering | |
| NEW: Fix broken a1_entries view | |
| NEW: Schema health check (validates all 157 tables) | |
| Usage: | |
| python3 Code_files/harden_v4_schema.py [--check-only] | |
| """ | |
| import sqlite3 | |
| import os | |
| from datetime import datetime | |
| BASE = os.path.dirname(os.path.abspath(__file__)) | |
| DB = os.path.join(BASE, "uslap_database_v3.db") | |
| def run(): | |
| conn = sqlite3.connect(DB) | |
| conn.execute("PRAGMA foreign_keys = OFF") # OFF during schema changes | |
| conn.execute("PRAGMA journal_mode = WAL") | |
| print("=" * 70) | |
| print("USLaP V4 Schema Hardening") | |
| print(f"Started: {datetime.now().isoformat()}") | |
| print("=" * 70) | |
| # ================================================================ | |
| # T4: schema_version table | |
| # ================================================================ | |
| print("\n[T4] Adding schema_version table...") | |
| conn.execute(""" | |
| CREATE TABLE IF NOT EXISTS schema_version ( | |
| version INTEGER NOT NULL, | |
| description TEXT NOT NULL, | |
| applied_at TEXT DEFAULT (datetime('now')), | |
| applied_by TEXT DEFAULT 'migration' | |
| ) | |
| """) | |
| conn.execute(""" | |
| INSERT INTO schema_version (version, description) VALUES | |
| (4, 'V4: PK/FK enforcement, contamination triggers, schema hardening') | |
| """) | |
| conn.commit() | |
| print(" schema_version created, V4 recorded") | |
| # ================================================================ | |
| # Save and drop ALL triggers + views (needed for table rebuilds) | |
| # ================================================================ | |
| print("\n[PREP] Saving triggers and views...") | |
| all_triggers = conn.execute( | |
| "SELECT name, sql FROM sqlite_master WHERE type='trigger'" | |
| ).fetchall() | |
| all_views = conn.execute( | |
| "SELECT name, sql FROM sqlite_master WHERE type='view'" | |
| ).fetchall() | |
| print(f" {len(all_triggers)} triggers, {len(all_views)} views") | |
| for vname, _ in all_views: | |
| conn.execute(f'DROP VIEW IF EXISTS "{vname}"') | |
| for tname, _ in all_triggers: | |
| conn.execute(f'DROP TRIGGER IF EXISTS "{tname}"') | |
| conn.commit() | |
| print(" All dropped temporarily") | |
| # ================================================================ | |
| # O1/W2: Add root_id column to bitig_a1_entries + populate | |
| # ================================================================ | |
| print("\n[O1/W2] Adding root_id to bitig_a1_entries...") | |
| # Check if column exists | |
| cols = [c[1] for c in conn.execute("PRAGMA table_info(bitig_a1_entries)")] | |
| if "root_id" not in cols: | |
| conn.execute("ALTER TABLE bitig_a1_entries ADD COLUMN root_id TEXT REFERENCES roots(root_id)") | |
| conn.commit() | |
| print(" Column added") | |
| else: | |
| print(" Column already exists") | |
| # Auto-populate from root_letters match | |
| updated = conn.execute(""" | |
| UPDATE bitig_a1_entries SET root_id = ( | |
| SELECT r.root_id FROM roots r WHERE r.root_letters = bitig_a1_entries.root_letters | |
| ) | |
| WHERE root_letters IS NOT NULL AND root_letters != '' | |
| AND root_id IS NULL | |
| """).rowcount | |
| conn.commit() | |
| print(f" Auto-mapped {updated} bitig entries to roots") | |
| # ================================================================ | |
| # O2/W3: Fix a4_derivatives orphan, add FK | |
| # ================================================================ | |
| print("\n[O2/W3] Fixing a4_derivatives orphan + adding FK...") | |
| # The 1 orphan has entry_id='MUQARNAS' (text, not int) — NULL it | |
| conn.execute(""" | |
| UPDATE a4_derivatives SET entry_id = NULL | |
| WHERE entry_id IS NOT NULL | |
| AND CAST(entry_id AS INTEGER) = 0 | |
| AND entry_id != '0' | |
| """) | |
| conn.commit() | |
| # Rebuild a4_derivatives with FK | |
| deriv_cols = conn.execute("PRAGMA table_info(a4_derivatives)").fetchall() | |
| col_names = [c[1] for c in deriv_cols] | |
| col_list = ", ".join(f'"{c}"' for c in col_names) | |
| conn.execute("DROP TABLE IF EXISTS _harden_a4") | |
| ddl_parts = [] | |
| for cid, name, ctype, notnull, dflt, pk in deriv_cols: | |
| if name == "deriv_id": | |
| ddl_parts.append('"deriv_id" TEXT PRIMARY KEY') | |
| elif name == "entry_id": | |
| ddl_parts.append('"entry_id" INTEGER REFERENCES entries(entry_id)') | |
| else: | |
| part = f'"{name}" {ctype or "TEXT"}' | |
| if dflt is not None: | |
| part += f" DEFAULT {dflt}" | |
| ddl_parts.append(part) | |
| conn.execute(f'CREATE TABLE _harden_a4 ({", ".join(ddl_parts)})') | |
| conn.execute(f'INSERT INTO _harden_a4 ({col_list}) SELECT {col_list} FROM a4_derivatives') | |
| before = conn.execute("SELECT count(*) FROM a4_derivatives").fetchone()[0] | |
| after = conn.execute("SELECT count(*) FROM _harden_a4").fetchone()[0] | |
| conn.execute("DROP TABLE a4_derivatives") | |
| conn.execute("ALTER TABLE _harden_a4 RENAME TO a4_derivatives") | |
| conn.commit() | |
| print(f" a4_derivatives: {before}→{after}, FK on entry_id→entries") | |
| # ================================================================ | |
| # O3/W5: Add indexes on core tables | |
| # ================================================================ | |
| print("\n[O3/W5] Adding indexes...") | |
| indexes = [ | |
| ("idx_entries_en_term", "entries", "en_term"), | |
| ("idx_entries_ru_term", "entries", "ru_term"), | |
| ("idx_entries_fa_term", "entries", "fa_term"), | |
| ("idx_entries_ar_word", "entries", "ar_word"), | |
| ("idx_entries_root_id", "entries", "root_id"), | |
| ("idx_entries_score", "entries", "score"), | |
| ("idx_entries_network_id", "entries", "network_id"), | |
| ("idx_bitig_root_letters", "bitig_a1_entries", "root_letters"), | |
| ("idx_bitig_root_id", "bitig_a1_entries", "root_id"), | |
| ("idx_bitig_orig2_term", "bitig_a1_entries", "orig2_term"), | |
| ("idx_eu_term", "european_a1_entries", "term"), | |
| ("idx_eu_lang", "european_a1_entries", "lang"), | |
| ("idx_eu_root_id", "european_a1_entries", "root_id"), | |
| ("idx_lat_term", "latin_a1_entries", "lat_term"), | |
| ("idx_lat_root_id", "latin_a1_entries", "root_id"), | |
| ("idx_a4_entry_id", "a4_derivatives", "entry_id"), | |
| ("idx_a5_from_id", "a5_cross_refs", "from_id"), | |
| ("idx_a5_to_id", "a5_cross_refs", "to_id"), | |
| ("idx_qwr_root", "quran_word_roots", "root"), | |
| ("idx_qwr_surah_ayah", "quran_word_roots", "surah, ayah"), | |
| ("idx_qv_root", "qv_translation_register", "ROOT"), | |
| ("idx_roots_letters", "roots", "root_letters"), | |
| ] | |
| idx_ok = 0 | |
| for idx_name, table, cols in indexes: | |
| try: | |
| conn.execute(f'CREATE INDEX IF NOT EXISTS "{idx_name}" ON "{table}" ({cols})') | |
| idx_ok += 1 | |
| except Exception as e: | |
| print(f" WARN: {idx_name}: {e}") | |
| conn.commit() | |
| print(f" {idx_ok}/{len(indexes)} indexes created") | |
| # ================================================================ | |
| # O4/W4: UNIQUE constraints (via unique indexes) | |
| # ================================================================ | |
| print("\n[O4/W4] Adding UNIQUE constraints...") | |
| uniques = [ | |
| ("uq_entries_en_root", "entries", "en_term, root_id"), | |
| ("uq_bitig_orig2", "bitig_a1_entries", "orig2_term, root_letters"), | |
| ("uq_eu_lang_term", "european_a1_entries", "lang, term"), | |
| ("uq_lat_term", "latin_a1_entries", "lat_term"), | |
| ("uq_roots_letters", "roots", "root_letters"), | |
| ] | |
| uq_ok = 0 | |
| for uq_name, table, cols in uniques: | |
| try: | |
| conn.execute(f'CREATE UNIQUE INDEX IF NOT EXISTS "{uq_name}" ON "{table}" ({cols})') | |
| uq_ok += 1 | |
| except Exception as e: | |
| print(f" WARN: {uq_name}: {e}") | |
| conn.commit() | |
| print(f" {uq_ok}/{len(uniques)} unique constraints created") | |
| # ================================================================ | |
| # O5/W6: CHECK constraints (via trigger-based validation) | |
| # SQLite can't ALTER TABLE ADD CHECK, so we use BEFORE INSERT triggers | |
| # ================================================================ | |
| print("\n[O5/W6] Adding CHECK-equivalent validation triggers...") | |
| check_triggers = [ | |
| ("trg_check_entries_score", "entries", "BEFORE INSERT", | |
| "SELECT RAISE(ABORT, 'score must be 0-10') WHERE NEW.score IS NOT NULL AND (NEW.score < 0 OR NEW.score > 10)"), | |
| ("trg_check_entries_score_upd", "entries", "BEFORE UPDATE", | |
| "SELECT RAISE(ABORT, 'score must be 0-10') WHERE NEW.score IS NOT NULL AND (NEW.score < 0 OR NEW.score > 10)"), | |
| ("trg_check_uz_orig_type", "uzbek_vocabulary", "BEFORE INSERT", | |
| "SELECT RAISE(ABORT, 'orig_type must be ORIG1 or ORIG2') WHERE NEW.orig_type NOT IN ('ORIG1', 'ORIG2')"), | |
| ("trg_check_uz_orig_type_upd", "uzbek_vocabulary", "BEFORE UPDATE", | |
| "SELECT RAISE(ABORT, 'orig_type must be ORIG1 or ORIG2') WHERE NEW.orig_type NOT IN ('ORIG1', 'ORIG2')"), | |
| ("trg_check_wg_status", "write_gate", "BEFORE INSERT", | |
| "SELECT RAISE(ABORT, 'status must be ANALYSED/WRITTEN/VERIFIED/CLOSED') WHERE NEW.status NOT IN ('ANALYSED','WRITTEN','VERIFIED','CLOSED')"), | |
| ("trg_check_wg_status_upd", "write_gate", "BEFORE UPDATE", | |
| "SELECT RAISE(ABORT, 'status must be ANALYSED/WRITTEN/VERIFIED/CLOSED') WHERE NEW.status NOT IN ('ANALYSED','WRITTEN','VERIFIED','CLOSED')"), | |
| ] | |
| chk_ok = 0 | |
| for tname, table, event, body in check_triggers: | |
| try: | |
| conn.execute(f'CREATE TRIGGER IF NOT EXISTS "{tname}" {event} ON "{table}" FOR EACH ROW BEGIN {body}; END') | |
| chk_ok += 1 | |
| except Exception as e: | |
| print(f" WARN: {tname}: {e}") | |
| conn.commit() | |
| print(f" {chk_ok}/{len(check_triggers)} check triggers created") | |
| # ================================================================ | |
| # O6: Retire xlsx_* tables (rename to _retired_*) | |
| # ================================================================ | |
| print("\n[O6] Retiring xlsx_* tables...") | |
| xlsx_tables = [r[0] for r in conn.execute( | |
| "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'xlsx_%'" | |
| ).fetchall()] | |
| retired = 0 | |
| for t in xlsx_tables: | |
| new_name = f"_retired_{t}" | |
| try: | |
| conn.execute(f'ALTER TABLE "{t}" RENAME TO "{new_name}"') | |
| retired += 1 | |
| except Exception as e: | |
| print(f" WARN: {t}: {e}") | |
| conn.commit() | |
| print(f" {retired}/{len(xlsx_tables)} tables retired") | |
| # ================================================================ | |
| # O7/W1: Add PK to remaining NO_PK tables via rowid | |
| # For foundation/mechanism/bitig_support tables: add integer PK | |
| # ================================================================ | |
| print("\n[O7/W1] Adding PK to remaining NO_PK tables...") | |
| no_pk_tables = [r[0] for r in conn.execute(""" | |
| SELECT m.name FROM sqlite_master m | |
| WHERE type='table' AND name NOT LIKE 'sqlite_%' | |
| AND name NOT LIKE '_retired_%' | |
| AND name NOT LIKE 'entries_fts%' | |
| AND NOT EXISTS(SELECT 1 FROM pragma_table_info(m.name) WHERE pk > 0) | |
| """).fetchall()] | |
| pk_added = 0 | |
| pk_skip = [] | |
| for table in no_pk_tables: | |
| cols = conn.execute(f'PRAGMA table_info("{table}")').fetchall() | |
| col_names = [c[1] for c in cols] | |
| col_list = ", ".join(f'"{c}"' for c in col_names) | |
| # Build new DDL with rowid_pk as PK | |
| new_col_defs = ['"rowid_pk" INTEGER PRIMARY KEY AUTOINCREMENT'] | |
| for cid, name, ctype, notnull, dflt, pk in cols: | |
| part = f'"{name}" {ctype or "TEXT"}' | |
| if notnull: | |
| part += " NOT NULL" | |
| if dflt is not None: | |
| part += f" DEFAULT {dflt}" | |
| new_col_defs.append(part) | |
| temp = f"_harden_{table[:40]}" | |
| try: | |
| conn.execute(f'DROP TABLE IF EXISTS "{temp}"') | |
| conn.execute(f'CREATE TABLE "{temp}" ({", ".join(new_col_defs)})') | |
| conn.execute(f'INSERT INTO "{temp}" ({col_list}) SELECT {col_list} FROM "{table}"') | |
| before = conn.execute(f'SELECT count(*) FROM "{table}"').fetchone()[0] | |
| after = conn.execute(f'SELECT count(*) FROM "{temp}"').fetchone()[0] | |
| if before == after: | |
| conn.execute(f'DROP TABLE "{table}"') | |
| conn.execute(f'ALTER TABLE "{temp}" RENAME TO "{table}"') | |
| pk_added += 1 | |
| else: | |
| conn.execute(f'DROP TABLE "{temp}"') | |
| pk_skip.append((table, f"count mismatch {before}→{after}")) | |
| except Exception as e: | |
| pk_skip.append((table, str(e))) | |
| try: | |
| conn.execute(f'DROP TABLE IF EXISTS "{temp}"') | |
| except Exception: | |
| pass | |
| conn.commit() | |
| print(f" {pk_added}/{len(no_pk_tables)} tables got PK") | |
| if pk_skip: | |
| for t, reason in pk_skip: | |
| print(f" SKIP: {t}: {reason}") | |
| # ================================================================ | |
| # T5: excel_data_consolidated should now have PK from O7 above | |
| # ================================================================ | |
| has_pk = any(c[5] > 0 for c in conn.execute("PRAGMA table_info(excel_data_consolidated)")) | |
| print(f"\n[T5] excel_data_consolidated PK: {'YES' if has_pk else 'NO'}") | |
| # ================================================================ | |
| # Recreate ALL triggers | |
| # ================================================================ | |
| print(f"\n[RESTORE] Recreating {len(all_triggers)} triggers...") | |
| # Add our new check triggers to the count | |
| trg_ok = 0 | |
| trg_fail = [] | |
| for tname, tsql in all_triggers: | |
| if tsql: | |
| try: | |
| conn.execute(tsql) | |
| trg_ok += 1 | |
| except Exception as e: | |
| trg_fail.append((tname, str(e))) | |
| conn.commit() | |
| print(f" OK: {trg_ok}, Failed: {len(trg_fail)}") | |
| if trg_fail: | |
| for tname, err in trg_fail[:15]: | |
| print(f" {tname}: {err}") | |
| # ================================================================ | |
| # Recreate ALL views | |
| # ================================================================ | |
| print(f"\n[RESTORE] Recreating {len(all_views)} views...") | |
| view_ok = 0 | |
| view_fail = [] | |
| for vname, vsql in all_views: | |
| if vsql: | |
| try: | |
| conn.execute(f'DROP VIEW IF EXISTS "{vname}"') | |
| conn.execute(vsql) | |
| view_ok += 1 | |
| except Exception as e: | |
| view_fail.append((vname, str(e))) | |
| conn.commit() | |
| print(f" OK: {view_ok}, Failed: {len(view_fail)}") | |
| if view_fail: | |
| for vname, err in view_fail: | |
| print(f" {vname}: {err}") | |
| # ================================================================ | |
| # NEW: QUF indexes for fast amr_lawh filtering | |
| # ================================================================ | |
| print("\n[QUF-IDX] Adding QUF pass indexes for fast filtering...") | |
| quf_tables = [r[0] for r in conn.execute(""" | |
| SELECT m.name FROM sqlite_master m | |
| WHERE m.type='table' AND m.name NOT LIKE '_retired_%' | |
| AND m.name NOT LIKE 'sqlite_%' AND m.name NOT LIKE 'entries_fts%' | |
| AND EXISTS(SELECT 1 FROM pragma_table_info(m.name) WHERE name='quf_pass') | |
| """).fetchall()] | |
| quf_idx_ok = 0 | |
| for tbl in quf_tables: | |
| idx_name = f"idx_quf_pass_{tbl[:50]}" | |
| try: | |
| conn.execute(f'CREATE INDEX IF NOT EXISTS "{idx_name}" ON "{tbl}" (quf_pass)') | |
| quf_idx_ok += 1 | |
| except Exception as e: | |
| print(f" WARN: {tbl}: {e}") | |
| conn.commit() | |
| print(f" {quf_idx_ok}/{len(quf_tables)} QUF indexes created") | |
| # ================================================================ | |
| # NEW: Fix broken a1_entries view | |
| # ================================================================ | |
| print("\n[VIEW-FIX] Fixing a1_entries view...") | |
| # The old view references qur_meaning column which no longer exists | |
| # entries table has qur_refs instead. Also add quf_pass filter. | |
| try: | |
| conn.execute("DROP VIEW IF EXISTS a1_entries") | |
| conn.execute(""" | |
| CREATE VIEW a1_entries AS | |
| SELECT entry_id, score, en_term, ru_term, fa_term, ar_word, | |
| root_id, root_letters, qur_refs, pattern, inversion_type, | |
| network_id, allah_name_id, phonetic_chain, source_form, | |
| ds_corridor, decay_level, dp_codes, ops_applied, | |
| foundation_refs, notes, qur_meaning, | |
| quf_q, quf_u, quf_f, quf_pass | |
| FROM entries | |
| """) | |
| conn.commit() | |
| view_count = conn.execute("SELECT COUNT(*) FROM a1_entries").fetchone()[0] | |
| print(f" a1_entries view recreated ({view_count} rows)") | |
| except Exception as e: | |
| print(f" WARN: {e}") | |
| # ================================================================ | |
| # NEW: Schema health check — validate AMR AI can query all 28 tables | |
| # ================================================================ | |
| print("\n[HEALTH] Schema health check for AMR AI tables...") | |
| amr_tables = { | |
| 'roots': 'root_id', | |
| 'entries': 'entry_id', | |
| 'european_a1_entries': 'entry_id', | |
| 'latin_a1_entries': 'entry_id', | |
| 'bitig_a1_entries': 'entry_id', | |
| 'uzbek_vocabulary': 'uz_id', | |
| 'a4_derivatives': 'deriv_id', | |
| 'a5_cross_refs': 'xref_id', | |
| 'names_of_allah': 'allah_id', | |
| 'qv_translation_register': 'QV_ID', | |
| 'quran_word_roots': 'word_id', | |
| 'quran_known_forms': 'form_id', | |
| 'dp_register': 'dp_code', | |
| 'disputed_words': 'word_id', | |
| 'contamination_blacklist': 'bl_id', | |
| 'phonetic_reversal': 'shift_code', | |
| 'aa_morpheme_map': 'morpheme_id', | |
| 'child_entries': 'child_id', | |
| 'bitig_convergence_register': 'conv_id', | |
| 'bitig_degradation_register': 'deg_id', | |
| 'chronology': 'rowid_pk', | |
| 'word_deployment_map': 'deploy_id', | |
| 'shift_lookup': 'en_consonant', | |
| 'name_root_hub': 'name_id', | |
| 'isnad': 'isnad_id', | |
| 'languages': 'lang_code', | |
| 'op_codes': 'op_code', | |
| } | |
| health_ok = 0 | |
| health_fail = [] | |
| for tbl, pk in amr_tables.items(): | |
| try: | |
| cnt = conn.execute(f'SELECT COUNT(*) FROM "{tbl}"').fetchone()[0] | |
| cols = [r[1] for r in conn.execute(f'PRAGMA table_info("{tbl}")').fetchall()] | |
| has_quf = 'quf_pass' in cols | |
| has_pk_col = pk in cols | |
| if cnt > 0 and has_quf and has_pk_col: | |
| health_ok += 1 | |
| else: | |
| issues = [] | |
| if cnt == 0: issues.append('EMPTY') | |
| if not has_quf: issues.append('NO_QUF') | |
| if not has_pk_col: issues.append(f'NO_PK({pk})') | |
| health_fail.append((tbl, ', '.join(issues))) | |
| except Exception as e: | |
| health_fail.append((tbl, str(e))) | |
| print(f" OK: {health_ok}/{len(amr_tables)}") | |
| if health_fail: | |
| for tbl, issue in health_fail: | |
| print(f" ISSUE: {tbl}: {issue}") | |
| # ================================================================ | |
| # NEW: Composite indexes for common query patterns | |
| # ================================================================ | |
| print("\n[PERF] Adding composite indexes for common patterns...") | |
| composites = [ | |
| ("idx_entries_root_quf", "entries", "root_id, quf_pass"), | |
| ("idx_eu_root_lang", "european_a1_entries", "root_id, lang"), | |
| ("idx_qwr_root_surah", "quran_word_roots", "root, surah"), | |
| ("idx_a4_entry_link", "a4_derivatives", "entry_id, link_type"), | |
| ("idx_roots_type_quf", "roots", "root_type, quf_pass"), | |
| ("idx_uz_orig_root", "uzbek_vocabulary", "orig_type, aa_root_id"), | |
| ] | |
| comp_ok = 0 | |
| for idx_name, table, cols in composites: | |
| try: | |
| conn.execute(f'CREATE INDEX IF NOT EXISTS "{idx_name}" ON "{table}" ({cols})') | |
| comp_ok += 1 | |
| except Exception as e: | |
| print(f" WARN: {idx_name}: {e}") | |
| conn.commit() | |
| print(f" {comp_ok}/{len(composites)} composite indexes created") | |
| # ================================================================ | |
| # Enable FK and verify | |
| # ================================================================ | |
| print("\n[VERIFY] FK enforcement...") | |
| conn.execute("PRAGMA foreign_keys = ON") | |
| violations = conn.execute("PRAGMA foreign_key_check").fetchall() | |
| print(f" FK violations: {len(violations)}") | |
| if violations: | |
| for v in violations[:10]: | |
| print(f" table={v[0]}, rowid={v[1]}, parent={v[2]}, fkid={v[3]}") | |
| # Integrity check | |
| print("\n[VERIFY] Integrity check...") | |
| result = conn.execute("PRAGMA integrity_check").fetchone()[0] | |
| print(f" {result}") | |
| # Final counts | |
| tables = conn.execute("SELECT count(*) FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").fetchone()[0] | |
| triggers = conn.execute("SELECT count(*) FROM sqlite_master WHERE type='trigger'").fetchone()[0] | |
| indexes = conn.execute("SELECT count(*) FROM sqlite_master WHERE type='index' AND name NOT LIKE 'sqlite_%'").fetchone()[0] | |
| no_pk_remaining = len([r for r in conn.execute(""" | |
| SELECT m.name FROM sqlite_master m | |
| WHERE type='table' AND name NOT LIKE 'sqlite_%' | |
| AND name NOT LIKE '_retired_%' | |
| AND name NOT LIKE 'entries_fts%' | |
| AND NOT EXISTS(SELECT 1 FROM pragma_table_info(m.name) WHERE pk > 0) | |
| """).fetchall()]) | |
| conn.close() | |
| size_mb = os.path.getsize(DB) / 1024 / 1024 | |
| print(f"\n{'=' * 70}") | |
| print(f"HARDENING COMPLETE") | |
| print(f" DB: {size_mb:.1f} MB") | |
| print(f" Tables: {tables} | Triggers: {triggers} | Indexes: {indexes}") | |
| print(f" NO_PK remaining: {no_pk_remaining}") | |
| print(f" FK violations: {len(violations)}") | |
| print(f"Finished: {datetime.now().isoformat()}") | |
| print(f"{'=' * 70}") | |
| if __name__ == "__main__": | |
| import sys | |
| if '--check-only' in sys.argv: | |
| # Run only the health check without making changes | |
| conn = sqlite3.connect(DB) | |
| print("Schema health check (read-only)...") | |
| tables = conn.execute("SELECT count(*) FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").fetchone()[0] | |
| triggers = conn.execute("SELECT count(*) FROM sqlite_master WHERE type='trigger'").fetchone()[0] | |
| indexes = conn.execute("SELECT count(*) FROM sqlite_master WHERE type='index' AND name NOT LIKE 'sqlite_%'").fetchone()[0] | |
| quf_tables = conn.execute(""" | |
| SELECT COUNT(DISTINCT m.name) FROM sqlite_master m | |
| WHERE m.type='table' AND EXISTS(SELECT 1 FROM pragma_table_info(m.name) WHERE name='quf_pass') | |
| """).fetchone()[0] | |
| size_mb = os.path.getsize(DB) / 1024 / 1024 | |
| result = conn.execute("PRAGMA integrity_check").fetchone()[0] | |
| fk_violations = len(conn.execute("PRAGMA foreign_key_check").fetchall()) | |
| conn.close() | |
| print(f" DB: {size_mb:.1f} MB | Tables: {tables} | Triggers: {triggers} | Indexes: {indexes}") | |
| print(f" QUF tables: {quf_tables} | Integrity: {result} | FK violations: {fk_violations}") | |
| else: | |
| run() | |