Lordkiki commited on
Commit
8bebe44
·
verified ·
1 Parent(s): a215f6c

Deploy URAAS — African Research Archival & Analytics System

Browse files
Files changed (3) hide show
  1. scripts/init_db.py +52 -1
  2. scripts/push_to_hf.py +18 -2
  3. uraas/database.py +46 -0
scripts/init_db.py CHANGED
@@ -9,7 +9,7 @@ import sys
9
  # Add project root to path (parent of scripts/)
10
  sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
11
 
12
- from uraas.database import Collection, Community, SessionLocal, init_db
13
 
14
  # Import citation tracker models so SQLAlchemy registers their tables with
15
  # Base.metadata before create_all() runs — otherwise citations,
@@ -18,6 +18,53 @@ import uraas.services.citation_tracker # noqa: F401
18
 
19
  from uraas.utils.unilag_classifier import UNILAG_STRUCTURE
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
  def seed_communities_and_collections():
23
  """Seed the database with UNILAG faculty and department structure."""
@@ -72,6 +119,10 @@ def main():
72
  print("[OK] Tables created successfully!")
73
  print()
74
 
 
 
 
 
75
  # Seed communities and collections
76
  seed_communities_and_collections()
77
  print()
 
9
  # Add project root to path (parent of scripts/)
10
  sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
11
 
12
+ from uraas.database import Collection, Community, SessionLocal, init_db, sync_schema_columns
13
 
14
  # Import citation tracker models so SQLAlchemy registers their tables with
15
  # Base.metadata before create_all() runs — otherwise citations,
 
18
 
19
  from uraas.utils.unilag_classifier import UNILAG_STRUCTURE
20
 
21
+ # Base.metadata.create_all() (called by init_db() below) only creates tables
22
+ # that don't exist yet — it never alters an EXISTING table to add a column a
23
+ # newer version of the ORM model expects. On a fresh DB that's a no-op (every
24
+ # table is new, so every column is already there); on a database that
25
+ # already existed before some column was added to the model (e.g. an HF
26
+ # Space's persistent /data/uraas.db surviving across deploys), the table is
27
+ # silently left on its old schema and the very next query touching that
28
+ # column crashes — confirmed live 2026-07-19: the Space failed to start
29
+ # entirely ("no such column: communities.unit_type") because its persistent
30
+ # DB predated that column and nothing ever ran the matching migration
31
+ # against it. Running every idempotent (column_exists()-guarded, ALTER-TABLE-
32
+ # only) migration here means any existing database — this one included —
33
+ # self-heals to the current schema on every startup, not just fresh ones.
34
+ # migrate_add_ror.py is deliberately excluded: unlike the others it also
35
+ # *writes* a default ROR value to existing NULL rows, and that default is
36
+ # the deprecated pre-2026 UNILAG ROR (03qcnxw14, since corrected to
37
+ # 05rk03822 by migrate_unilag_ror.py) — safe to run once by hand, not safe
38
+ # to run unconditionally on every boot.
39
+ _SCHEMA_MIGRATIONS = [
40
+ "migrate_add_pid_source",
41
+ "migrate_add_community_ace_columns",
42
+ "migrate_add_author_isni",
43
+ "migrate_add_item_funders",
44
+ "migrate_2026_upgrade",
45
+ "migrate_add_sc_columns",
46
+ ]
47
+
48
+
49
+ def run_schema_migrations():
50
+ import importlib
51
+
52
+ print("Applying schema migrations (idempotent — safe to re-run)...")
53
+ print(" Generic column sync:")
54
+ sync_schema_columns()
55
+ for mod_name in _SCHEMA_MIGRATIONS:
56
+ try:
57
+ mod = importlib.import_module(mod_name)
58
+ mod.main()
59
+ except Exception as e:
60
+ # A migration failing shouldn't take down the whole app if the
61
+ # underlying tables/columns it depends on genuinely aren't there
62
+ # yet for some other reason — log and continue rather than abort
63
+ # startup entirely (seeding below will surface the real error if
64
+ # it still matters).
65
+ print(f" [WARN] {mod_name} failed (continuing): {e}")
66
+ print()
67
+
68
 
69
  def seed_communities_and_collections():
70
  """Seed the database with UNILAG faculty and department structure."""
 
119
  print("[OK] Tables created successfully!")
120
  print()
121
 
122
+ # Heal any existing database (e.g. a persistent volume surviving across
123
+ # deploys) whose tables predate a column the current models expect.
124
+ run_schema_migrations()
125
+
126
  # Seed communities and collections
127
  seed_communities_and_collections()
128
  print()
scripts/push_to_hf.py CHANGED
@@ -39,7 +39,18 @@ IGNORE_FILES = {
39
  "docker-compose.replica.yml",
40
  "docker-compose.demo.yml",
41
  }
42
- IGNORE_EXTS = {".pyc", ".pyo", ".pyd"}
 
 
 
 
 
 
 
 
 
 
 
43
 
44
 
45
  def should_skip(rel_path: str, is_dir: bool) -> bool:
@@ -47,9 +58,14 @@ def should_skip(rel_path: str, is_dir: bool) -> bool:
47
  name = parts[-1]
48
  if is_dir:
49
  return name in IGNORE_DIRS
 
 
 
 
 
50
  return (
51
  name in IGNORE_FILES
52
- or os.path.splitext(name)[1] in IGNORE_EXTS
53
  )
54
 
55
 
 
39
  "docker-compose.replica.yml",
40
  "docker-compose.demo.yml",
41
  }
42
+ IGNORE_EXTS = {
43
+ ".pyc", ".pyo", ".pyd",
44
+ # Database files/backups — belt-and-suspenders beyond the exact
45
+ # "uraas.db" name check below and the startswith("uraas.db") check,
46
+ # since a missed pattern here means real crawled data (author names,
47
+ # DOIs, institutional affiliations, emails) goes to a PUBLIC repo.
48
+ # Confirmed this actually happened (2026-07-19): uraas.db.bak, a
49
+ # 1819-item/14MB snapshot, was uploaded because only "uraas.db" itself
50
+ # was excluded, not the ".bak" variant — deleted from the live Space
51
+ # after the fact, but should never have gone up in the first place.
52
+ ".bak", ".backup", ".old", ".orig", ".db", ".sqlite", ".sqlite3",
53
+ }
54
 
55
 
56
  def should_skip(rel_path: str, is_dir: bool) -> bool:
 
58
  name = parts[-1]
59
  if is_dir:
60
  return name in IGNORE_DIRS
61
+ # Catches any suffix variant regardless of extension — uraas.db-wal,
62
+ # uraas.db-shm, uraas.db-journal, timestamped backups like
63
+ # uraas.db.20260629, etc. — not just the exact names/extensions above.
64
+ if name.startswith("uraas.db"):
65
+ return True
66
  return (
67
  name in IGNORE_FILES
68
+ or os.path.splitext(name)[1].lower() in IGNORE_EXTS
69
  )
70
 
71
 
uraas/database.py CHANGED
@@ -393,3 +393,49 @@ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
393
 
394
  def init_db():
395
  Base.metadata.create_all(bind=engine, checkfirst=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
 
394
  def init_db():
395
  Base.metadata.create_all(bind=engine, checkfirst=True)
396
+
397
+
398
+ def sync_schema_columns():
399
+ """Add any column declared on an ORM model but missing from the actual
400
+ database table.
401
+
402
+ create_all() only creates whole tables that don't exist yet — it never
403
+ alters an existing table, so a database that predates some column
404
+ (e.g. a persistent volume surviving across deploys) is silently left on
405
+ its old schema forever, and the first query touching that column
406
+ crashes. Confirmed live 2026-07-19: an HF Space failed to start
407
+ entirely because its persistent DB predated Community.unit_type, and a
408
+ synthetic even-older test schema also turned up a second, completely
409
+ undocumented gap (Community.ror) with no dedicated migration script at
410
+ all — one-column-at-a-time migration scripts don't scale to catching
411
+ every possible drift. This is generic instead: it walks every declared
412
+ ORM column and ALTERs in whatever the live table is missing, so it
413
+ self-heals regardless of which columns happen to be absent or when they
414
+ were added to the models.
415
+ """
416
+ from sqlalchemy import inspect, text
417
+
418
+ insp = inspect(engine)
419
+ existing_tables = set(insp.get_table_names())
420
+ added = 0
421
+ with engine.begin() as conn:
422
+ for table in Base.metadata.sorted_tables:
423
+ if table.name not in existing_tables:
424
+ continue # brand new table — create_all() already made it correctly
425
+ existing_cols = {c["name"] for c in insp.get_columns(table.name)}
426
+ for column in table.columns:
427
+ if column.name in existing_cols:
428
+ continue
429
+ try:
430
+ ddl_type = column.type.compile(dialect=engine.dialect)
431
+ except Exception as e:
432
+ print(f" [WARN] cannot compile DDL type for {table.name}.{column.name}: {e}")
433
+ continue
434
+ stmt = f"ALTER TABLE {table.name} ADD COLUMN {column.name} {ddl_type}"
435
+ try:
436
+ conn.execute(text(stmt))
437
+ print(f" -> {stmt}")
438
+ added += 1
439
+ except Exception as e:
440
+ print(f" [WARN] {stmt} failed: {e}")
441
+ print(f" schema sync: {added} missing column(s) added" if added else " schema sync: already in sync")