From 28ae4ff0bbb901b1624ed45a5195762e131d16f1 Mon Sep 17 00:00:00 2001
From: Adamos Kyriakou <somada141@gmail.com>
Date: Wed, 3 Oct 2018 13:43:42 +1000
Subject: [PATCH] Added an `Aggregation` section to the `tips` documentation
 page.

---
 docs/tips.rst | 183 ++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 183 insertions(+)

diff --git a/docs/tips.rst b/docs/tips.rst
index 1e8d0a6c..d110b48b 100644
--- a/docs/tips.rst
+++ b/docs/tips.rst
@@ -85,3 +85,186 @@ some of the allowed queries are
         }
     }
 
+
+Aggregating
+-----------
+
+By default the `sqlalchemy.orm.Query` object that is retrieved through the `SQLAlchemyObjectType.get_query` method auto-selects the underlying SQLAlchemy ORM class. In order to change fields under the `SELECT` statement, e.g., when performing an aggregation, one can retrieve the `sqlalchemy.orm.Session` object from the provided `info` argument and create a new query as such:
+
+.. code::
+
+    session = info.context.get("session")  # type: sqlalchemy.orm.Session
+    query = session.query(SomeOtherModel, some_aggregation_function)
+
+Consider the following SQLAlchemy ORM models:
+
+.. code::
+
+    class Author(Base):
+        __tablename__ = "authors"
+
+        author_id = sqlalchemy.Column(
+            sqlalchemy.types.Integer(),
+            primary_key=True,
+        )
+
+        name_first = sqlalchemy.Column(
+            sqlalchemy.types.Unicode(length=80),
+            nullable=False,
+        )
+
+        name_last = sqlalchemy.Column(
+            sqlalchemy.types.Unicode(length=80),
+            nullable=False,
+        )
+
+        books = sqlalchemy.orm.relationship(
+            argument="Book",
+            secondary="author_books",
+            back_populates="authors",
+        )
+
+
+    class Book(Base):
+        __tablename__ = "books"
+
+        book_id = sqlalchemy.Column(
+            sqlalchemy.types.Integer(),
+            primary_key=True,
+        )
+
+        title = sqlalchemy.Column(
+            sqlalchemy.types.Unicode(length=80),
+            nullable=False,
+        )
+
+        year = sqlalchemy.Column(
+            sqlalchemy.types.Integer(),
+            nullable=False,
+        )
+
+        cover_artist = sqlalchemy.Column(
+            sqlalchemy.types.Unicode(length=80),
+            nullable=True,
+        )
+
+        authors = sqlalchemy.orm.relationship(
+            argument="Author",
+            secondary="author_books",
+            back_populates="books",
+        )
+
+
+    class AuthorBook(Base):
+        __tablename__ = "author_books"
+
+        author_book_id = sqlalchemy.Column(
+            sqlalchemy.types.Integer(),
+            primary_key=True,
+        )
+
+        author_id = sqlalchemy.Column(
+            sqlalchemy.types.Integer(),
+            sqlalchemy.ForeignKey("authors.author_id"),
+            index=True,
+        )
+
+        book_id = sqlalchemy.Column(
+            sqlalchemy.types.Integer(),
+            sqlalchemy.ForeignKey("books.book_id"),
+            index=True,
+        )
+
+exposed to the GraphQL schema through the following types:
+
+.. code::
+
+    class TypeAuthor(SQLAlchemyObjectType):
+        class Meta:
+            model = Author
+
+
+    class TypeBook(SQLAlchemyObjectType):
+        class Meta:
+            model = Book
+
+
+    class TypeAuthorBook(SQLAlchemyObjectType):
+        class Meta:
+            model = AuthorBook
+
+If we wanted to perform an aggregation, e.g., count the number of books by cover-artist, we'd first define such a custom type:
+
+.. code::
+
+    class TypeCountBooksCoverArtist(graphene.ObjectType):
+        cover_artist = graphene.String()
+        count_books = graphene.Int()
+
+which we can then expose through a class deriving `graphene.ObjectType` as follows:
+
+.. code::
+
+    class TypeStats(graphene.ObjectType):
+
+        count_books_by_cover_artist = graphene.List(
+            of_type=TypeCountBooksCoverArtist
+        )
+
+        @staticmethod
+        def resolve_count_books_by_cover_artist(
+            args: Dict,
+            info: graphql.execution.base.ResolveInfo,
+        ) -> List[TypeCountBooksCoverArtist]:
+            # Retrieve the session out of the context as the `get_query` method
+            # automatically selects the model.
+            session = info.context.get("session")  # type: sqlalchemy.orm.Session
+
+            # Define the `COUNT(books.book_id)` function.
+            func_count_books = sqlalchemy_func.count(Book.book_id)
+
+            # Query out the count of books by cover-artist
+            query = session.query(Book.cover_artist, func_count_books)
+            query = query.group_by(Book.cover_artist)
+            results = query.all()
+
+            # Wrap the results of the aggregation in `TypeCountBooksCoverArtist`
+            # objects.
+            objs = [
+                TypeCountBooksCoverArtist(
+                    cover_artist=result[0],
+                    count_books=result[1]
+                ) for result in results
+            ]
+
+            return objs
+
+As can be seen, the `sqlalchemy.orm.Session` object is retrieved from the `info.context` and a new query specifying the desired field and aggregation function is defined. The results of the aggregation do not directly correspond to an ORM class so they're wrapped in the `TypeCountBooksCoverArtist` class and returned.
+
+The `TypeStats` class can then be exposed under the `Query` class as such:
+
+.. code::
+
+    class Query(graphene.ObjectType):
+
+        stats = graphene.Field(type=TypeStats)
+
+        @staticmethod
+        def resolve_stats(
+            args: Dict,
+            info: graphql.execution.base.ResolveInfo,
+        ):
+            return TypeStats
+
+thus allowing for the following query:
+
+.. code::
+
+    query getCountBooksByCoverArtist{
+      stats {
+        countBooksByCoverArtist {
+          coverArtist,
+          countBooks
+        }
+      }
+    }