Skip to content

perf(geo): optimize the geoFeatures connection (44 → 5 queries) - #471

Merged
nossila merged 1 commit into
feature/baseapp_geofrom
feature/baseapp_geo-query-optimization
Oct 8, 2026
Merged

nossila merged 1 commit into
feature/baseapp_geofrom
feature/baseapp_geo-query-optimization

Conversation

@nossila

@nossila nossila commented Oct 8, 2026

Copy link
Copy Markdown
Member

Problem

geoFeatures used graphene_django.filter.DjangoFilterConnectionField. The query_optimizer compiler never runs under it, so pre_optimization_hook and every field optimizer_hook are dead code — which compose-a-block's connection-fields.md names as the trap that silently discards the whole optimization layer.

Measured on a 10-feature page selecting every field: 44 queries, growing linearly with rows. Three separate N+1s, found by reading the captured SQL rather than guessing:

  1. Every column behind properties. GeoJSONFeatureProperties is a plain graphene.ObjectType resolved by attribute access on the feature, so the optimizer cannot tell that properties { featureType } reads a model column. It deferred them, and each access fanned out into refresh_from_db(fields=['feature_type']) — one round trip per field per row.
  2. The generic target. target walks target_document.content_object: one query for the DocumentId, one for the target.
  3. The target's own relay id. DocumentId.get_public_id_from_object ran once per target, even after the target itself was prefetched.

Changes

  • geo_features → query_optimizer.DjangoConnectionField, with filterset_class moved onto the ObjectType's Meta where that field reads it from. max_limit=100 unchanged.
  • GeoJSONFeatureObjectType.pre_optimization_hook: select_related the target document and its content type, prefetch_related the generic target (one query per content type, not per row), and push into only_fields the columns resolvers read but GraphQL defers — geometry, target_document_id, and every properties column.
  • resolve_target goes through baseapp_core's request-scoped document cache and seeds the target's mapped_public_id from the DocumentId already loaded, which is what PublicIdResolver reads instead of querying.

44 → 5 queries, flat as rows grow.

Testing

  • New baseapp/geo/tests/test_graphql_query_counts.py: pins the count at 5 with a per-query explanation, asserts it does not change from 10 to 30 features, and covers features sharing one target.
  • baseapp/geo/tests/: 109 passed. black, isort, flake8 clean.
  • test_max_limit_caps_page_size_at_100 now matches query_optimizer's wording (Requesting first 200 records exceeds the limit of 100.). The cap it asserts is unchanged — only the library emitting the message changed.

Notes for the reviewer

This activates hooks that were previously inert, so it changes SQL by design. The count test is the guard; per connection-fields.md a jump in that number means something reverted to the unoptimized field.

Not addressed here: baseapp/files, content_feed and activity_log are still on the unoptimized connection field. That's pre-existing repo-wide debt, not geo's.

🤖 Generated with Claude Code

https://claude.ai/code/session_015fczpSCpN3kCRs3UBjbity

geoFeatures used graphene_django's DjangoFilterConnectionField, under which the
query_optimizer compiler never runs. A 10-feature page selecting every field cost 44
queries; it now costs 5, and the count no longer grows with the number of rows.

- Switch geo_features to query_optimizer.DjangoConnectionField and move filterset_class
  onto the ObjectType's Meta, where that field reads it from. max_limit=100 is unchanged.
- Add GeoJSONFeatureObjectType.pre_optimization_hook: select_related the target document
  and its content type, prefetch the generic target in one query per content type, and
  force into only_fields the columns the resolvers read but GraphQL defers. Everything
  behind `properties` was deferred, because GeoJSONFeatureProperties is a plain graphene
  type the optimizer cannot map back to model columns, so each field refetched per row.
- Resolve properties.target through baseapp_core's request-scoped document cache, seeding
  the target's mapped_public_id from the DocumentId already loaded. Without it the
  target's own relay id was one DocumentId query per feature.
- Add test_graphql_query_counts.py pinning the count at 5, asserting it stays flat from 10
  to 30 features, and covering the shared-target case.

test_max_limit_caps_page_size_at_100 now matches query_optimizer's wording; the cap it
asserts (100) is unchanged.

baseapp/geo/tests: 109 passed. black, isort and flake8 clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015fczpSCpN3kCRs3UBjbity
@coderabbitai

coderabbitai Bot commented Oct 8, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 31a276a2-c486-4439-bffe-7710cb4094b3

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@nossila
nossila merged commit 363ece6 into feature/baseapp_geo Oct 8, 2026
3 of 5 checks passed
@nossila
nossila deleted the feature/baseapp_geo-query-optimization branch October 8, 2026 20:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant