From 95eee095e1160b89cdec2cffcb8392d0d0d8327f Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 27 Mar 2020 18:13:28 +0000 Subject: [PATCH 1/8] Add Query Optimzier that selects fields from query AST --- example/db.sqlite3 | 4 +- example/example/settings/base.py | 5 + example/example/urls.py | 2 +- example/home/models.py | 2 + grapple/apps.py | 14 +- grapple/db/__init__.py | 0 grapple/db/optimizer.py | 243 +++++++++++++++++++++++++++++++ grapple/db/query.py | 88 +++++++++++ grapple/types/pages.py | 62 +++++--- grapple/urls.py | 7 +- grapple/utils.py | 8 +- 11 files changed, 403 insertions(+), 32 deletions(-) create mode 100644 grapple/db/__init__.py create mode 100644 grapple/db/optimizer.py create mode 100644 grapple/db/query.py diff --git a/example/db.sqlite3 b/example/db.sqlite3 index 349515a1..21dc7330 100644 --- a/example/db.sqlite3 +++ b/example/db.sqlite3 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a4d887725df3fca2e2d7703868ba9b90538f49506f9b3ea57515dee785ca90a -size 872448 +oid sha256:3cf55d959bac4d54de5cb03962245fcde1e0d8e68612be540464948bf26b1dd7 +size 987136 diff --git a/example/example/settings/base.py b/example/example/settings/base.py index f7a7a2a9..630b98b4 100644 --- a/example/example/settings/base.py +++ b/example/example/settings/base.py @@ -54,6 +54,7 @@ "grapple", "graphene_django", "channels", + "django_extensions", ] MIDDLEWARE = [ @@ -178,3 +179,7 @@ "ROUTING": "grapple.urls.channel_routing", } } + +# Query Optimisation helpers +SHELL_PLUS_PRINT_SQL = True +RUNSERVER_PLUS_PRINT_SQL_TRUNCATE = 100000 diff --git a/example/example/urls.py b/example/example/urls.py index bad90cf6..05fe60d1 100644 --- a/example/example/urls.py +++ b/example/example/urls.py @@ -10,6 +10,7 @@ from search import views as search_views urlpatterns = [ + url(r"", include(grapple_urls)), url(r"^django-admin/", admin.site.urls), url(r"^admin/", include(wagtailadmin_urls)), url(r"^documents/", include(wagtaildocs_urls)), @@ -18,7 +19,6 @@ # Wagtail's page serving mechanism. This should be the last pattern in # the list: url(r"", include(wagtail_urls)), - url(r"", include(grapple_urls)), # Alternatively, if you want Wagtail pages to be served from a subpath # of your site, rather than the site root: # url(r'^pages/', include(wagtail_urls)), diff --git a/example/home/models.py b/example/home/models.py index 2fa9e18a..2bc2d30a 100644 --- a/example/home/models.py +++ b/example/home/models.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from django.db import models from modelcluster.fields import ParentalKey @@ -6,6 +7,7 @@ from wagtail.contrib.settings.models import BaseSetting, register_setting from wagtail.core import blocks +from wagtail.core.fields import RichTextField, StreamField from wagtail.admin.edit_handlers import FieldPanel, StreamFieldPanel, InlinePanel from wagtail.images.blocks import ImageChooserBlock from wagtail.documents.blocks import DocumentChooserBlock diff --git a/grapple/apps.py b/grapple/apps.py index a8ea971d..f05b7ef5 100644 --- a/grapple/apps.py +++ b/grapple/apps.py @@ -5,13 +5,21 @@ class Grapple(AppConfig): name = "grapple" def ready(self): - """ - Import all the django apps defined in django settings then process each model - in these apps and create graphql node types from them. + """ + Import all the django apps defined in django settings then process each model + in these apps and create graphql node types from them. """ from .actions import import_apps, load_type_fields from .types.streamfield import register_streamfield_blocks + self.preload_tasks() import_apps() load_type_fields() register_streamfield_blocks() + + def preload_tasks(self): + # Monkeypatch Wagtails' PageQueryset .specific method to a more optimized one + from wagtail.core.query import PageQuerySet + from .db.query import specific + + PageQuerySet.specific = specific diff --git a/grapple/db/__init__.py b/grapple/db/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/grapple/db/optimizer.py b/grapple/db/optimizer.py new file mode 100644 index 00000000..26e4f5b9 --- /dev/null +++ b/grapple/db/optimizer.py @@ -0,0 +1,243 @@ +import re +from collections.abc import Iterable +from django.db.models.query_utils import DeferredAttribute +from django.db.models.fields.related_descriptors import ( + ReverseOneToOneDescriptor, + ReverseManyToOneDescriptor, + ForwardManyToOneDescriptor, + ForwardOneToOneDescriptor, + ReverseOneToOneDescriptor, +) +from graphene.types.definitions import GrapheneInterfaceType +from graphql.language.ast import ( + Field, + InlineFragment, + FragmentSpread, + InterfaceTypeDefinition, +) + +from modelcluster.fields import ParentalKey +from django.db.models.fields.related import ForeignKey + +pascal_to_snake = re.compile(r"(? Date: Fri, 27 Mar 2020 18:29:22 +0000 Subject: [PATCH 2/8] Add django_extensions to example pip --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index b63d86a2..3b8f44ef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ factory-boy==2.12.0 wagtail-factories==2.0.0 django-cors-headers==3.0.2 wagtail-headless-preview==0.1.4 +django_extensions==2.2.9 From 641bcbaa63680b40b90091a18f01f7b65c80ef31 Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 27 Mar 2020 18:32:39 +0000 Subject: [PATCH 3/8] Remove renduntant changes --- example/example/urls.py | 2 +- grapple/types/pages.py | 57 ++++++++++++++++++----------------------- grapple/utils.py | 1 - 3 files changed, 26 insertions(+), 34 deletions(-) diff --git a/example/example/urls.py b/example/example/urls.py index 05fe60d1..bad90cf6 100644 --- a/example/example/urls.py +++ b/example/example/urls.py @@ -10,7 +10,6 @@ from search import views as search_views urlpatterns = [ - url(r"", include(grapple_urls)), url(r"^django-admin/", admin.site.urls), url(r"^admin/", include(wagtailadmin_urls)), url(r"^documents/", include(wagtaildocs_urls)), @@ -19,6 +18,7 @@ # Wagtail's page serving mechanism. This should be the last pattern in # the list: url(r"", include(wagtail_urls)), + url(r"", include(grapple_urls)), # Alternatively, if you want Wagtail pages to be served from a subpath # of your site, rather than the site root: # url(r'^pages/', include(wagtail_urls)), diff --git a/grapple/types/pages.py b/grapple/types/pages.py index d7719658..7644ca08 100644 --- a/grapple/types/pages.py +++ b/grapple/types/pages.py @@ -118,43 +118,38 @@ class Meta: interfaces = (PageInterface,) -def get_specific_page(id, slug, url, token, content_type=None, info=None): +def get_specific_page(id, slug, token, content_type=None, info=None): """ Get a spcecific page, also get preview if token is passed """ page = None - # try: - # Generate queryset based on given key & Optimise query based on request AST - pages = WagtailPage.objects.live().public() - pages = QueryOptimzer.query(pages, info) - if id: - page = pages.get(id=id) - elif slug: - page = pages.get(slug=slug) - elif url: - for matching_page in pages.filter(url_path__contains=url): - if matching_page.url == url: - page = matching_page - break - - if page: - page = page.specific - - if token: + try: + # Generate queryset based on given key & Optimise query based on request AST + pages = WagtailPage.objects.live().public() + pages = QueryOptimzer.query(pages, info) + if id: + page = pages.get(id=id) + elif slug: + page = pages.get(slug=slug) + if page: - page_type = type(page) - if hasattr(page_type, "get_page_from_preview_token"): - page = page_type.get_page_from_preview_token(token) + page = page.specific + + if token: + if page: + page_type = type(page) + if hasattr(page_type, "get_page_from_preview_token"): + page = page_type.get_page_from_preview_token(token) - elif content_type: - app_label, model = content_type.lower().split(".") - mdl = ContentType.objects.get(app_label=app_label, model=model) - cls = mdl.model_class() - if hasattr(cls, "get_page_from_preview_token"): - page = cls.get_page_from_preview_token(token) + elif content_type: + app_label, model = content_type.lower().split(".") + mdl = ContentType.objects.get(app_label=app_label, model=model) + cls = mdl.model_class() + if hasattr(cls, "get_page_from_preview_token"): + page = cls.get_page_from_preview_token(token) - # except BaseException: - # page = None + except BaseException: + page = None return page @@ -169,7 +164,6 @@ class Mixin: PageInterface, id=graphene.Int(), slug=graphene.String(), - url=graphene.String(), token=graphene.String(), content_type=graphene.String(), ) @@ -185,7 +179,6 @@ def resolve_page(self, info, **kwargs): return get_specific_page( id=kwargs.get("id"), slug=kwargs.get("slug"), - url=kwargs.get("url"), token=kwargs.get("token"), content_type=kwargs.get("content_type"), info=info, diff --git a/grapple/utils.py b/grapple/utils.py index f07070f7..c4beddb1 100644 --- a/grapple/utils.py +++ b/grapple/utils.py @@ -1,7 +1,6 @@ import os import base64 import tempfile -import graphene_django_optimizer as gql_optimizer from PIL import Image, ImageFilter from django.conf import settings from wagtail.search.index import class_is_indexed From e21e9f1de4ae2db15f1db1b7b65c603476954f50 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 17 Jun 2020 20:32:16 +0100 Subject: [PATCH 4/8] =?UTF-8?q?Fix=20optimizer=20so=20it=20works=20with=20?= =?UTF-8?q?dynamic=20graphql=20root=20schema=E2=80=99s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- grapple/db/optimizer.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/grapple/db/optimizer.py b/grapple/db/optimizer.py index 26e4f5b9..484a5238 100644 --- a/grapple/db/optimizer.py +++ b/grapple/db/optimizer.py @@ -60,7 +60,7 @@ def query(qs, info): # Sort the requested fields, depending on relation to model. def sort_fields(self): - for field_name in self.query_fields: + for field_name in self.query_fields or []: # Make sure field name is snake not pascal (graphene converts them that way) field_name = pascal_to_snake.sub("_", field_name).lower() @@ -169,6 +169,10 @@ def get_pages_interface(self): return field def parse_field(self, field, field_prefix): + # Don't crash if the field isn't parsable... + if field is None: + return + field_name = field.name.value # If field has subset fields From d7730f788596196d0156933268eb00c67ff7a2d4 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 18 Jun 2020 18:22:59 +0100 Subject: [PATCH 5/8] Fix issue with calling property functions --- example/db.sqlite3 | 2 +- .../migrations/0015_auto_20200618_1452.py | 20 +++++++++++++++++++ example/home/models.py | 2 +- grapple/db/optimizer.py | 4 ++++ grapple/db/query.py | 2 +- 5 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 example/home/migrations/0015_auto_20200618_1452.py diff --git a/example/db.sqlite3 b/example/db.sqlite3 index 21dc7330..b38c40eb 100644 --- a/example/db.sqlite3 +++ b/example/db.sqlite3 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3cf55d959bac4d54de5cb03962245fcde1e0d8e68612be540464948bf26b1dd7 +oid sha256:238f6c6864e7e0eb078d2fba9d7b61b81aca45a15f8d2910b67c2915b037bb7d size 987136 diff --git a/example/home/migrations/0015_auto_20200618_1452.py b/example/home/migrations/0015_auto_20200618_1452.py new file mode 100644 index 00000000..1d8455dd --- /dev/null +++ b/example/home/migrations/0015_auto_20200618_1452.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.13 on 2020-06-18 14:52 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('images', '0001_initial'), + ('home', '0014_auto_20200303_1234'), + ] + + operations = [ + migrations.AlterField( + model_name='blogpage', + name='cover', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='images.CustomImage'), + ), + ] diff --git a/example/home/models.py b/example/home/models.py index 673884f0..e1ddc293 100644 --- a/example/home/models.py +++ b/example/home/models.py @@ -58,7 +58,7 @@ class BlogPage(HeadlessPreviewMixin, Page): related_name="+", ) cover = models.ForeignKey( - "wagtailimages.Image", + "images.CustomImage", null=True, blank=True, on_delete=models.SET_NULL, diff --git a/grapple/db/optimizer.py b/grapple/db/optimizer.py index 484a5238..f763aa8d 100644 --- a/grapple/db/optimizer.py +++ b/grapple/db/optimizer.py @@ -98,6 +98,10 @@ def select_field(self, field, field_name, field_name_prefix=None): if hasattr(model, "id"): self.only_fields.append("id") + # Don't select property functions + if isinstance(field, property): + return + if not getattr(field, "is_relation", False): if model: # Cache selection for future optimisation (query.py) diff --git a/grapple/db/query.py b/grapple/db/query.py index dfa62141..06a75939 100644 --- a/grapple/db/query.py +++ b/grapple/db/query.py @@ -73,7 +73,7 @@ def specific_iterator(qs, defer=True): # Query pages pages = specific_model.objects.filter(pk__in=pks) # Defer all fields apart from those required - pages = pages.only(*only_fields, *only_fields_specific) + pages = pages.only(*only_fields, *only_fields_specific, *select_related_fields) # Apply select_related fields (passed down from optimizer.py) pages = pages.select_related(*select_related_fields) # Apply prefetch_related fields (passed down from optimizer.py) From 2bb0d54056e49f0c2bc577c67cad42d1f3977963 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 7 Jul 2020 21:20:32 +0100 Subject: [PATCH 6/8] Ignore __typename field as it causes null reponse --- grapple/db/optimizer.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/grapple/db/optimizer.py b/grapple/db/optimizer.py index f763aa8d..d90fdca6 100644 --- a/grapple/db/optimizer.py +++ b/grapple/db/optimizer.py @@ -64,6 +64,10 @@ def sort_fields(self): # Make sure field name is snake not pascal (graphene converts them that way) field_name = pascal_to_snake.sub("_", field_name).lower() + # Ignore typename that causes null error + if field_name == "__typename": + continue + # Support using simple field field = getattr(self.model, field_name, None) if field: From 079148ba904a680ec1a858ff4497db120d46837a Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 8 Jul 2020 16:44:47 +0100 Subject: [PATCH 7/8] Allow processing of fragments --- grapple/db/optimizer.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/grapple/db/optimizer.py b/grapple/db/optimizer.py index d90fdca6..2783433b 100644 --- a/grapple/db/optimizer.py +++ b/grapple/db/optimizer.py @@ -223,9 +223,9 @@ def parse_fragment_spread(self, fragment_spread, field_prefix): # Get fragment type from name and return it parsed. fragment_type = self.fragments[fragment_name] - return self.parse_inline_fragment(fragment_type, field_prefix) + return self.parse_inline_fragment(fragment_type, field_prefix, is_fragment_spread = True) - def parse_inline_fragment(self, inline_fragment, field_prefix): + def parse_inline_fragment(self, inline_fragment, field_prefix, is_fragment_spread = False): # Get type of inline fragment gql_type_name = inline_fragment.type_condition.name.value gql_type = self.schema.get_type(gql_type_name) @@ -248,7 +248,8 @@ def prefix_type(field): selections = [] if inline_fragment.selection_set: selections = self.parse_selection_set( - inline_fragment.selection_set, type_prefix + inline_fragment.selection_set, + field_prefix if is_fragment_spread else type_prefix ) selections = list(map(prefix_type, selections)) From b44b3f31ffc96f9670732b4d3e1ad5f659ca7836 Mon Sep 17 00:00:00 2001 From: Rui Saraiva Date: Tue, 24 Nov 2020 23:52:43 +0000 Subject: [PATCH 8/8] Fix issue with calling method functions --- example/db.sqlite3 | Bin 851968 -> 851968 bytes grapple/db/optimizer.py | 4 ++++ 2 files changed, 4 insertions(+) diff --git a/example/db.sqlite3 b/example/db.sqlite3 index 1090f76b294dfbe0e555183059ba87c72e32ca23..aaa854ffad9a68a289c0e0206817fbac6a749355 100644 GIT binary patch delta 4765 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 Hpk@C8dFUJS diff --git a/grapple/db/optimizer.py b/grapple/db/optimizer.py index dc95ad81..baf2af24 100644 --- a/grapple/db/optimizer.py +++ b/grapple/db/optimizer.py @@ -98,6 +98,10 @@ def select_field(self, field, field_name, field_name_prefix=None): if isinstance(field, property): return + # Don't select method functions + if callable(field): + return + if not getattr(field, "is_relation", False): if model: # Cache selection for future optimization (query.py)