Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add __like and __ilike filters (#421) #954

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from

Conversation

simoncampion
Copy link

Adds support for __like and __ilike filters (#421)

Description

Adds support for filtering by arbitrary LIKE patterns. For example:

await User.filter(name__like='J_hn%')

I made three potentially contentious implementation decisions:

  1. The caller needs to escape % and _ with backslashes in the pattern string if they desire to match those characters literally. If they are not escaped, they are treated as SQL wildcards. The caller does not need to escape \ to match it literally. As far as I can see, this is consistent with how __contains works, for example. It also escapes \ on behalf of the caller.
  2. I implemented __ilike using UPPER + LIKE. Therefore, it works with flavors of SQL that do not support the ILIKE operator, such as MySQL.
  3. The LIKE operator, and therefore __like, at least in the current implementation, is case-sensitive or case-insensitive depending on the flavor of SQL that is used. If desired, this might be changed, so that __like is guaranteed to be case-sensitive independently of the underlying database. I did not try to enforce case-sensitivity because LIKE queries on the database and __like filters in Tortoise would yield different results, which might be confusing.

Motivation and Context

Fixes #421
(The issue is assigned to someone else but has not been worked on for over a year. I hope it's fine that I opened a PR addressing it.)

How Has This Been Tested?

I added tests in test_filters.py. Someone with a good understanding of SQL injections should take a look at the tests and let me know if there are further tests I should add.

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.

The two tests tests/test_default.py::TestDefault::test_default and tests/fields/test_time.py::TestDatetimeFields::test_update fail when running make test_postgres locally on my machine. However, that is the case on the develop branch as well and seems unrelated to the changes I made.

This is my first contribution to Tortoise ORM. Please let me know if there are more elegant ways to implement LIKE filters using existing functionality in the code base that I might not be familiar with.

@long2ice
Copy link
Member

__startswith, __endswith is not enough?

@simoncampion
Copy link
Author

__like allows for more powerful filters than __startswith and __endswith (see the linked issue for an example). The question is whether users will value these more powerful filters sufficiently to justify adding support for them. My sense is that they will, but there's room for reasonable disagreement about this.

(FWIW, @abondar and @grigi seem to have thought the feature was worth adding.)

@MLBZ521
Copy link

MLBZ521 commented Feb 11, 2023

I'd vote for this feature -- I could utilize it in my project.

@MLBZ521
Copy link

MLBZ521 commented Feb 11, 2023

I figured out how to use and combined Q expressions to get a somewhat similar functionality for now.

For anyone else that may stumble across this, here's what I did:

core.send.py

from tortoise.expressions import Q
import core
[...]

async def policy_list(filter_values: str, username: str):

	user_object = await core.user.get({"username": username})

	q_expression = Q()

	for filter_value in filter_values.split(" "):
		q_expression &= Q(name__icontains=filter_value)

	if not user_object.full_admin:
		sites = user_object.site_access.split(", ")
		q_expression &= Q(site__in=sites)

	policies_object = await core.policy.get(q_expression)

core.policy.py

from tortoise.expressions import Q
import core
[...]

async def get(policy_filter: dict | Q | None = None):

	if not policy_filter:
		return await models.Policies.all()
	elif isinstance(policy_filter, dict):
		results = await models.Policy_Out.from_queryset(models.Policies.filter(**policy_filter))
	elif isinstance(policy_filter, Q):
		results = await models.Policy_Out.from_queryset(models.Policies.filter(policy_filter))

	return results[0] if len(results) == 1 else results

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.

Feature request: filter like and ilike
3 participants