Skip to content

Support for LIKE operator #2

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

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion customquery/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ class Parser:
def __init__(self, model, date_format='%Y-%m-%d'):
self.model = model
self.date_format = date_format
self.is_startswith = False
self.is_endswith = False

def parse(self, query):
"""Parse SQL-like condition statements and return Django Q objects"""
Expand Down Expand Up @@ -92,9 +94,10 @@ def _operate(self, a, operator, b):

def _compare(self, subject, operator, predicate):
# Raise exception if field does not exist
value = self._get_value(subject.value, predicate)
key, cond = self._make_key(operator, subject.value)
kwargs = {}
kwargs[key] = self._get_value(subject.value, predicate)
kwargs[key] = value

result = Q(**kwargs)
if not cond:
Expand All @@ -120,11 +123,22 @@ def _get_value(self, key, predicate):
elif v.startswith('"') and v.endswith('"'):
v = v[1:-1]

self.is_startswith = False
self.is_endswith = False
if v.startswith('%'):
self.is_endswith = True
v = v[1:]
if v.endswith('%'):
self.is_startswith = True
v = v[:-1]

field = self._get_field(key)
if isinstance(field, fields.DateField):
return datetime.strptime(str(v), self.date_format).date()
if predicate.ttype is t.Token.Literal.Number.Integer:
return int(v)
if predicate.ttype is t.Token.Literal.Number.Float:
return float(v)
if isinstance(predicate, sql.Identifier):
return predicate.get_name()
if predicate.ttype is t.Token.Literal.String.Single:
Expand All @@ -149,6 +163,22 @@ def _make_key(self, op, key):
return [key, False]
if op.value == 'NOT':
return [key, False]
if op.match(comp, 'LIKE'):
if self.is_endswith and self.is_startswith:
return [key + '__contains', True]
if self.is_startswith:
return [key + '__startswith', True]
if self.is_endswith:
return [key + '__endswith', True]
return [key + '__contains', True]
if op.match(comp, 'NOT LIKE'):
if self.is_endswith and self.is_startswith:
return [key + '__contains', False]
if self.is_startswith:
return [key + '__startswith', False]
if self.is_endswith:
return [key + '__endswith', False]
return [key + '__contains', False]
raise exceptions.UnknownOperator(op.normalized)

def _get_field(self, key):
Expand Down
4 changes: 4 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ django-custom-query was created by Luis Fagundes and was sponsored by `Spatial D
Changelog
=========

- 0.4.1

- Support for LIKE operator

- 0.4.0

- Support for IS NULL and IS NOT NULL queries
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import os

setup(name = 'django-custom-query',
version = '0.4.0',
version = '0.4.1',
description = 'Custom user query parser for Django ORM',
long_description = open(os.path.join(os.path.dirname(__file__), "README")).read(),
author = "Luis Fagundes",
Expand Down
15 changes: 15 additions & 0 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ class SingleParserTest(BaseTest):
def test_number(self):
self.assertEquals(self.parse("numfield=1"), Q(numfield=1))

def test_number_float(self):
self.assertEquals(self.parse("numfield=1.1"), Q(numfield=1.1))

def test_whitespaces_are_ignored(self):
self.assertEquals(self.parse("numfield = 1"), Q(numfield=1))
self.assertEquals(self.parse("numfield = 1"), Q(numfield=1))
Expand Down Expand Up @@ -71,6 +74,18 @@ def test_related_field(self):
def test_related_field_can_be_acessed_with_doc(self):
self.assertEquals(self.parse('related.name="foo bar"'), Q(related__name="foo bar"))

def test_like(self):
self.assertEquals(self.parse('charfield LIKE "foo"'), Q(charfield__contains="foo"))
self.assertEquals(self.parse("charfield LIKE 'foo'"), Q(charfield__contains="foo"))
self.assertEquals(self.parse("charfield LIKE '%foo'"), Q(charfield__endswith="foo"))
self.assertEquals(self.parse("charfield LIKE 'foo%'"), Q(charfield__startswith="foo"))
self.assertEquals(self.parse("charfield LIKE '%foo%'"), Q(charfield__contains="foo"))

self.assertEquals(self.parse('charfield NOT LIKE "foo"'), ~Q(charfield__contains="foo"))
self.assertEquals(self.parse("charfield NOT LIKE 'foo'"), ~Q(charfield__contains="foo"))
self.assertEquals(self.parse("charfield NOT LIKE '%foo'"), ~Q(charfield__endswith="foo"))
self.assertEquals(self.parse("charfield NOT LIKE 'foo%'"), ~Q(charfield__startswith="foo"))
self.assertEquals(self.parse("charfield NOT LIKE '%foo%'"), ~Q(charfield__contains="foo"))

class AnnotationTest(BaseTest):
def setUp(self):
Expand Down