Source code for codegen_database.plugins.fk

"""Foreign key plugin for codegen_database dimensions.

:class:`TableFKPlugin` converts inline
:class:`~codegen_database.fk.CodegenDatabaseForeignKey` column markers into real
SQLAlchemy ``ForeignKeyConstraint`` objects on a table.  Two-part
references (``"dimension.column"``) are resolved via the dimension
registry in ``metadata.info``.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from codegen_database.errors import CodegenDatabaseValidationError
from codegen_database.fk import (
    _INLINE_FK_INFO_KEY,
    CodegenDatabaseForeignKey,
    defer_fk_resolution,
    materialize_fk,
    resolve_fk_reference,
)
from codegen_database.plugin import Dynamic, Plugin, requires

if TYPE_CHECKING:
    from codegen_database.factory.context import FactoryContext

#: Number of parts in a fully-qualified ``schema.table.column`` ref.
_FULLY_QUALIFIED_PARTS = 3

#: Number of parts in a ``dimension.column`` ref.
_DIMENSION_PARTS = 2


[docs] @requires(Dynamic("table_key")) class TableFKPlugin(Plugin): """Materialize FK declarations as ``ForeignKeyConstraint`` objects. Handles inline :class:`~codegen_database.fk.CodegenDatabaseForeignKey` markers attached to ``Column`` constructors (single-column). Two-part ``"dimension.column"`` references are resolved via the dimension registry. Factories run at class-creation time, so a reference may name a dimension whose model hasn't been imported yet -- those are deferred and materialize when the dimension registers (see :func:`~codegen_database.fk.register_dimension`); import order never constrains who may reference whom. Three-part ``"schema.table.column"`` references are passed through directly. Args: table_key: Key in ``ctx`` for the target table (default ``"primary"``). """ def __init__(self, table_key: str = "primary") -> None: """Store the context key.""" self.table_key = table_key
[docs] def run(self, ctx: FactoryContext) -> None: """Collect and create foreign key constraints from inline markers.""" table = ctx[self.table_key] registry: dict[str, object] = ctx.metadata.info.get( "codegen_database_dimensions", {} ) for col in table.columns: inline_fks: list[CodegenDatabaseForeignKey] = col.info.get( _INLINE_FK_INFO_KEY, [] ) for inline_fk in inline_fks: ref = inline_fk.reference parts = ref.split(".") if len(parts) == _FULLY_QUALIFIED_PARTS: resolved_ref = ref elif len(parts) != _DIMENSION_PARTS: msg = ( f"FK reference {ref!r} must be " f"'dimension.column' or " f"'schema.table.column' format." ) raise CodegenDatabaseValidationError(msg) elif parts[0] not in registry: # The dimension's model may simply not be # imported yet; register_dimension materializes # this entry when it arrives. defer_fk_resolution( ctx.metadata, parts[0], table, col.name, inline_fk, ) continue else: resolved_ref = resolve_fk_reference(ctx.metadata, ref) materialize_fk( ctx.metadata, table, col.name, inline_fk, resolved_ref, )