Summary
omop_graph/extensions/omop_alchemy.py declares its extension tables with @cdm_table on the shared orm_loader.helpers.Base, but never sets __omop_table_category__. omop-alchemy's maintenance scan walks Base.registry.mappers and hard-fails on any mapped class missing that attribute, so importing both packages into one process makes omop-alchemy's vocabulary loading, table creation, and schema reconciliation impossible.
Confirmed this is not limited to vocab loading: every maintenance CLI command that touches table collection (cli_schema_summary, cli_schema_tables, cli_schema_info, cli_schema_reconcile, cli_tables, cli_foreign_keys, cli_vocab, cli_indexes) funnels through the same collection function and crashes identically.
Reproduction
import omop_graph.extensions.omop_alchemy # noqa: F401 (any groundworkers import does this)
import sqlalchemy as sa
from omop_alchemy.maintenance.cli_vocab import load_vocab_source
engine = sa.create_engine("sqlite+pysqlite:///scratch.sqlite")
load_vocab_source(engine, source_path="./athena_files")
# raises RuntimeError: RelationshipClass is missing __omop_table_category__
The trigger is broader than the scoped code above, since RelationshipClass and RelationshipMapping of omop_graph/extensions/omop_alchemy.py are loaded eagerly via omop_graph/__init__.py, api.py, and
graph/kg.py. As a result, the error fires on any bare import omop_graph, not only deliberate use of the relationship tables.
Cause
omop_alchemy/cdm/base/decorators.py's cdm_table() unconditionally sets __omop_is_cdm_table__ = True and __omop_table_category__ = _infer_table_category(cls). _infer_table_category only returns a value if cls.__module__ starts with the hardcoded omop_alchemy.cdm.model. prefix, and otherwise returns None.
omop_alchemy/maintenance/tables.py's _table_category() raises RuntimeError when that attribute is None, and the registry-wide scan in collect_maintenance_tables() means one such class breaks every command listed above, regardless of which table is actually being touched.
Suggested fix
The actual mechanism belongs in omop-alchemy, not here, since _infer_table_category's closed-world assumption and TableCategory's fixed 8-member enum are both omop-alchemy's own design. Four approaches were considered, tracked in the linked omop-alchemy feature request rather than resolved here:
- Hardcode a category value directly on
omop-graph's two classes: Fast, but does not generalise to any future third-party table, here or elsewhere.
- Add an explicit, optional category override to
cdm_table itself, with no other change. Root-causes it for any third party, but risks letting an in-tree omop-alchemy model silently override its own inferred category.
- A fully separate decorator for third-party tables, with its own independent copy of
cdm_table's structural checks. Cleanest separation in principle, but duplicates logic that already exists at this stage.
- A single
cdm_table with an explicit origin parameter (internal versus extension) that routes to the right resolution logic internally, with a thin, separately named alias for the extension path.
Summary
omop_graph/extensions/omop_alchemy.pydeclares its extension tables with@cdm_tableon the sharedorm_loader.helpers.Base, but never sets__omop_table_category__.omop-alchemy's maintenance scan walksBase.registry.mappersand hard-fails on any mapped class missing that attribute, so importing both packages into one process makesomop-alchemy's vocabulary loading, table creation, and schema reconciliation impossible.Confirmed this is not limited to vocab loading: every maintenance CLI command that touches table collection (
cli_schema_summary,cli_schema_tables,cli_schema_info,cli_schema_reconcile,cli_tables,cli_foreign_keys,cli_vocab,cli_indexes) funnels through the same collection function and crashes identically.Reproduction
The trigger is broader than the scoped code above, since
RelationshipClassandRelationshipMappingofomop_graph/extensions/omop_alchemy.pyare loaded eagerly viaomop_graph/__init__.py,api.py, andgraph/kg.py. As a result, the error fires on any bareimport omop_graph, not only deliberate use of the relationship tables.Cause
omop_alchemy/cdm/base/decorators.py'scdm_table()unconditionally sets__omop_is_cdm_table__ = Trueand__omop_table_category__ = _infer_table_category(cls)._infer_table_categoryonly returns a value ifcls.__module__starts with the hardcodedomop_alchemy.cdm.model.prefix, and otherwise returnsNone.omop_alchemy/maintenance/tables.py's_table_category()raisesRuntimeErrorwhen that attribute isNone, and the registry-wide scan incollect_maintenance_tables()means one such class breaks every command listed above, regardless of which table is actually being touched.Suggested fix
The actual mechanism belongs in
omop-alchemy, not here, since_infer_table_category's closed-world assumption andTableCategory's fixed 8-member enum are both omop-alchemy's own design. Four approaches were considered, tracked in the linkedomop-alchemyfeature request rather than resolved here:omop-graph's two classes: Fast, but does not generalise to any future third-party table, here or elsewhere.cdm_tableitself, with no other change. Root-causes it for any third party, but risks letting an in-treeomop-alchemymodel silently override its own inferred category.cdm_table's structural checks. Cleanest separation in principle, but duplicates logic that already exists at this stage.cdm_tablewith an explicit origin parameter (internal versus extension) that routes to the right resolution logic internally, with a thin, separately named alias for the extension path.