This PR completes the Sprint 2 public api-v2 surface and includes the follow-up performance work needed to keep the new endpoints usable against production-scale data.
Included in this branch:
series browse payloads and add the v2 browse indexissues.on_sale_iso_weekuniverses endpointsgroups endpointscharacters endpointscreators endpointscharacters, groups, and issuesCode-level verification:
ruff check passed on changed filesruff format --check passed on changed filespython manage.py check passed in gcd-django-docker-web-1Manual verification against the running production-copy DB:
200Representative perf improvements from the manual harness:
issues modified__gt=2025-01-01: 2564.6ms -> 48.0msissues variant_only: 798.0ms -> 85.5mscharacters list: 213.5ms -> 48.2mscharacters language=en: 181.1ms -> 77.9msgroups list: 180.0ms -> 98.0mshttps://github.com/GrandComicsDatabase/gcd-django/pull/713
(55 files)
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@DeusExTaco pushed 1 commit.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@gemini-code-assist[bot] commented on this pull request.
This pull request adds new v2 API endpoints for characters, creators, groups, and universes, and optimizes existing issue and series endpoints with specialized pagination and database indexing. The review feedback recommends enhancing robustness against non-numeric legacy data in integer casts, using .get() instead of .first() for unique lookups to ensure data integrity, and utilizing queryset intersections for more efficient issue filtering.
In apps/api_v2/filters/creators.py:
> + When(**{f'{field_name}__isnull': True}, then=Value(0)),
+ When(**{f'{field_name}__{component}': ''}, then=Value(0)),
+ default=F(f'{field_name}__{component}'),
+ output_field=IntegerField(),
Using F() with output_field=IntegerField() on CharField fields like year, month, and day can cause runtime errors if the database contains non-numeric strings (e.g., legacy markers like '????' or '1940?'). While MySQL might implicitly cast these to an integer (e.g., '1940?' to 1940), other databases like PostgreSQL will raise an error. Consider explicitly handling non-numeric values in the When clauses to ensure cross-database compatibility and robustness against legacy data.
In apps/api_v2/filters/characters.py:
> + language_id = (
+ Language.objects.filter(code=value)
+ .values_list('id', flat=True)
+ .first()
+ )
As per the general rules, prefer using .get() instead of .first() when the data model expects a single object. This helps identify data integrity issues (like duplicate language codes) by raising a MultipleObjectsReturned exception. You will need to handle the DoesNotExist exception to maintain the current logic for invalid codes.
try: language_id = Language.objects.values_list('id', flat=True).get(code=value) except Language.DoesNotExist: language_id = None
In apps/api_v2/filters/issues.py:
> @@ -110,3 +142,13 @@ def filter_variant_of(self, queryset, name, value):
"""Filter by whether an issue is a variant."""
del name
return queryset.filter(variant_of__isnull=not value)
+
+ def filter_on_sale_iso_week(self, queryset, name, value):
+ """Filter issues to the Monday-Sunday window for an ISO week."""
+ del name
+ year_text, week_text = value.split('-W')
+ week_qs, _monday, _sunday = issues_for_iso_week(
+ int(year_text),
+ int(week_text),
+ )
+ return queryset.filter(pk__in=week_qs.values('pk'))
Since both queryset and week_qs are querysets for the Issue model, you can use the intersection operator & to combine them. This is generally more efficient than a pk__in subquery as it allows the database to optimize the combined WHERE clause directly, which aligns with the performance goals of this PR.
- return queryset.filter(pk__in=week_qs.values('pk'))
+ return queryset & week_qs
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@DeusExTaco commented on this pull request.
In apps/api_v2/filters/issues.py:
> @@ -110,3 +142,13 @@ def filter_variant_of(self, queryset, name, value):
"""Filter by whether an issue is a variant."""
del name
return queryset.filter(variant_of__isnull=not value)
+
+ def filter_on_sale_iso_week(self, queryset, name, value):
+ """Filter issues to the Monday-Sunday window for an ISO week."""
+ del name
+ year_text, week_text = value.split('-W')
+ week_qs, _monday, _sunday = issues_for_iso_week(
+ int(year_text),
+ int(week_text),
+ )
+ return queryset.filter(pk__in=week_qs.values('pk'))
Roger that...I switched this over to queryset intersection instead of the pk__in subquery.
That keeps the generated SQL as a direct on_sale_date predicate, including the split month-
boundary week case, which is a better fit for the perf work in this PR. I also added a small
regression test around the SQL shape.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@DeusExTaco commented on this pull request.
In apps/api_v2/filters/characters.py:
> + language_id = (
+ Language.objects.filter(code=value)
+ .values_list('id', flat=True)
+ .first()
+ )
I’m going to leave this one as-is.
Language.code is already unique in the schema, so .get() doesn’t really buy us anything in the
normal case here. The behavior I want on bad query params is still “no matches” rather than an
exception path, and the current code keeps that nice and soft while also letting us cache the
resolved language_id on the request.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@DeusExTaco pushed 2 commits.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@DeusExTaco commented on this pull request.
In apps/api_v2/filters/creators.py:
> + When(**{f'{field_name}__isnull': True}, then=Value(0)),
+ When(**{f'{field_name}__{component}': ''}, then=Value(0)),
+ default=F(f'{field_name}__{component}'),
+ output_field=IntegerField(),
This was an interesting one....
The current filter logic was relying on implicit integer coercion from the stored Date char
fields, which gets weird with legacy values like ????, 19??, and 200?. I changed the range
filters so they only build sort keys from numeric stored components and ignore non-comparable
legacy partial dates instead of trying to coerce them.
That keeps the behavior consistent for birth_date__gte/lte and death_date__gte/lte, and avoids
those uncertain rows leaking into lte results.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@jochengcd commented on this pull request.
In apps/api_v2/filters/creators.py:
> + When(**{f'{field_name}__isnull': True}, then=Value(0)),
+ When(**{f'{field_name}__{component}': ''}, then=Value(0)),
+ default=F(f'{field_name}__{component}'),
+ output_field=IntegerField(),
These are not legacy values, but valid data in our database. If we know only partial information we fill these with ?. So 200? means sometime between 2000 and 2009.
For filtering we use these in the site.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@jochengcd commented on this pull request.
In apps/api_v2/filters/issues.py:
> @@ -110,3 +142,13 @@ def filter_variant_of(self, queryset, name, value):
"""Filter by whether an issue is a variant."""
del name
return queryset.filter(variant_of__isnull=not value)
+
+ def filter_on_sale_iso_week(self, queryset, name, value):
+ """Filter issues to the Monday-Sunday window for an ISO week."""
+ del name
+ year_text, week_text = value.split('-W')
+ week_qs, _monday, _sunday = issues_for_iso_week(
+ int(year_text),
+ int(week_text),
+ )
+ return queryset.filter(pk__in=week_qs.values('pk'))
More out of curiosity, did you do time measurements here ?
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@DeusExTaco commented on this pull request.
In apps/api_v2/filters/issues.py:
> @@ -110,3 +142,13 @@ def filter_variant_of(self, queryset, name, value):
"""Filter by whether an issue is a variant."""
del name
return queryset.filter(variant_of__isnull=not value)
+
+ def filter_on_sale_iso_week(self, queryset, name, value):
+ """Filter issues to the Monday-Sunday window for an ISO week."""
+ del name
+ year_text, week_text = value.split('-W')
+ week_qs, _monday, _sunday = issues_for_iso_week(
+ int(year_text),
+ int(week_text),
+ )
+ return queryset.filter(pk__in=week_qs.values('pk'))
I went back and measured this on the local production-copy DB instead of just relying on the SQL shape. I compared the old pk__in version with the current queryset intersection, using 3 warmup runs and 12 measured runs for each, and the intersection version was consistently faster, so I think this is still the right change to keep.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@DeusExTaco commented on this pull request.
In apps/api_v2/filters/creators.py:
> + When(**{f'{field_name}__isnull': True}, then=Value(0)),
+ When(**{f'{field_name}__{component}': ''}, then=Value(0)),
+ default=F(f'{field_name}__{component}'),
+ output_field=IntegerField(),
Good catch here. I had made the filter too strict while fixing the cast issue. I’ve updated the v2 creator date filtering so partial years like 19?? and 200? are treated as bounded ranges again instead of getting dropped, while fully unknown values like ???? still stay out of date-range matches. I also added coverage for the filter and endpoint behavior and spot-checked it against the local production-copy DB.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
@DeusExTaco pushed 1 commit.
—
View it on GitHub or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()