commit 48c77424a4142b64b9f45828a55ce612e13949db Author: José Sánchez Moreno Date: Mon Nov 24 08:20:58 2014 +0100 Initial version diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..18520cd --- /dev/null +++ b/__init__.py @@ -0,0 +1,244 @@ +class classproperty(object): + + def __init__(self, getter): + self.getter = getter + + def __get__(self, instance, owner): + return self.getter(owner) + + +class QMixin(object): + AND = 'AND' + OR = 'OR' + NOT = 'NOT' + + def _combine(self, other, conn): + return Operator(conn, self, other) + + def __or__(self, other): + return self._combine(other, self.OR) + + def __and__(self, other): + return self._combine(other, self.AND) + + def __invert__(self,): + return Operator(self.NOT, self) + + +class Operator(QMixin): + + def __init__(self, op=None, left=None, right=None): + self.op = op + self.left = left + self.right = right + + def __repr__(self,): + if self.left and self.right: + return "(%s %s %s)" % (self.left, self.op, self.right) + + if self.right: + return "%s" % (self.right) + + if self.left and self.op == "NOT": + return "%s %s" % (self.op, self.left) + + return "%s" % self.left + + __str__ = __repr__ + + def __bool__(self): + return bool(self.right or self.left) + + __nonzero__ = __bool__ + + +class Q(QMixin): + lookup_types = [ + 'iexact', 'contains', 'icontains', + 'startswith', 'istartswith', 'endswith', 'iendswith', + 'month', 'day', 'week_day', 'hour', 'minute', 'second', + 'isnull', 'search', 'regex', 'iregex'] + + op_map = { + 'lte': '<=', + 'gte': '>=', + 'lt': '<', + 'gt': '>', + } + + def __init__(self, *args, **kwargs): + self.conditions = kwargs + + def __repr__(self,): + return self._compile() + + def __bool__(self): + return bool(self.conditions) + + __nonzero__ = __bool__ + + def _process(self, compose_column, value): + try: + column, lookup = compose_column.split("__") + except: + lookup = None + column = compose_column + + if lookup in self.lookup_types: + if lookup == "icontains": + return "{0} like '%{1}%'".format(column, value) + + if lookup == "iendswith": + return "{0} like '%{1}'".format(column, value) + + if lookup == "istartwith": + return "{0} like '{1}%'".format(column, value) + + if lookup == 'isnull': + op = "" + if not value: + op = "not" + return "{0} is {1} null".format(column, op) + + if lookup in self.op_map.keys(): + return "{0}{1}{2}".format(column, self.op_map[lookup], value) + + return "{0}{1}'{2}'".format(column, "=", value) + + def _compile(self,): + filters = [] + for k, v in self.conditions.items(): + filters.append(self._process(k, v)) + + if filters: + return "(%s)" % " AND ".join(filters) + + return "" + + +class SQLQuery(object): + + def __init__(self, table=None): + if table: + self._table = table + self._values = [] + self._order_by = [] + self._group_by = [] + self._joins = [] + self._filters = Q() + self._excludes = Q() + + def values(self, *args): + self._values = args + return self + + def filter(self, *args, **kwargs): + for arg in args: + if issubclass(arg.__class__, QMixin): + self._filters &= arg + + self._filters &= Q(**kwargs) + return self + + def exclude(self, *args, **kwargs): + for arg in args: + if issubclass(arg.__class__, QMixin): + self._excludes &= arg + + self._excludes &= Q(**kwargs) + return self + + def order_by(self, *args): + self._order_by = args + return self + + def group_by(self, *args): + self._group_by = args + return self + + def join(self, table, on="", how="inner join"): + if on: + on = "ON " + on + self._joins.append("{how} {table} {on}".format(how=how, table=table, on=on)) + return self + + +class SQLCompiler(object): + + def get_columns(self,): + if self._values: + return ", ".join(self._values) + return "*" + + def get_table(self,): + return self._table + + def get_where(self): + filters = self._filters & ~self._excludes + + if filters: + return "WHERE " + str(filters) + + return "" + + def get_order_by(self,): + conds = [] + for cond in self._order_by: + order = "" + column = cond + try: + if cond[0] == "-": + order = "desc" + column = cond[1:] + except: + pass + + conds.append("{0} {1}".format(column, order)) + + if conds: + return "ORDER BY " + ", ".join(conds) + return "" + + def get_group_by(self,): + if self._group_by: + return "GROUP BY " + ", ".join(self._group_by) + return "" + + def get_joins(self,): + if self._joins: + return " ".join(self._joins) + return "" + + def _compile(self): + sql = """ + SELECT {columns} + FROM {table} + {joins} + {where} + {group_by} + {order_by} + """.format( + columns=self.get_columns(), + table=self.get_table(), + joins=self.get_joins(), + where=self.get_where(), + group_by=self.get_group_by(), + order_by=self.get_order_by() + ) + return sql + + def __repr__(self): + return self._compile() + + __str__ = __repr__ + + +class Queryset(SQLCompiler, SQLQuery): + pass + + +class SQLModel(object): + + @classproperty + def objects(cls): + return Queryset(cls.table) diff --git a/readme.rst b/readme.rst new file mode 100644 index 0000000..633b280 --- /dev/null +++ b/readme.rst @@ -0,0 +1,40 @@ +SQL Query Builder based on django ORM +===================================== + +What's that? +----------- + +Is a library that you can use to build sql queries if your are accustomed to use Django ORM + + +How to use +---------- + +There are 3 main objects Q, QuerySet and SQLModel. + +Using it +--------------- +.. code-block:: python + + from sqlbuilder import SQLModel, Queryset, Q + + class Client(SQLModel): + table = "clients" + + + Client.objects.filter(name="Jhon").exclude(lastname="Doe").group_by("family") + + +.. code-block:: python + + sql = Queryset("clients").filter(name="Jhon").exclude(lastname="Doe").group_by("family") + + + sql = Client.objects.filter(Q(name="John") & ~Q(lastname="Doe")) + + sql.group_by("family") + + +str(sql) will result an string with the sql generated + + diff --git a/tests.py b/tests.py new file mode 100644 index 0000000..10c842d --- /dev/null +++ b/tests.py @@ -0,0 +1,13 @@ +import unittest +from . import Q + + +class TestSqlBuilder(unittest.TestCase): + + def test_q(self): + self.assertEqual(str(Q(a=1)), "(a='1')") + self.assertEqual(str(Q(a=1) & ~Q(b=2)), "((a='1') AND NOT (b='2'))") + + +if __name__ == '__main__': + unittest.main()