GSoC 2026: Add support for generate_series in postgres (Draft Proposal Feedback)

Hi,
I am Samriddha. I am interested in distributed systems and understanding how databases work internally, which is why I enjoy working on ORM-related issues in Django.
This is my open source work till now (mainly in Django) - GitHub - Samriddha9619/github-open-source-logs · GitHub
I’m putting together my GSoC 2026 proposal for the “Add support for
generate_series in postgres” project . Before I finalize my proposal, I wanted to get early
feedback on my implementation approach.

The Bug

When .annotate(val=GenerateSeries(1, 5)).filter(val__gt=3) is called,
build_filter() calls solve_lookup_type()refs_expression(),
which returns the raw GenerateSeries expression as
reffed_expression. The early return path in build_filter() then
compiles generate_series(1, 5) directly into the WHERE clause.
PostgreSQL raises ProgrammingError because SRFs are not allowed
in WHERE.

The root cause: add_annotation() doesn’t inspect the
set_returning flag. All annotations are treated as SELECT-clause
values.

Proposed Architecture

The fix point is Query.add_annotation(). When set_returning=True
is detected:

  1. Route the expression to a new TableValuedFunction AST node in
    alias_map (alongside Join and BaseTable in
    datastructures.py).
  2. Replace the annotation with a Col reference to the FROM-clause
    alias.
  3. Subsequent refs_expression calls return the Col, so the WHERE
    clause gets "val"."value" > 3 instead of the raw function.
User writes:       .annotate(series=GenerateSeries(1, 10)).filter(series__gt=5)

                            │                                        │
                            ▼                                        │
              Query.add_annotation()                                 │
              detects set_returning=True                             │
                            │                                        │
              ┌─────────────┴──────────────┐                         │
              ▼                            ▼                         │
     alias_map["series"] =       annotations["series"] =             │
    TableValuedFunction(        Col("series", IntegerField)          │
       generate_series(1,10),                                        │
       alias="series"            ◄───────────────────────────────────┘
    )                           resolve_ref("series") returns Col
              │                 compiler emits: "series"."value" > 5
              ▼
    get_from_clause() emits:
    , generate_series(1, 10) AS "series"("value")

This requires no new QuerySet methods everything goes through
.annotate(). get_from_clause() already iterates over alias_map
and calls compile() on each entry, so the TableValuedFunction
node integrates with zero changes to the compiler’s as_sql().

I’m scoping this to single-column SRFs (generate_series, unnest)
first, leaving multi-column functions like json_each for a
follow-up phase per Lily’s comments on new-features Issue #25
about alias management for composite return types.

The core routing lives in django.db.models.sql, not
contrib.postgres, so the infrastructure is reusable for other
backends’ SRFs in the future.

My question:

Query.join() handles deduplication and join-type promotion via
JoinPromoter, but expects a Join object with join_field and
join_cols attributes that don’t exist for a virtual table source.
Should I adapt join() to accept TableValuedFunction nodes, or is
direct alias_map insertion with manual refcount management the
cleaner path?

Looking forward to any feedback and pointers.

Thanks.