2017-04-23 14:08:14 +03:00
|
|
|
import six
|
|
|
|
import pytz
|
|
|
|
from copy import copy
|
|
|
|
|
|
|
|
|
|
|
|
# TODO
|
|
|
|
# - and/or between Q objects
|
|
|
|
# - check that field names are valid
|
2017-04-26 15:46:34 +03:00
|
|
|
# - qs slicing
|
|
|
|
# - operators for arrays: length, has, empty
|
2017-04-23 14:08:14 +03:00
|
|
|
|
|
|
|
class Operator(object):
|
2017-04-26 15:46:34 +03:00
|
|
|
"""
|
|
|
|
Base class for filtering operators.
|
|
|
|
"""
|
2017-04-23 14:08:14 +03:00
|
|
|
|
|
|
|
def to_sql(self, model_cls, field_name, value):
|
2017-04-26 15:46:34 +03:00
|
|
|
"""
|
|
|
|
Subclasses should implement this method. It returns an SQL string
|
|
|
|
that applies this operator on the given field and value.
|
|
|
|
"""
|
2017-04-23 14:08:14 +03:00
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
|
|
|
|
class SimpleOperator(Operator):
|
2017-04-26 15:46:34 +03:00
|
|
|
"""
|
|
|
|
A simple binary operator such as a=b, a<b, a>b etc.
|
|
|
|
"""
|
2017-04-23 14:08:14 +03:00
|
|
|
|
|
|
|
def __init__(self, sql_operator):
|
|
|
|
self._sql_operator = sql_operator
|
|
|
|
|
|
|
|
def to_sql(self, model_cls, field_name, value):
|
|
|
|
field = getattr(model_cls, field_name)
|
|
|
|
value = field.to_db_string(field.to_python(value, pytz.utc))
|
|
|
|
return ' '.join([field_name, self._sql_operator, value])
|
|
|
|
|
|
|
|
|
|
|
|
class InOperator(Operator):
|
2017-04-26 15:46:34 +03:00
|
|
|
"""
|
|
|
|
An operator that implements IN.
|
|
|
|
Accepts 3 different types of values:
|
|
|
|
- a list or tuple of simple values
|
|
|
|
- a string (used verbatim as the contents of the parenthesis)
|
|
|
|
- a queryset (subquery)
|
|
|
|
"""
|
2017-04-23 14:08:14 +03:00
|
|
|
|
|
|
|
def to_sql(self, model_cls, field_name, value):
|
|
|
|
field = getattr(model_cls, field_name)
|
|
|
|
if isinstance(value, QuerySet):
|
2017-05-05 15:21:55 +03:00
|
|
|
value = value.as_sql()
|
2017-04-23 14:08:14 +03:00
|
|
|
elif isinstance(value, six.string_types):
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
value = ', '.join([field.to_db_string(field.to_python(v, pytz.utc)) for v in value])
|
|
|
|
return '%s IN (%s)' % (field_name, value)
|
|
|
|
|
|
|
|
|
|
|
|
class LikeOperator(Operator):
|
2017-04-26 15:46:34 +03:00
|
|
|
"""
|
|
|
|
A LIKE operator that matches the field to a given pattern. Can be
|
|
|
|
case sensitive or insensitive.
|
|
|
|
"""
|
2017-04-23 14:08:14 +03:00
|
|
|
|
|
|
|
def __init__(self, pattern, case_sensitive=True):
|
|
|
|
self._pattern = pattern
|
|
|
|
self._case_sensitive = case_sensitive
|
|
|
|
|
|
|
|
def to_sql(self, model_cls, field_name, value):
|
|
|
|
field = getattr(model_cls, field_name)
|
|
|
|
value = field.to_db_string(field.to_python(value, pytz.utc), quote=False)
|
|
|
|
value = value.replace('\\', '\\\\').replace('%', '\\\\%').replace('_', '\\\\_')
|
|
|
|
pattern = self._pattern.format(value)
|
|
|
|
if self._case_sensitive:
|
|
|
|
return '%s LIKE \'%s\'' % (field_name, pattern)
|
|
|
|
else:
|
|
|
|
return 'lowerUTF8(%s) LIKE lowerUTF8(\'%s\')' % (field_name, pattern)
|
|
|
|
|
|
|
|
|
|
|
|
class IExactOperator(Operator):
|
2017-04-26 15:46:34 +03:00
|
|
|
"""
|
|
|
|
An operator for case insensitive string comparison.
|
|
|
|
"""
|
2017-04-23 14:08:14 +03:00
|
|
|
|
|
|
|
def to_sql(self, model_cls, field_name, value):
|
|
|
|
field = getattr(model_cls, field_name)
|
|
|
|
value = field.to_db_string(field.to_python(value, pytz.utc))
|
|
|
|
return 'lowerUTF8(%s) = lowerUTF8(%s)' % (field_name, value)
|
|
|
|
|
|
|
|
|
2017-06-15 11:19:56 +03:00
|
|
|
class NotOperator(Operator):
|
|
|
|
"""
|
|
|
|
A wrapper around another operator, which negates it.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, base_operator):
|
|
|
|
self._base_operator = base_operator
|
|
|
|
|
|
|
|
def to_sql(self, model_cls, field_name, value):
|
|
|
|
# Negate the base operator
|
|
|
|
return 'NOT (%s)' % self._base_operator.to_sql(model_cls, field_name, value)
|
|
|
|
|
|
|
|
|
2017-04-26 15:46:34 +03:00
|
|
|
# Define the set of builtin operators
|
|
|
|
|
2017-04-23 14:08:14 +03:00
|
|
|
_operators = {}
|
|
|
|
|
|
|
|
def register_operator(name, sql):
|
|
|
|
_operators[name] = sql
|
|
|
|
|
2017-04-26 15:46:34 +03:00
|
|
|
register_operator('eq', SimpleOperator('='))
|
2017-06-15 11:19:56 +03:00
|
|
|
register_operator('ne', SimpleOperator('!='))
|
2017-04-26 15:46:34 +03:00
|
|
|
register_operator('gt', SimpleOperator('>'))
|
|
|
|
register_operator('gte', SimpleOperator('>='))
|
|
|
|
register_operator('lt', SimpleOperator('<'))
|
|
|
|
register_operator('lte', SimpleOperator('<='))
|
|
|
|
register_operator('in', InOperator())
|
2017-06-15 11:19:56 +03:00
|
|
|
register_operator('not_in', NotOperator(InOperator()))
|
2017-04-26 15:46:34 +03:00
|
|
|
register_operator('contains', LikeOperator('%{}%'))
|
|
|
|
register_operator('startswith', LikeOperator('{}%'))
|
|
|
|
register_operator('endswith', LikeOperator('%{}'))
|
|
|
|
register_operator('icontains', LikeOperator('%{}%', False))
|
2017-04-23 14:08:14 +03:00
|
|
|
register_operator('istartswith', LikeOperator('{}%', False))
|
2017-04-26 15:46:34 +03:00
|
|
|
register_operator('iendswith', LikeOperator('%{}', False))
|
|
|
|
register_operator('iexact', IExactOperator())
|
2017-04-23 14:08:14 +03:00
|
|
|
|
|
|
|
|
|
|
|
class FOV(object):
|
2017-04-26 15:46:34 +03:00
|
|
|
"""
|
|
|
|
An object for storing Field + Operator + Value.
|
|
|
|
"""
|
2017-04-23 14:08:14 +03:00
|
|
|
|
|
|
|
def __init__(self, field_name, operator, value):
|
|
|
|
self._field_name = field_name
|
|
|
|
self._operator = _operators[operator]
|
|
|
|
self._value = value
|
|
|
|
|
|
|
|
def to_sql(self, model_cls):
|
|
|
|
return self._operator.to_sql(model_cls, self._field_name, self._value)
|
|
|
|
|
|
|
|
|
|
|
|
class Q(object):
|
|
|
|
|
|
|
|
def __init__(self, **kwargs):
|
|
|
|
self._fovs = [self._build_fov(k, v) for k, v in six.iteritems(kwargs)]
|
|
|
|
self._negate = False
|
|
|
|
|
|
|
|
def _build_fov(self, key, value):
|
|
|
|
if '__' in key:
|
|
|
|
field_name, operator = key.rsplit('__', 1)
|
|
|
|
else:
|
|
|
|
field_name, operator = key, 'eq'
|
|
|
|
return FOV(field_name, operator, value)
|
|
|
|
|
|
|
|
def to_sql(self, model_cls):
|
|
|
|
if not self._fovs:
|
|
|
|
return '1'
|
|
|
|
sql = ' AND '.join(fov.to_sql(model_cls) for fov in self._fovs)
|
|
|
|
if self._negate:
|
|
|
|
sql = 'NOT (%s)' % sql
|
|
|
|
return sql
|
|
|
|
|
|
|
|
def __invert__(self):
|
|
|
|
q = copy(self)
|
|
|
|
q._negate = True
|
|
|
|
return q
|
|
|
|
|
|
|
|
|
|
|
|
class QuerySet(object):
|
2017-05-05 15:31:08 +03:00
|
|
|
"""
|
|
|
|
A queryset is an object that represents a database query using a specific `Model`.
|
|
|
|
It is lazy, meaning that it does not hit the database until you iterate over its
|
|
|
|
matching rows (model instances).
|
|
|
|
"""
|
2017-04-23 14:08:14 +03:00
|
|
|
|
|
|
|
def __init__(self, model_cls, database):
|
2017-05-05 15:31:08 +03:00
|
|
|
"""
|
|
|
|
Initializer. It is possible to create a queryset like this, but the standard
|
|
|
|
way is to use `MyModel.objects_in(database)`.
|
|
|
|
"""
|
2017-04-23 14:08:14 +03:00
|
|
|
self._model_cls = model_cls
|
|
|
|
self._database = database
|
2017-06-15 11:19:56 +03:00
|
|
|
self._order_by = []
|
2017-04-23 14:08:14 +03:00
|
|
|
self._q = []
|
|
|
|
self._fields = []
|
|
|
|
|
|
|
|
def __iter__(self):
|
|
|
|
"""
|
|
|
|
Iterates over the model instances matching this queryset
|
|
|
|
"""
|
2017-05-05 15:21:55 +03:00
|
|
|
return self._database.select(self.as_sql(), self._model_cls)
|
2017-04-23 14:08:14 +03:00
|
|
|
|
2017-04-26 15:46:34 +03:00
|
|
|
def __bool__(self):
|
|
|
|
"""
|
2017-05-05 15:31:08 +03:00
|
|
|
Returns true if this queryset matches any rows.
|
2017-04-26 15:46:34 +03:00
|
|
|
"""
|
|
|
|
return bool(self.count())
|
|
|
|
|
|
|
|
def __nonzero__(self): # Python 2 compatibility
|
|
|
|
return type(self).__bool__(self)
|
|
|
|
|
2017-05-05 15:21:55 +03:00
|
|
|
def __unicode__(self):
|
|
|
|
return self.as_sql()
|
|
|
|
|
|
|
|
def as_sql(self):
|
2017-04-23 14:08:14 +03:00
|
|
|
"""
|
2017-05-05 15:31:08 +03:00
|
|
|
Returns the whole query as a SQL string.
|
2017-04-23 14:08:14 +03:00
|
|
|
"""
|
|
|
|
fields = '*'
|
|
|
|
if self._fields:
|
|
|
|
fields = ', '.join('`%s`' % field for field in self._fields)
|
2017-06-15 11:19:56 +03:00
|
|
|
ordering = '\nORDER BY ' + self.order_by_as_sql() if self._order_by else ''
|
|
|
|
params = (fields, self._database.db_name, self._model_cls.table_name(), self.conditions_as_sql(), ordering)
|
|
|
|
return u'SELECT %s\nFROM `%s`.`%s`\nWHERE %s%s' % params
|
2017-04-23 14:08:14 +03:00
|
|
|
|
|
|
|
def order_by_as_sql(self):
|
|
|
|
"""
|
2017-05-05 15:31:08 +03:00
|
|
|
Returns the contents of the query's `ORDER BY` clause as a string.
|
2017-04-23 14:08:14 +03:00
|
|
|
"""
|
2017-05-05 15:39:01 +03:00
|
|
|
return u', '.join([
|
2017-04-23 14:08:14 +03:00
|
|
|
'%s DESC' % field[1:] if field[0] == '-' else field
|
|
|
|
for field in self._order_by
|
|
|
|
])
|
|
|
|
|
|
|
|
def conditions_as_sql(self):
|
|
|
|
"""
|
2017-05-05 15:31:08 +03:00
|
|
|
Returns the contents of the query's `WHERE` clause as a string.
|
2017-04-23 14:08:14 +03:00
|
|
|
"""
|
|
|
|
if self._q:
|
2017-05-05 15:39:01 +03:00
|
|
|
return u' AND '.join([q.to_sql(self._model_cls) for q in self._q])
|
2017-04-23 14:08:14 +03:00
|
|
|
else:
|
2017-05-05 15:39:01 +03:00
|
|
|
return u'1'
|
2017-04-23 14:08:14 +03:00
|
|
|
|
|
|
|
def count(self):
|
|
|
|
"""
|
|
|
|
Returns the number of matching model instances.
|
|
|
|
"""
|
|
|
|
return self._database.count(self._model_cls, self.conditions_as_sql())
|
|
|
|
|
|
|
|
def order_by(self, *field_names):
|
|
|
|
"""
|
2017-05-05 15:31:08 +03:00
|
|
|
Returns a new `QuerySet` instance with the ordering changed.
|
2017-04-23 14:08:14 +03:00
|
|
|
"""
|
|
|
|
qs = copy(self)
|
|
|
|
qs._order_by = field_names
|
|
|
|
return qs
|
|
|
|
|
|
|
|
def only(self, *field_names):
|
|
|
|
"""
|
2017-05-05 15:31:08 +03:00
|
|
|
Returns a new `QuerySet` instance limited to the specified field names.
|
2017-04-23 14:08:14 +03:00
|
|
|
Useful when there are large fields that are not needed,
|
|
|
|
or for creating a subquery to use with an IN operator.
|
|
|
|
"""
|
|
|
|
qs = copy(self)
|
|
|
|
qs._fields = field_names
|
|
|
|
return qs
|
|
|
|
|
|
|
|
def filter(self, **kwargs):
|
|
|
|
"""
|
2017-05-05 15:31:08 +03:00
|
|
|
Returns a new `QuerySet` instance that includes only rows matching the conditions.
|
2017-04-23 14:08:14 +03:00
|
|
|
"""
|
|
|
|
qs = copy(self)
|
|
|
|
qs._q = list(self._q) + [Q(**kwargs)]
|
|
|
|
return qs
|
|
|
|
|
|
|
|
def exclude(self, **kwargs):
|
|
|
|
"""
|
2017-05-05 15:31:08 +03:00
|
|
|
Returns a new `QuerySet` instance that excludes all rows matching the conditions.
|
2017-04-23 14:08:14 +03:00
|
|
|
"""
|
|
|
|
qs = copy(self)
|
|
|
|
qs._q = list(self._q) + [~Q(**kwargs)]
|
|
|
|
return qs
|