Ctrl K
ComunidadGuías…

tests/db_helpers.py

Rama dev · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Explicit lifetimes for database engines owned by individual tests or examples."""

import sqlite3
import threading
from collections.abc import Iterator
from contextlib import contextmanager

from sqlalchemy import Engine, MetaData, Table, create_engine
from sqlalchemy.orm import Session

_schema_template: sqlite3.Connection | None = None
_schema_template_lock = threading.Lock()


@contextmanager
def disposing_engine(engine: Engine) -> Iterator[Engine]:
    """Dispose an engine after its sessions have closed, including failed setup.

    Args:
        engine: The engine owned by the surrounding test, fixture, or property example.

    Yields:
        The same engine, with disposal guaranteed on leaving the context.
    """
    try:
        yield engine
    finally:
        engine.dispose()


def insert_legacy(session: Session, model: type, rows: list[dict]) -> None:
    """Insert rows into a table as an OLDER migration left it, for tests that upgrade to a past revision.

    Neither the ORM nor a core insert on the model can do this: both write every column the model
    gives a Python default, including columns a later migration adds, which the old table lacks. So
    this reads the table as it stands and fills only its columns, with the model's defaults.

    Args:
        session: A session on the database at the older revision.
        model: The ORM class whose table to insert into.
        rows: Column values for each row; the model's defaults fill the rest.
    """
    table = Table(model.__tablename__, MetaData(), autoload_with=session.connection())
    defaults = {}
    for column in model.__table__.columns:
        default = column.default
        if column.name in table.c and default is not None:
            defaults[column.name] = default.arg(None) if default.is_callable else default.arg
    session.execute(table.insert(), [{**defaults, **row} for row in rows])


def create_schema(engine: Engine) -> None:
    """`Base.metadata.create_all(engine)`, for a fresh, empty database — by copying a schema built once.

    Building the schema costs ~50 ms; copying it with SQLite's backup API costs ~0.1 ms. Property tests
    build one per example, so the difference was minutes per run. The copy REPLACES whatever the
    database held, which is why it is only for a database that was just created.

    Args:
        engine: A newly created SQLite engine whose database is still empty.
    """
    global _schema_template
    with _schema_template_lock:
        if _schema_template is None:
            from shortlist.server.db.models import Base

            with disposing_engine(create_engine("sqlite://")) as builder:
                Base.metadata.create_all(builder)
                template = sqlite3.connect(":memory:", check_same_thread=False)
                with builder.connect() as conn:
                    conn.connection.driver_connection.backup(template)
            _schema_template = template
        with engine.connect() as conn:
            _schema_template.backup(conn.connection.driver_connection)