"""Program schema — per-program DB table definitions + subprogram list.""" from __future__ import annotations from dataclasses import dataclass, field from pathlib import Path from typing import Optional @dataclass class ColumnDef: name: str type: str # SQL type: "CHAR(6)", "NUMERIC(4)", "VARCHAR(30)" primary_key: bool = False nullable: bool = False default: Optional[str] = None cobol_field: Optional[str] = None # COBOL field name if different @dataclass class TableDef: name: str columns: list[ColumnDef] = field(default_factory=list) create_if_missing: bool = True sql_name: Optional[str] = None # COBOL SQL table name if different from YAML name @dataclass class SysinDef: """SYSIN card configuration for a single run scenario. Each program defines its own sysin fields; period/modes are KIN08DBU specific. """ period: str | None = "202607" include_invalid_period: bool = False modes: list[str] = field(default_factory=lambda: ["NORMAL"]) @dataclass class ScenarioDef: """A single run scenario: one set of inputs for one COBOL execution.""" id: str sysin: SysinDef = field(default_factory=SysinDef) inject_duplicate_pk: bool = False @dataclass class ProgramSchema: program_id: str db_tables: list[TableDef] = field(default_factory=list) subprograms: list[str] = field(default_factory=list) db_type: str = "SQLite" db_name: str = "OVERTIME.DB" runs: list[ScenarioDef] = field(default_factory=list) @classmethod def from_yaml(cls, path: str | Path) -> ProgramSchema: import yaml with open(path, encoding="utf-8") as f: raw = yaml.safe_load(f) tables = [] for t in raw.get("db_tables", []): cols = [ColumnDef(**c) for c in t.get("columns", [])] tables.append(TableDef( name=t["name"], columns=cols, sql_name=t.get("sql_name"), )) runs_raw = raw.get("runs", []) runs = [] for r in runs_raw: sysin_raw = r.get("sysin", {}) runs.append(ScenarioDef( id=r["id"], sysin=SysinDef(**sysin_raw), inject_duplicate_pk=r.get("inject_duplicate_pk", False), )) return cls( program_id=raw["program_id"], db_tables=tables, subprograms=raw.get("subprograms", []), db_type=raw.get("db_type", "SQLite"), db_name=raw.get("db_name", "OVERTIME.DB"), runs=runs, ) def load_schema(program_id: str, search_dirs: list[str | Path] | None = None) -> ProgramSchema: """Load per-program YAML schema by program ID.""" if search_dirs is None: search_dirs = [Path(__file__).parent / "programs"] for d in search_dirs: p = Path(d) / f"{program_id}.yaml" if p.exists(): return ProgramSchema.from_yaml(p) raise FileNotFoundError(f"Schema not found for {program_id} in {search_dirs}")