""" Catalogs the non-table Access objects (Queries, Forms, Reports) via DAO COM automation (win32com), since ODBC only exposes Tables and a subset of non-hidden SELECT queries (as "VIEW"). DAO exposes everything, including: - QueryDefs: every saved query, with full .SQL text (this is where a lot of the business logic actually lives - billing calculations, renewal schedules, report data sources) - Containers("Reports").Documents: every saved Report object (name only - DAO's catalog interface doesn't expose RecordSource/layout; that needs the full Access.Application object model, which isn't available without MS Access itself installed) - Containers("Forms").Documents: every saved Form object (name only, same limitation) Requires: pywin32 (`pip install pywin32`) and the Access Database Engine (same requirement as everything else in migration/ - Windows only). Usage: python catalog_objects.py --output objects.json """ from __future__ import annotations import argparse import json from pathlib import Path import win32com.client from config import SOURCE_ROOT HIDDEN_QUERY_PREFIXES = ("~sq_", "~TMP") # Unlike config.SOURCES (which points "seguros" at the _be data file), the # Queries/Forms/Reports objects live in the linked *frontend* files - the # _be backend is pure data storage. Both SEGUROS files are catalogued here # since either could theoretically hold saved objects. OBJECT_SOURCES = { "utilities": SOURCE_ROOT / "UTILITIES.accdb", "seguros_frontend": SOURCE_ROOT / "SEGUROS 16.mdb", "seguros_backend": SOURCE_ROOT / "SEGUROS 16_be.mdb", "scothia": SOURCE_ROOT / "SCOTHIA.mdb", } def catalog_source(path: Path) -> dict: engine = win32com.client.Dispatch("DAO.DBEngine.120") db = engine.OpenDatabase(str(path)) queries = [] for i in range(db.QueryDefs.Count): qd = db.QueryDefs(i) name = qd.Name is_hidden = name.startswith(HIDDEN_QUERY_PREFIXES) try: sql = qd.SQL except Exception as exc: # noqa: BLE001 sql = f"" try: query_type = qd.Type # 0=Select, 32=CrossTab, 48=Delete, 64=Update, 80=Append, 96=MakeTable, 112=DDL, 128=PassThrough, 144=SPTBulk, 160=SetOperation, 176=Procedure except Exception: query_type = None queries.append({"name": name, "hidden": is_hidden, "sql": sql, "type": query_type}) def list_container(container_name: str) -> list[str]: container = db.Containers(container_name) names = [] for i in range(container.Documents.Count): names.append(container.Documents(i).Name) return sorted(names) reports = list_container("Reports") forms = list_container("Forms") db.Close() return { "query_count": len(queries), "queries": queries, "report_count": len(reports), "reports": reports, "form_count": len(forms), "forms": forms, } def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--output", type=Path, default=Path("objects.json")) args = parser.parse_args() catalog = {} for source_name, path in OBJECT_SOURCES.items(): print(f"Cataloging objects in {source_name} ({path})...") if not Path(path).exists(): print(f" [skip] not found") continue catalog[source_name] = catalog_source(path) c = catalog[source_name] print(f" queries={c['query_count']} reports={c['report_count']} forms={c['form_count']}") args.output.write_text(json.dumps(catalog, indent=2, default=str), encoding="utf-8") print(f"Wrote {args.output}") if __name__ == "__main__": main()