How Mantle Works
================

Mantle builds on the dynamic power of Django's ORM. It's allows us move our
domain logic in simple typed Python classes.

Mantle’s Query layer bridges from the ORM to your data shapes in an efficient and scalable way.

It's a deliberate but small separation. We keep everything from Django, and we
build on top of that.

At a high level, Mantle combines:

* ``attrs`` for typed shape classes
* ``django-readers`` for efficient ORM querying
* ``cattrs`` for converting our data into attrs instances

Mantle's ``Query`` is the glue: it connects these three pieces into one read pipeline: Mantle generates a *readers spec* from your attrs shape classes, processes that with django-readers, and uses cattrs to map the results back to instances of your shape class.

There are utilities for writing to the database, as well as validation, and tools for :doc:`custom-querying`, but the core flow is ``Query``.

The goal of this doc is to explain that.

``attrs``: your typed result shape
----------------------------------

Mantle uses `attrs`_ classes as the source of truth for your data shapes.

.. _attrs: https://www.attrs.org/en/stable/

.. code-block:: python

    from attrs import define

    @define
    class BookmarkData:
        url: str
        comment: str
        favourite: bool

The goal is to have explicit, typed Python objects, decoupled from the ORM.

Defining the classes is easy. The challenge is the mapping between those and the ORM.


``django-readers``: efficient ORM querying
------------------------------------------

`django-readers`_ is a great library that allows efficient querying from the ORM.

* It specifies that exact fields you need for your data, avoiding unlimited field fetches
* It automatically handles prefetching related objects, so there are no N+1 query problems when working with nested data

.. _django-readers: https://www.django-readers.org

``django-readers`` starts from a **spec**. A spec describes the fields (and
relationships) you want.

In raw *readers* code, you ``process()`` the spec, and get back two functions:

* ``prepare(queryset)``: returns an optimized queryset
* ``project(instance)``: returns a dictionary with the requested data

You use these to fetch your data:

.. code-block:: python

    from django_readers import specs

    spec = [
        "url",
        "comment",
        {"owner": ["username"]},
    ]

    prepare, project = specs.process(spec)
    queryset = prepare(Bookmark.objects.all())
    rows = [project(instance) for instance in queryset]

A useful basic concept here is a **reader pair**: two functions grouped as a
tuple that capture a dependency:

* a queryset function that loads what is needed
* a producer/projector function that reads from the prepared instance

So a pair is the contract: "prepare first, then produce/project".

Mantle's ``Query`` API gives you access to each of the steps here, but abstracts this flow by default.

It's worth familiarising yourself with the `django-readers`_ docs, and at least reviewing the tutorial and cookbook sections there. You'll need at least some understanding when you need to dig into :doc:`custom-querying`.

Readers is wonderful, but it's output is untyped.


``cattrs``: dicts -> attrs instances
------------------------------------

``django-readers`` projects model instances to dictionaries. Mantle then uses
`cattrs`_  to map that raw data from the ORM into your attrs classes.

.. _cattrs: https://catt.rs/en/stable/

``cattrs`` is a modern and performant library for structuring and unstructuring
data (normally for serialisation).

Mantle ships with a default cattrs converter for common Django value types, including
``date``, ``datetime``, ``time``, ``timedelta``, ``UUID``, and ``Decimal``.


How Mantle connects everything
------------------------------

When you run:

.. code-block:: python

    rows = Query(Bookmark.objects.all(), BookmarkData).all()

Mantle roughly does this:

1. Generate a readers spec from ``BookmarkData`` using ``to_spec()``.
2. Process that spec with ``django-readers`` into ``(prepare, project)``.
3. Call ``prepare(queryset)`` to build an optimized queryset.
4. Execute the queryset and call ``project(instance)`` for each row (dict output).
5. Use ``cattrs`` to structure each dict into ``BookmarkData``.

So the full read flow is:

``attrs class -> readers spec -> (prepare, project) -> dicts -> cattrs -> attrs instances``


Where ``@overrides`` fits
-------------------------

Auto-generated specs cover common cases. When needed, ``@overrides`` lets you
replace a field's generated spec entry with an explicit django-readers pair.

.. code-block:: python

    from attrs import define
    from django_readers import qs
    from mantle import Query, overrides

    prepare_is_public = qs.include_fields("status")

    def produce_is_public(bookmark):
        return bookmark.status == "public"

    @overrides({"is_public": (prepare_is_public, produce_is_public)})
    @define
    class BookmarkSummary:
        url: str
        is_public: bool

    row = Query(Bookmark.objects.all(), BookmarkSummary).get()

See :doc:`custom-querying` for more.
