============================
Customising query generation
============================

For both flat and nested data, Mantle will automatically generate the ORM
queries you need. It leverages ``django-readers`` in order to do
this.

Mantle's role is to generate the required *readers spec* for your ``attrs``
shape classes, and then map the returned data back to ``attrs`` instances using
``cattrs``.

Make sure you've read :doc:`how-mantle-works` to understand that flow completely.

This page explains how to customise query generation.

In summary:

* In the majority of cases you use the ``@overrides`` decorator to customise
  individual fields.
* For more complex cases you can use the ``@spec`` decorator to provide the
  readers spec directly.
* By combining a manual ``@spec`` with a custom ``cattrs`` ``converter``,
  passed to ``Query``, you can enable **advanced data reshaping** if (or when) that need
  arrises.


Examples
========

The following examples range from simple customisations to a much more complex
data reshaping case. They assume you're familiar, in particular, with
``django-readers`` *pairs*.

You can likely work out the simpler examples *as you go*. The final example
might require a little more.

Again, see :doc:`how-mantle-works` if you're unsure.


Mapping a field value
---------------------

Probably the most basic use of ``@overrides`` is to adjust the value produced
from the ORM to apply a custom method or transform.

A Django ``FileField`` is a good example. Here we want to populate our data
shape with the filename for an attachement's file field::

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

    @overrides(
        {"file": (qs.include_fields("file"), lambda a: a.file.name)}
    )
    @define
    class Attachment:
        id: int
        name: str
        file: str  # Django file.name relative to media root

We override the ``file`` prepare-produce pair to include the file field and
then extract the field name from the ORM instance.


Annotations
-----------

Another example is providing ORM annotations, either for aggregations or to
include a field value from a parent model::

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

    @overrides({
        "username": (
            qs.annotate(username=F("user__username")),
            producers.attr("username"),
        )
    })
    @define
    class BookmarkData:
        id: int
        title: str
        favourite: bool
        username: str

Here we want to include the ``username`` for the parent user for our
``Bookmark``. We annotate the QuerySet at the prepare phase, and then use that
value for our final data.


Disable a field
---------------

Sometimes we might have an ``attrs`` field that is set in application code, and
not via the data from the ORM. In this case we can exclude the field from the
readers spec by setting the override to ``None``::

    @overrides({
        "related_bookmarks": None,
    })
    @define
    class BookmarkData:
        ...

        related_bookmarks: List[BookmarkData] = field(default=list)

Here ``related_bookmarks`` will initially be an empty list, provided by the
field default, and would be set subsequently by your application.


Reshaping data
--------------

Finally, for a much more complex example, we may wish to reshape our data
significantly, providing a manual spec and code to map that to our data shape.

Imagine an Django ``User`` model, with a related ``Profile``, that has an
``avatar_image`` ``ImageField``.

We want to map that to a flat ``UserInfo`` shape with only ``name``, ``email``,
and ``avatar_url`` attributes.

Our problem is that readers specs naturally follow the model relationships, so
the raw projected data is still nested. Our task is to flatten it.

1. Define a model-shaped spec on your target class
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Here we pass a readers spec manually using the ``@spec`` decorator:

.. code-block:: python

   from attrs import define
   from django_readers import qs
   from mantle.readers import spec


   @spec([
       {
           "name": (
               qs.include_fields("first_name", "last_name"),
               lambda user: user.get_full_name(),
           )
       },
       "email",
       {
           "profile": [
               {
                   "avatar_image": [
                       "url",
                   ]
               }
           ]
       },
   ])
   @define
   class UserInfo:
       name: str
       email: str
       avatar_url: str

The ``name`` field is using the ``lambda`` technique again to call the ``User`` model's ``get_full_name()`` method.

The ``email`` field is just a plain string, which is the readers shortcut for "include this field, and then select it".

The ``profile`` field is a nested spec for the profile reaching in the the ``avatar_image`` and the ``url`` property of that. Coming out of django-readers, the data for the profile will be a nested dictionaries down to the ``url`` field we're interested in.

2. Add a classmethod to reshape spec output
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

In order to flatten the data we add a classmethod to create our UserInfo
instance from the expected data:

.. code-block:: python

   @spec(...)  # Not shown again here
   @define
   class UserInfo:
       name: str
       email: str
       avatar_url: str

       @classmethod
       def from_spec_data(cls, data: dict) -> "UserInfo":
           profile = data.get("profile") or {}
           avatar_image = profile.get("avatar_image") or {}
           return cls(
               name=data["name"],
               email=data["email"],
               avatar_url=avatar_image.get("url", ""),
           )

Here ``from_spec_data`` defensively extracts the ``avatar_url`` we need.


3. Use a custom cattrs converter to structure the data
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Now we need to register our new method with ``cattrs`` so it knows to use the classmethod for structuring ``UserInfo`` instances.

This is a technique described in the cattrs docs here: `Using class-specific
structure and unstructure methods
<https://catt.rs/en/stable/strategies.html#using-class-specific-structure-and-
unstructure-methods>`_.

.. code-block:: python

   from mantle.db import make_default_converter


   user_info_converter = make_default_converter()


   @user_info_converter.register_structure_hook
   def structure_user_info(data: dict, _) -> UserInfo:
       return UserInfo.from_spec_data(data)

We start from Mantle's ``make_default_converter``, which defines structure
hooks for common Django value types, including ``date``, ``datetime``,
``time``, ``timedelta``, ``UUID``, and ``Decimal``.


4. Fetch with ``Query``
~~~~~~~~~~~~~~~~~~~~~~~

Finally, we fetch with ``Query``, making sure use our custom converter:

.. code-block:: python

   from mantle import Query
   from django.contrib.auth.models import User


   user_info = Query(
       User.objects.filter(pk=user_id),
       UserInfo,
       converter=user_info_converter,
   ).get()

By using ``@overrides`` or by defining the spec manually with ``@spec``, and by
customising the ``cattrs`` converter, you have full control over the query
generation and structuring process.
