Skip to content

feat: add DISTINCT ON support for PostgreSQL.#2211

Open
VladislavYar wants to merge 2 commits into
tortoise:developfrom
VladislavYar:distinct-by-specific-fields
Open

feat: add DISTINCT ON support for PostgreSQL.#2211
VladislavYar wants to merge 2 commits into
tortoise:developfrom
VladislavYar:distinct-by-specific-fields

Conversation

@VladislavYar
Copy link
Copy Markdown

Description

Added DISTINCT ON (fields) support for PostgreSQL to QuerySet.distinct(*fields).

Previously, .distinct() only supported plain DISTINCT (no arguments). Now, passing field
names generates DISTINCT ON (fields) on PostgreSQL, keeping one row per unique combination
of the specified fields. The feature is fully propagated to .values(), .values_list(),
and .only().

Also added:

  • skipCapability decorator to tortoise.contrib.test — the inverse of requireCapability,
    skips a test when the specified capabilities match.
  • _apply_db() helper in _ChooseDBMixin that consistently sets the DB connection and
    switches the query builder to PostgreSQLQueryBuilder across all query classes.

Motivation and Context

PostgreSQL's DISTINCT ON is a powerful feature that standard DISTINCT cannot replace —
it allows selecting one representative row per group without a GROUP BY, while retaining
full model objects. This is commonly needed for "latest per group" or "first per category"
queries.

How Has This Been Tested?

Added tests/test_distinct.py covering:

  • Basic DISTINCT ON with and without ORDER BY
  • Multiple DISTINCT ON fields
  • Combined with .values_list() — single field, multiple fields, field outside DISTINCT ON
  • Combined with .values() — same variations
  • Combined with .only()
  • ORDER BY respects the selected row within each group
  • OperationalError when ORDER BY doesn't start with DISTINCT ON fields
  • OperationalError when used on a non-PostgreSQL database

Checklist:

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have added the changelog accordingly.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Jun 6, 2026

Merging this PR will not alter performance

✅ 24 untouched benchmarks


Comparing VladislavYar:distinct-by-specific-fields (fa341d0) with develop (403a4dc)

Open in CodSpeed

Comment thread tortoise/contrib/test/__init__.py Outdated
return decorator


def skipCapability(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So many lines of this function. I would prefer:

def skip_dialect(*names: str, connection_name: str = 'models') -> Callable[[_FT], _FT]:
    return requireCapability(connection_name, dialect=NotIn(*names))

Comment thread tests/test_distinct.py Outdated
await Tournament.all().distinct("name").order_by("desc")


@test.skipCapability(dialect="postgres")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about @test.requireCapability(dialect=NotIn("postgres"))

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. I removed the skipCapability function and changed it to requireCapability(dialect=NotIn("postgres")).

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.

2 participants