Skip to content

Ormdantic

The Ormdantic class is the database registry and runtime facade.

Create one instance for one database URL:

from ormdantic import Ormdantic

db = Ormdantic("sqlite:///app.sqlite3")

Use it to:

  • decorate Pydantic models with @db.table(...);
  • initialize schema with await db.init();
  • access table handles with db[Model];
  • open db.transaction() and db.session() contexts;
  • inspect the live database with db.inspect();
  • create snapshots, plans, and migration artifacts with db.migrations;
  • register lifecycle hooks with db.on(...).

ormdantic.orm.Ormdantic

Ormdantic(connection)

Ormdantic provides a way to create ORM models and schemas.

Register models as ORM models and create schemas

Source code in ormdantic/orm.py
def __init__(self, connection: str) -> None:
    """Register models as ORM models and create schemas"""
    self._tables: dict[Type, Table] = {}  # type: ignore
    self._connection = connection
    self._events = EventRegistry()
    self._table_map: Map = Map()
    self._namespaces: list[DatabaseNamespace] = []
    self._sequences: list[DatabaseSequence] = []
    self._views: list[DatabaseView] = []
    self._runtime: Any | None = None

migrations property

migrations

Return the migration manager.

table

table(
    tablename=None,
    *,
    pk,
    schema=None,
    indexed=None,
    unique=None,
    indexes=None,
    column_options=None,
    unique_constraints=None,
    check_constraints=None,
    foreign_key_constraints=None,
    exclusion_constraints=None,
    comment=None,
    tablespace=None,
    mysql_engine=None,
    mysql_charset=None,
    mysql_collation=None,
    mysql_row_format=None,
    mysql_key_block_size=None,
    mysql_pack_keys=None,
    mysql_checksum=None,
    mysql_delay_key_write=None,
    mysql_stats_persistent=None,
    mysql_stats_auto_recalc=None,
    mysql_stats_sample_pages=None,
    mysql_avg_row_length=None,
    mysql_max_rows=None,
    mysql_min_rows=None,
    mysql_insert_method=None,
    mysql_data_directory=None,
    mysql_index_directory=None,
    mysql_connection=None,
    mysql_union=None,
    mysql_partition_by=None,
    mysql_partitions=None,
    mysql_subpartition_by=None,
    mysql_subpartitions=None,
    mysql_auto_increment=None,
    oracle_compress=None,
    postgres_inherits=None,
    postgres_with=None,
    postgres_using=None,
    postgres_unlogged=False,
    postgres_partition_by=None,
    postgres_partition_of=None,
    postgres_partition_for=None,
    sqlite_strict=False,
    sqlite_without_rowid=False,
    back_references=None
)

Register a model as a database table.

Source code in ormdantic/orm.py
def table(
    self,
    tablename: str | None = None,
    *,
    pk: str,
    schema: str | None = None,
    indexed: list[str] | None = None,
    unique: list[str] | None = None,
    indexes: list[TableIndex] | None = None,
    column_options: dict[str, TableColumn] | None = None,
    unique_constraints: list[list[str] | TableUnique] | None = None,
    check_constraints: list[TableCheck] | None = None,
    foreign_key_constraints: list[TableForeignKey] | None = None,
    exclusion_constraints: list[TableExclusion] | None = None,
    comment: str | None = None,
    tablespace: str | None = None,
    mysql_engine: str | None = None,
    mysql_charset: str | None = None,
    mysql_collation: str | None = None,
    mysql_row_format: str | None = None,
    mysql_key_block_size: int | None = None,
    mysql_pack_keys: bool | None = None,
    mysql_checksum: bool | None = None,
    mysql_delay_key_write: bool | None = None,
    mysql_stats_persistent: bool | None = None,
    mysql_stats_auto_recalc: bool | None = None,
    mysql_stats_sample_pages: int | None = None,
    mysql_avg_row_length: int | None = None,
    mysql_max_rows: int | None = None,
    mysql_min_rows: int | None = None,
    mysql_insert_method: str | None = None,
    mysql_data_directory: str | None = None,
    mysql_index_directory: str | None = None,
    mysql_connection: str | None = None,
    mysql_union: list[str] | None = None,
    mysql_partition_by: str | None = None,
    mysql_partitions: int | None = None,
    mysql_subpartition_by: str | None = None,
    mysql_subpartitions: int | None = None,
    mysql_auto_increment: int | None = None,
    oracle_compress: int | bool | None = None,
    postgres_inherits: list[str] | None = None,
    postgres_with: dict[str, str | int | bool] | None = None,
    postgres_using: str | None = None,
    postgres_unlogged: bool = False,
    postgres_partition_by: str | None = None,
    postgres_partition_of: str | None = None,
    postgres_partition_for: str | None = None,
    sqlite_strict: bool = False,
    sqlite_without_rowid: bool = False,
    back_references: dict[str, str] | None = None,
) -> Callable[[Type[ModelType]], Type[ModelType]]:
    """Register a model as a database table."""

    def _wrapper(cls: Type[ModelType]) -> Type[ModelType]:
        """Decorator function."""
        tablename_ = tablename or snake_case(cls.__name__)
        schema_ = normalized_storage_identifier(
            schema,
            option_name=f"schema for table '{tablename_}'",
        )
        comment_ = normalized_table_comment(tablename_, comment)
        tablespace_ = normalized_storage_identifier(
            tablespace,
            option_name=f"tablespace for table '{tablename_}'",
        )
        mysql_engine_ = normalized_storage_token(
            mysql_engine,
            option_name=f"MySQL engine for table '{tablename_}'",
        )
        mysql_charset_ = normalized_storage_token(
            mysql_charset,
            option_name=f"MySQL charset for table '{tablename_}'",
        )
        mysql_collation_ = normalized_storage_token(
            mysql_collation,
            option_name=f"MySQL collation for table '{tablename_}'",
        )
        mysql_row_format_ = normalized_storage_token(
            mysql_row_format,
            option_name=f"MySQL row format for table '{tablename_}'",
        )
        mysql_key_block_size_ = normalized_positive_int(
            mysql_key_block_size,
            option_name=(f"MySQL/MariaDB KEY_BLOCK_SIZE for table '{tablename_}'"),
        )
        mysql_pack_keys_ = normalized_bool(
            mysql_pack_keys,
            option_name=f"MySQL/MariaDB PACK_KEYS for table '{tablename_}'",
        )
        mysql_checksum_ = normalized_bool(
            mysql_checksum,
            option_name=f"MySQL/MariaDB CHECKSUM for table '{tablename_}'",
        )
        mysql_delay_key_write_ = normalized_bool(
            mysql_delay_key_write,
            option_name=(f"MySQL/MariaDB DELAY_KEY_WRITE for table '{tablename_}'"),
        )
        mysql_stats_persistent_ = normalized_bool(
            mysql_stats_persistent,
            option_name=(
                f"MySQL/MariaDB STATS_PERSISTENT for table '{tablename_}'"
            ),
        )
        mysql_stats_auto_recalc_ = normalized_bool(
            mysql_stats_auto_recalc,
            option_name=(
                f"MySQL/MariaDB STATS_AUTO_RECALC for table '{tablename_}'"
            ),
        )
        mysql_stats_sample_pages_ = normalized_positive_int(
            mysql_stats_sample_pages,
            option_name=(
                f"MySQL/MariaDB STATS_SAMPLE_PAGES for table '{tablename_}'"
            ),
        )
        mysql_avg_row_length_ = normalized_positive_int(
            mysql_avg_row_length,
            option_name=f"MySQL/MariaDB AVG_ROW_LENGTH for table '{tablename_}'",
        )
        mysql_max_rows_ = normalized_positive_int(
            mysql_max_rows,
            option_name=f"MySQL/MariaDB MAX_ROWS for table '{tablename_}'",
        )
        mysql_min_rows_ = normalized_positive_int(
            mysql_min_rows,
            option_name=f"MySQL/MariaDB MIN_ROWS for table '{tablename_}'",
        )
        mysql_insert_method_ = normalized_storage_token(
            mysql_insert_method,
            option_name=f"MySQL/MariaDB INSERT_METHOD for table '{tablename_}'",
        )
        mysql_data_directory_ = normalized_storage_path(
            mysql_data_directory,
            option_name=f"MySQL/MariaDB DATA DIRECTORY for table '{tablename_}'",
        )
        mysql_index_directory_ = normalized_storage_path(
            mysql_index_directory,
            option_name=f"MySQL/MariaDB INDEX DIRECTORY for table '{tablename_}'",
        )
        mysql_connection_ = normalized_storage_string(
            mysql_connection,
            option_name=f"MySQL/MariaDB CONNECTION for table '{tablename_}'",
        )
        mysql_union_ = normalized_storage_identifier_list(
            mysql_union,
            option_name=f"MySQL/MariaDB UNION for table '{tablename_}'",
        )
        mysql_partition_by_ = normalized_mysql_partition_by(
            mysql_partition_by,
            table_name=tablename_,
        )
        mysql_partitions_ = normalized_positive_int(
            mysql_partitions,
            option_name=f"MySQL/MariaDB PARTITIONS for table '{tablename_}'",
        )
        mysql_subpartition_by_ = normalized_mysql_partition_by(
            mysql_subpartition_by,
            table_name=tablename_,
            option="SUBPARTITION BY",
        )
        mysql_subpartitions_ = normalized_positive_int(
            mysql_subpartitions,
            option_name=f"MySQL/MariaDB SUBPARTITIONS for table '{tablename_}'",
        )
        mysql_auto_increment_ = normalized_positive_int(
            mysql_auto_increment,
            option_name=f"MySQL/MariaDB AUTO_INCREMENT for table '{tablename_}'",
        )
        oracle_compress_ = normalized_oracle_table_compress(
            oracle_compress,
            table_name=tablename_,
        )
        postgres_inherits_ = [
            normalized_storage_identifier(
                parent,
                option_name=f"PostgreSQL inherited table for table '{tablename_}'",
            )
            or ""
            for parent in postgres_inherits or []
        ]
        postgres_with_ = normalized_postgres_storage_parameters(
            postgres_with,
            table_name=tablename_,
        )
        postgres_using_ = normalized_storage_token(
            postgres_using,
            option_name=f"PostgreSQL table access method for table '{tablename_}'",
        )
        postgres_partition_by_ = normalized_postgres_partition_by(
            postgres_partition_by,
            table_name=tablename_,
        )
        postgres_partition_of_ = normalized_storage_identifier(
            postgres_partition_of,
            option_name=f"PostgreSQL partition parent for table '{tablename_}'",
        )
        postgres_partition_for_ = normalized_postgres_partition_for(
            postgres_partition_for,
            table_name=tablename_,
        )
        if (postgres_partition_of_ is None) != (postgres_partition_for_ is None):
            raise ValueError(
                f"PostgreSQL partition table '{tablename_}' requires both "
                "postgres_partition_of and postgres_partition_for"
            )
        if postgres_partition_of_ is not None and postgres_inherits_:
            raise ValueError(
                f"PostgreSQL partition table '{tablename_}' cannot also use "
                "postgres_inherits"
            )
        indexes_ = indexes or []
        anonymous_unique_constraints, named_unique_constraints = (
            split_unique_constraints(unique_constraints or [])
        )
        clustered_names = [
            f"index {index.name}" for index in indexes_ if index.mssql_clustered
        ]
        clustered_names.extend(
            f"unique constraint {constraint.name}"
            for constraint in named_unique_constraints
            if constraint.mssql_clustered is True
        )
        if len(clustered_names) > 1:
            names = ", ".join(clustered_names)
            raise ValueError(
                f"table '{tablename_}' has multiple SQL Server clustered "
                f"indexes or unique constraints: {names}"
            )
        cls_back_references = back_references or {}
        fields = model_fields(cls)
        column_options_ = column_options or {}
        unknown_options = set(column_options_) - set(fields)
        if unknown_options:
            unknown = ", ".join(sorted(unknown_options))
            raise ValueError(
                f"column_options for table '{tablename_}' reference unknown fields: {unknown}"
            )
        foreign_key_constraints_ = foreign_key_constraints or []
        unknown_foreign_key_columns = sorted(
            {
                column
                for constraint in foreign_key_constraints_
                for column in constraint.columns
                if column not in fields
            }
        )
        if unknown_foreign_key_columns:
            unknown = ", ".join(unknown_foreign_key_columns)
            raise ValueError(
                f"foreign_key_constraints for table '{tablename_}' "
                f"reference unknown fields: {unknown}"
            )
        exclusion_constraints_ = exclusion_constraints or []
        unknown_exclusion_columns = sorted(
            {
                column
                for constraint in exclusion_constraints_
                for column, _operator in constraint.columns
                if column not in fields
            }
        )
        if unknown_exclusion_columns:
            unknown = ", ".join(unknown_exclusion_columns)
            raise ValueError(
                f"exclusion_constraints for table '{tablename_}' "
                f"reference unknown fields: {unknown}"
            )
        table_metadata = OrmTable[ModelType](
            model=cls,
            tablename=tablename_,
            pk=pk,
            schema_name=schema_,
            comment=comment_,
            tablespace=tablespace_,
            mysql_engine=mysql_engine_,
            mysql_charset=mysql_charset_,
            mysql_collation=mysql_collation_,
            mysql_row_format=mysql_row_format_,
            mysql_key_block_size=mysql_key_block_size_,
            mysql_pack_keys=mysql_pack_keys_,
            mysql_checksum=mysql_checksum_,
            mysql_delay_key_write=mysql_delay_key_write_,
            mysql_stats_persistent=mysql_stats_persistent_,
            mysql_stats_auto_recalc=mysql_stats_auto_recalc_,
            mysql_stats_sample_pages=mysql_stats_sample_pages_,
            mysql_avg_row_length=mysql_avg_row_length_,
            mysql_max_rows=mysql_max_rows_,
            mysql_min_rows=mysql_min_rows_,
            mysql_insert_method=mysql_insert_method_,
            mysql_data_directory=mysql_data_directory_,
            mysql_index_directory=mysql_index_directory_,
            mysql_connection=mysql_connection_,
            mysql_union=mysql_union_,
            mysql_partition_by=mysql_partition_by_,
            mysql_partitions=mysql_partitions_,
            mysql_subpartition_by=mysql_subpartition_by_,
            mysql_subpartitions=mysql_subpartitions_,
            mysql_auto_increment=mysql_auto_increment_,
            oracle_compress=oracle_compress_,
            postgres_inherits=postgres_inherits_,
            postgres_with=postgres_with_,
            postgres_using=postgres_using_,
            postgres_unlogged=postgres_unlogged,
            postgres_partition_by=postgres_partition_by_,
            postgres_partition_of=postgres_partition_of_,
            postgres_partition_for=postgres_partition_for_,
            sqlite_strict=sqlite_strict,
            sqlite_without_rowid=sqlite_without_rowid,
            indexed=indexed or [],
            unique=unique or [],
            indexes=indexes_,
            column_options=column_options_,
            unique_constraints=anonymous_unique_constraints,
            named_unique_constraints=named_unique_constraints,
            check_constraints=check_constraints or [],
            foreign_key_constraints=foreign_key_constraints_,
            exclusion_constraints=exclusion_constraints_,
            columns=[field for field in fields if field not in cls_back_references],
            relationships={},
            back_references=cls_back_references,
        )
        self._table_map.model_to_data[cls] = table_metadata
        self._table_map.name_to_data[tablename_] = table_metadata
        return cls

    return _wrapper

namespace

namespace(name, *, comment=None)

Register a database namespace/schema for snapshots and migrations.

Source code in ormdantic/orm.py
def namespace(self, name: str, *, comment: str | None = None) -> DatabaseNamespace:
    """Register a database namespace/schema for snapshots and migrations."""
    namespace = DatabaseNamespace(name=name, comment=comment)
    if any(existing.name == namespace.name for existing in self._namespaces):
        raise ValueError(f"duplicate namespace '{namespace.name}'")
    self._namespaces.append(namespace)
    return namespace

sequence

sequence(
    name,
    *,
    schema=None,
    data_type=None,
    start=None,
    increment=None,
    min_value=None,
    max_value=None,
    no_min_value=False,
    no_max_value=False,
    cycle=False,
    cache=None,
    comment=None,
    order=False
)

Register a database sequence for schema snapshots and migrations.

Source code in ormdantic/orm.py
def sequence(
    self,
    name: str,
    *,
    schema: str | None = None,
    data_type: str | None = None,
    start: int | None = None,
    increment: int | None = None,
    min_value: int | None = None,
    max_value: int | None = None,
    no_min_value: bool = False,
    no_max_value: bool = False,
    cycle: bool = False,
    cache: int | None = None,
    comment: str | None = None,
    order: bool = False,
) -> DatabaseSequence:
    """Register a database sequence for schema snapshots and migrations."""
    sequence = DatabaseSequence(
        name=name,
        schema=schema,
        data_type=data_type,
        start=start,
        increment=increment,
        min_value=min_value,
        max_value=max_value,
        no_min_value=no_min_value,
        no_max_value=no_max_value,
        cycle=cycle,
        cache=cache,
        comment=comment,
        order=order,
    )
    key = (sequence.schema_name, sequence.name)
    if any(
        (existing.schema_name, existing.name) == key for existing in self._sequences
    ):
        qualified = (
            sequence.name
            if sequence.schema_name is None
            else f"{sequence.schema_name}.{sequence.name}"
        )
        raise ValueError(f"duplicate sequence '{qualified}'")
    self._sequences.append(sequence)
    return sequence

view

view(
    name,
    definition,
    *,
    schema=None,
    materialized=False,
    comment=None
)

Register a database view for schema snapshots and migrations.

Source code in ormdantic/orm.py
def view(
    self,
    name: str,
    definition: str,
    *,
    schema: str | None = None,
    materialized: bool = False,
    comment: str | None = None,
) -> DatabaseView:
    """Register a database view for schema snapshots and migrations."""
    view = DatabaseView(
        name=name,
        schema=schema,
        definition=definition,
        materialized=materialized,
        comment=comment,
    )
    key = (view.schema_name, view.name)
    if any(
        (existing.schema_name, existing.name) == key for existing in self._views
    ):
        qualified = (
            view.name
            if view.schema_name is None
            else f"{view.schema_name}.{view.name}"
        )
        raise ValueError(f"duplicate view '{qualified}'")
    self._views.append(view)
    return view

init async

init()

Initialize ORM models.

Source code in ormdantic/orm.py
async def init(self) -> None:
    """Initialize ORM models."""
    for table_data in self._table_map.name_to_data.values():
        rels = self.get(table_data)
        table_data.relationships = rels
    for table_data in self._table_map.name_to_data.values():
        for field_name in table_data.relationships:
            install_relationship_path_descriptor(table_data.model, field_name)
    self._runtime = self._build_runtime_database()
    for table_data in self._table_map.name_to_data.values():
        self._tables[table_data.model] = Table(
            table_data=table_data,
            table_map=self._table_map,
            rust_handle=self._runtime.table(table_data.model.__name__),
            events=self._events,
            runtime=self._runtime,
        )
    await self.create_all()

create_all async

create_all()

Create all registered tables.

Source code in ormdantic/orm.py
async def create_all(self) -> None:
    """Create all registered tables."""
    if self._runtime is None:
        self._runtime = self._build_runtime_database()
    self._create_registered_namespaces()
    self._create_registered_sequences()
    self._runtime.create_all()
    self._create_registered_postgres_unique_options()
    self._create_registered_constraint_comments()
    self._create_registered_postgres_index_options()
    self._create_registered_mssql_index_options()
    self._create_registered_oracle_index_options()
    self._create_registered_mysql_index_options()
    self._create_registered_index_tablespaces()
    self._create_registered_index_comments()
    self._create_registered_enum_type_comments()
    self._create_registered_views()

drop_all async

drop_all()

Drop all registered tables.

Source code in ormdantic/orm.py
async def drop_all(self) -> None:
    """Drop all registered tables."""
    if self._runtime is not None:
        self._drop_registered_views()
        self._runtime.drop_all()
        self._drop_registered_sequences()
        self._drop_registered_namespaces()
        return
    self._drop_registered_views()
    for table_data in reversed(list(self._table_map.name_to_data.values())):
        sql = _ormdantic.compile_drop_table_sql(
            self._connection, table_data.tablename
        )
        _ormdantic.execute_native(self._connection, sql, [])
    for enum_type in reversed(self._runtime_enum_type_specs()):
        _ormdantic.execute_native(
            self._connection,
            _drop_runtime_enum_type_sql(enum_type),
            [],
        )
    self._drop_registered_sequences()
    self._drop_registered_namespaces()

transaction

transaction(
    *,
    isolation_level=None,
    read_only=False,
    deferrable=None
)

Open a native transaction context.

Source code in ormdantic/orm.py
def transaction(
    self,
    *,
    isolation_level: TransactionIsolationLevel | str | None = None,
    read_only: bool = False,
    deferrable: bool | None = None,
) -> Any:
    """Open a native transaction context."""
    return _OrmdanticTransaction(
        self,
        self._transaction_options(
            isolation_level=isolation_level,
            read_only=read_only,
            deferrable=deferrable,
        ),
    )

session

session(
    *,
    isolation_level=None,
    read_only=False,
    deferrable=None
)

Open an async unit-of-work session.

Source code in ormdantic/orm.py
def session(
    self,
    *,
    isolation_level: TransactionIsolationLevel | str | None = None,
    read_only: bool = False,
    deferrable: bool | None = None,
) -> Session:
    """Open an async unit-of-work session."""
    return Session(
        self,
        transaction_options=self._transaction_options(
            isolation_level=isolation_level,
            read_only=read_only,
            deferrable=deferrable,
        ),
    )

inspect

inspect()

Return a database inspector.

Source code in ormdantic/orm.py
def inspect(self) -> Inspector:
    """Return a database inspector."""
    return Inspector(self)

relation

relation(
    model,
    relationship,
    *,
    outer_alias=None,
    target_alias=None
)

Return a typed helper for relationship predicates and aggregates.

Source code in ormdantic/orm.py
def relation(
    self,
    model: Type[ModelType],
    relationship: str,
    *,
    outer_alias: str | None = None,
    target_alias: str | None = None,
) -> RelationExpression:
    """Return a typed helper for relationship predicates and aggregates."""
    table = self._table_map.model_to_data.get(model)
    if table is not None and not table.relationships:
        table.relationships = self.get(table)
    return relation_expression(
        self._table_map,
        model,
        relationship,
        outer_alias=outer_alias,
        target_alias=target_alias,
    )

savepoint

savepoint(name)

Open a savepoint context.

Source code in ormdantic/orm.py
def savepoint(self, name: str) -> Any:
    """Open a savepoint context."""
    return _OrmdanticSavepoint(self, name)

on

on(event, handler)

Register an event handler.

Source code in ormdantic/orm.py
def on(self, event: str, handler: EventHandler) -> EventHandler:
    """Register an event handler."""
    return self._events.on(event, handler)

off

off(event, handler)

Remove a registered event handler.

Source code in ormdantic/orm.py
def off(self, event: str, handler: EventHandler) -> None:
    """Remove a registered event handler."""
    self._events.off(event, handler)

clear_events

clear_events(event=None)

Clear event handlers for one event or all events.

Source code in ormdantic/orm.py
def clear_events(self, event: str | None = None) -> None:
    """Clear event handlers for one event or all events."""
    self._events.clear(event)

load async

load(model, path)

Explicitly load a relationship path for a model instance.

Source code in ormdantic/orm.py
async def load(self, model: ModelType, path: LoaderPathLike) -> Any:
    """Explicitly load a relationship path for a model instance."""
    table = self._table_map.model_to_data[type(model)]
    normalized_path = ".".join(path_parts(path))
    loaded = await self[type(model)].find_one(
        getattr(model, table.pk), load=[joinedload(normalized_path)]
    )
    if loaded is None:
        return None
    value: Any = loaded
    for part in path_parts(normalized_path):
        value = getattr(value, part)
    return value

get

get(table_data)

Get relationships for a given table.

Source code in ormdantic/orm.py
def get(self, table_data: OrmTable[ModelType]) -> dict[str, Relationship]:
    """Get relationships for a given table."""
    relationships = {}
    for field_name, field in model_fields(table_data.model).items():
        related_table = self._get_related_table(field)
        if related_table is None:
            continue
        if back_reference := table_data.back_references.get(field_name):
            relationships[field_name] = self._get_many_relationship(
                field_name, back_reference, table_data, related_table
            )

            continue
        if contains_list_annotation(
            field.annotation
        ) or field.annotation == ForwardRef(f"{related_table.model.__name__}"):
            raise UndefinedBackReferenceError(
                table_data.tablename, related_table.tablename, field_name
            )

        args = get_args(field.annotation)
        correct_type = (
            model_field(related_table.model, related_table.pk).annotation in args
        )
        origin = get_origin(field.annotation)
        if not args or origin not in {UnionType, Union} or not correct_type:
            raise MustUnionForeignKeyError(
                table_data.tablename,
                related_table.tablename,
                field_name,
                related_table.model,
                model_field(
                    related_table.model, related_table.pk
                ).annotation.__name__,
            )

        relationships[field_name] = Relationship(
            foreign_table=related_table.tablename
        )

    return relationships