Skip to content

feat: add union query support#2146

Merged
abondar merged 26 commits intotortoise:developfrom
seladb:add-union
Apr 9, 2026
Merged

feat: add union query support#2146
abondar merged 26 commits intotortoise:developfrom
seladb:add-union

Conversation

@seladb
Copy link
Copy Markdown
Contributor

@seladb seladb commented Mar 17, 2026

Add union query support in an API similar to Django's API

Addresses these issues:
#1507
#2056 (partially)

Description

Add union query support:

qs1 = Tournament.filter(name="T1").only("id")
qs2 = Reporter.filter(name="R1").only("id")

result = await qs1.union(qs2)

As opposed to Django, this API supports result of multiple models (in Django all results will be instances of the first model).
However, similar to Django, the requested field names and types should be the same across all queried models. This is because of how DBs support union queries.

Motivation and Context

It addresses the following issues:
#1507
#2056 (partially)

Also - it brings more feature parity with Django.

How Has This Been Tested?

Added test coverage for all written code. It was tested with MySQL, Postgres and SQLite.

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 Mar 17, 2026

Merging this PR will not alter performance

✅ 24 untouched benchmarks


Comparing seladb:add-union (5f4ad80) with develop (4874995)

Open in CodSpeed

assert result == expected


@requireCapability(dialect=NotEQ("mssql"))
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

MSSql limit syntax is different but it's not currently supported in _SetOperation which has its own implementation of _limit_sql() instead of using the dialect SQL. I guess we can leave it as a TODO for later

https://github.com/tortoise/pypika-tortoise/blob/378f6145f7529d88fa58510851d80454b742406e/pypika_tortoise/queries.py#L672

@seladb seladb marked this pull request as ready for review March 18, 2026 09:10
@seladb seladb requested a review from abondar March 25, 2026 07:37
@seladb
Copy link
Copy Markdown
Contributor Author

seladb commented Apr 3, 2026

@abondar I addressed your comments, can you please re-review this PR?

@seladb seladb changed the title Add union query support feat: add union query support Apr 4, 2026
@seladb
Copy link
Copy Markdown
Contributor Author

seladb commented Apr 8, 2026

@abondar I addressed your comments, can you please re-review this PR?

@abondar a gentle reminder to re-review this PR - I addressed all of your comments

model._meta.app == row[app_field]
and model._meta._model.__name__ == row[model_field]
):
instance_list.append(model._init_from_db(**row))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it is not good idea to pass row like that, with extra values of app field and model field. I think we should strip them. I understand that it works now like that, but it seems fragile and can interfere with actual fields declared on model. Maybe even consider making these special field names more unique, to lower chance of overlapping with actual fields (maybt "_*" prefix?)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure what you mean. We add 2 annotation fields: tortoise_app and tortoise_model here:
https://github.com/seladb/tortoise-orm/blob/074a61e59e88bbe221aa835f7ba6aa8390e7a605/tortoise/queryset.py#L2316-L2319

I can change the field names to __tortoise_app__ and __tortoise_model__ if you think it's better, but there's always a (very small) chance it will interfere with existing fields.

I think we should strip them

What do you mean by that?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You are passing these field values inside model init, I think we shouldn't do it, we should pop them from row dict

Copy link
Copy Markdown
Contributor Author

@seladb seladb Apr 9, 2026

Choose a reason for hiding this comment

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

Oh ok, it makes sense. Addressed in 9d541c2

if getattr(select, "alias") not in [cls.TORTOISE_APP_FIELD, cls.TORTOISE_MODEL_FIELD]
]

def _make_query(self) -> None:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I feel we still should make it more idempotent, maybe just add self._union_query = None at start?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added in 7ddbc5e

return list(dict(result[0]).values())[0]


class UnionQuery(AwaitableQuery[MODEL]):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I am skeptical about inheretence here - we inherit AwaitableQuery, but don't use many of it's fields, also redefining __slots__ and not setting those base fields in _clone

Is there reason why we want that inheretence at all? only for choose_db methods? Maybe we should extract choose_db methods to some mixing used by both?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Do you want me to implement it in this PR or move it to a separate PR?
I'm a bit concerned that this PR already contains quite a lot of changes, maybe we should avoid making even more changes?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Well, it's still change that concerns new class, it would be strange to ship it with bad inheritance model and start refactoring it right after

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I made this change in 5f4ad80, please let me know what you think

@seladb seladb requested a review from abondar April 9, 2026 07:56
@seladb
Copy link
Copy Markdown
Contributor Author

seladb commented Apr 9, 2026

@abondar I addressed your PR comments, please let me know what you think

@abondar abondar merged commit 56335a1 into tortoise:develop Apr 9, 2026
25 checks passed
@seladb
Copy link
Copy Markdown
Contributor Author

seladb commented Apr 10, 2026

Thank you @abondar for the thorough review and for merging this PR, much appreciated! 🙏
I'll update my other PRs and would love if you can review them also

@seladb seladb deleted the add-union branch April 10, 2026 02:20
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