├── .hgignore ├── .hgtags ├── CONTRIBUTORS.txt ├── LICENSE ├── MANIFEST.in ├── README.rst ├── django_sqlbuilder ├── __init__.py ├── compilers │ └── __init__.py ├── dialects │ ├── __init__.py │ └── sqlite.py ├── models.py ├── signals.py └── tests.py ├── docs ├── Makefile ├── _static │ └── pyparsing_examples │ │ ├── select_parser.py │ │ └── simpleSQL.py ├── conf.py ├── index.rst └── make.bat ├── runtests.py ├── setup.py └── sqlbuilder ├── __init__.py ├── django_sqlbuilder └── __init__.py ├── mini ├── __init__.py ├── parser.py └── tests.py └── smartsql ├── __init__.py ├── compiler.py ├── compilers └── __init__.py ├── constants.py ├── contrib ├── __init__.py ├── evaluate.py └── tests │ ├── __init__.py │ └── test_evaluate.py ├── datatypes.py ├── dialects ├── __init__.py ├── cassandra.py ├── mongodb.py ├── mysql.py ├── python.py └── sqlite.py ├── exceptions.py ├── expressions.py ├── factory.py ├── fields.py ├── operator_registry.py ├── operators.py ├── pycompat.py ├── queries.py ├── tables.py ├── tests ├── __init__.py ├── base.py ├── dialects │ ├── __init__.py │ ├── test_mongodb.py │ └── test_python.py ├── test_expressions.py ├── test_fields.py ├── test_legacy.py ├── test_queries.py ├── test_tables.py └── test_utils.py └── utils.py /.hgignore: -------------------------------------------------------------------------------- 1 | \.pyc$ 2 | \.swp$ 3 | \.orig$ 4 | \.diff$ 5 | ^\.\# 6 | ^\# 7 | ^etags$ 8 | ^.ropeproject/ 9 | ^\.idea$ 10 | ^\.project$ 11 | ^\.pydevproject$ 12 | ^\.settings/ 13 | .autosave$ 14 | ^\.search/ 15 | ^dist/ 16 | ^sqlbuilder.egg-info/ 17 | ^build/ 18 | ^docs/_build 19 | -------------------------------------------------------------------------------- /.hgtags: -------------------------------------------------------------------------------- 1 | b44770cd4dc0c5546b51bb460a5696de1f7399a8 0.7.1 2 | b44770cd4dc0c5546b51bb460a5696de1f7399a8 0.7.1 3 | 2c14779339d01cc36c70bd863d5b6d47019bb524 0.7.1 4 | 2c14779339d01cc36c70bd863d5b6d47019bb524 0.7.1 5 | 7262ce1185d254a4fe78bb4d3a095e090b386167 0.7.1 6 | 7262ce1185d254a4fe78bb4d3a095e090b386167 0.7.1 7 | 375c650aa15afe68d2e74518496ddf3d4944bbb8 0.7.1 8 | d9181981fd780ec10bde631423e0881dfc2a21bc 0.7.2 9 | 21eb9a38c6e61b80a16e981dee0d28cb6ec6c24a 0.7.4.1 10 | 29fb2cdf3a890c9a61fe45886b2fcca646410b16 0.7.4.2 11 | 0038249ec1c4370462c3cc3d5c3a9b1a34524b99 0.7.4.3 12 | 0939cfafc3597a81519ad75e90c32cfa5ec92323 0.7.5 13 | 35aaf888ad764620511b7bb630ddc01ec63f9796 0.7.6 14 | ad90725ab59a6ea2d9a8aa4e0400920d57c50b24 0.7.7 15 | e04e417d23cfe6b228bb895bb87680db4c8f1613 0.7.7.1 16 | 882ebe7809addfb9acf4c5a169dad546d8fd4144 0.7.7.2 17 | 882ebe7809addfb9acf4c5a169dad546d8fd4144 0.7.7.2 18 | 8a42cf6cafae7b03f3fa19fda27cb3b6c15d0f6e 0.7.7.2 19 | 8a42cf6cafae7b03f3fa19fda27cb3b6c15d0f6e 0.7.7.2 20 | e8db3ed32ff33240a8435af77ed0c458a529a58c 0.7.7.2 21 | 21008405934661c5f60f8850128ed32369930079 0.7.7.3 22 | 37d45130e662e45753474ad3494829c84011698e 0.7.7.5 23 | 0515536439434debae85f51a2505b45760da29e4 0.7.7.6 24 | 10151d4a8bdfea6a8ebe679c7ef97e74a6ad8b02 0.7.7.7 25 | ffb619bbfba2cddd547ac6edf5f123d5ec244b56 0.7.8.0 26 | 23b7396a08c9dd3e6879ae23034edebc9bc58c4b 0.7.9.0 27 | f20f97d6809b8d60ba9ac1031822733ac73cf0ea 0.7.9.1 28 | f2342e12213a7dd348e00d8371b42810d7880c36 0.7.9.2 29 | c918a270f2277192a3f07732ac2479c960d92576 0.7.9.3 30 | 98579f9685d0d6884eabc7df7d186f0f3d59af8a 0.7.9.4 31 | 774f365d4cefdaf0c96c1fae4e7990998692edb1 0.7.9.5 32 | 4683cea61e38ad304b59235063d00bead36094ee 0.7.9.6 33 | f9291e0feeae4ffd57cb3322e369637d8e5c1c44 0.7.9.7 34 | d1f7600a54248701c208652ad3eb18b18c870cfb 0.7.9.8 35 | cdf5747ed641418b568f0d13b8a7d14c6c2f6c9f 0.7.9.9 36 | cb20c75b5edbbc6f097852da475d810b53e0ab16 0.7.9.10 37 | e6da1e08e6518509a5dcd1528874f63b8797e1c6 0.7.9.11 38 | d7d70d30bf7173b3916d310fd9599fda8bed9b95 0.7.9.12 39 | 00e7ee3a6ae8094cb893d7fa13f128244b92d5ed 0.7.9.13 40 | 80d2ae1f025f459c5ea5b3a733e4b474f057089d 0.7.9.14 41 | e4f9ed6b0d58e1bc6a2326dfb56088d2635abf5e 0.7.9.15 42 | c88a725479be4ee56fdf1b1566ed15d1e8607466 0.7.9.16 43 | 68db29a4b880a53ce037ad6d498cca3b558fc21c 0.7.9.17 44 | dd0ff9c9c8d73cc6e3d8c11983301bcc4ce24732 0.7.9.18 45 | 3f5ad3b2c9273bd8503d845994c674b78d47614a 0.7.9.19 46 | dac85e8cbae51376996ec5e8e01704b5aa3b46e5 0.7.9.20 47 | 71c9eddbfcd36b5741124e42e4c876673cb4a55c 0.7.9.21 48 | 9ae75e41931e64bb4fc601579a244c93135e6d76 0.7.9.22 49 | 607fc469eb4ca8e2b2408ee39bd605975ee43aab 0.7.9.23 50 | 876fe2150e275237766beb9c85a1060412029950 0.7.9.24 51 | fff714979f6966c24c55c21bdc5a43c5f79cdd91 0.7.9.25 52 | 32ef59410459ef125d449522de75054f36f9572b 0.7.9.26 53 | 32ef59410459ef125d449522de75054f36f9572b 0.7.9.26 54 | 43277e563b0540b25487317a4fbfc5bc78da371c 0.7.9.26 55 | bb7e84ed48c15d33669566d6b284a8b0a28061d0 0.7.9.27 56 | d41cb03337d385569227ace5b12aaef8bec2ed3f 0.7.9.28 57 | bdf3853bc498303b318f26031502ea7c11e2531c 0.7.9.29 58 | dec1a660fab4256565309f326a9d363b76a5c551 0.7.9.30 59 | 4c957d98c3f79b5a9a70c21e91c97df9c507c952 0.7.9.31 60 | 11910f42c6d86c4246b1ff4ba5bdefa72aa879e2 0.7.9.32 61 | 7d01da0b8aae2704c92fc3e1b612b4acfd5a5b41 0.7.9.33 62 | 7d01da0b8aae2704c92fc3e1b612b4acfd5a5b41 0.7.9.33 63 | 025fba2e95ee427335bb95583f0164c9dc5bccfb 0.7.9.33 64 | 7a2871edc8529c89452124d717bf2320280be8fe 0.7.9.34 65 | 7ffeaf0cd655237d7467ccd9be0beb427fe185e3 0.7.9.35 66 | 632a575fea92b501d4b2c52109dcee74681d944f 0.7.9.36 67 | 044740f5cb2f1bc3f433ed568a0ad08ffcc24e3a 0.7.9.37 68 | f8479322fb7f40414b1cca6cd89bfcd0d637fab9 0.7.9.38 69 | b67117277caea1866f33a560f82c76109b75b667 0.7.9.39 70 | be7d549b23237fa06aae3d49f2feadfaee934700 0.7.9.40 71 | b7cc327061d8af31b73500cc1e6465fcc62d719e 0.7.9.41 72 | 2bb486c08645a8cbccefd5b74e61f9864bd8ede2 0.7.9.42 73 | d2cdeea4f21888913eae7943ad9223d41e197c0d 0.7.9.43 74 | 6a529cc25e786b9204446ad0fda922878b093722 0.7.9.44 75 | 24f29a6c1cc82c2931200782a31401bc9b9e5fe1 0.7.9.45 76 | 5dac4e9b75b0621904643686655e8cd170c13c52 0.7.9.46 77 | 003d8946c47669c1998bab83dd64329ad584c013 0.7.9.47 78 | b5fff2fd2434e17009e939d7c5656bc89f3a1ebe 0.7.9.48 79 | ad7252369718bf1d2b66de9cadeebfd804634499 0.7.9.49 80 | 6e7c22941bcfc74f848fa048ec37d0d5f1a521f2 0.7.9.50 81 | 6627fa2951e9476bbe6e9d4dd73b2e46a6a4e546 0.7.9.51 82 | 1a213c8ff699e264675b3bf47bbae56a0fc311e7 0.7.9.52 83 | cfdda2a27de3f313160c97cdac63d0e5e1d61dba 0.7.9.53 84 | d0ade6c7e9d0bf9c92d04689e53922f82b18a49a 0.7.10.00 85 | 583a38b00f1e064cb2f45d22b34cfdcc90f4a0e4 0.7.10.01 86 | 9b8d4151c73f03443ae758ce5d4afbc0fb837bcf 0.7.10.02 87 | fa365583d0984ffdc336ae7557e93b59df386385 0.7.10.03 88 | 32a3f0c3e1e4a949a6857d153be4d8682689408e 0.7.10.04 89 | cb653d756d0952eff6585f37ca653ebf3228238e 0.7.10.05 90 | a145f8266b9b58f582b3a3f1ba1db5f26fe0f998 0.7.10.06 91 | d39161e4b8f47bb5e51303ec10d7aa97f0d92733 0.7.10.7 92 | d39161e4b8f47bb5e51303ec10d7aa97f0d92733 0.7.10.7 93 | 6ee29e58e4e0fac73842a4588239c352fbda2a5a 0.7.10.7 94 | deacd2025251108ec9cb203e1e4e56a1c3f14276 0.7.10.8 95 | 7bfe8966dc45ad8863553887a5f0d01ee9a06a91 0.7.10.9 96 | 4cb8ccd5ea0e119c85e21bebccdfc5477cca348f 0.7.10.10 97 | a97763b2220516570662acaac7a70f22a9874d7a 0.7.10.11 98 | aa90e47c19ec0ce120b056b3c772ec1dee4c1b67 0.7.10.12 99 | 219196e05ba38de02ceeb2c4808b3ef78fd1c825 0.7.10.13 100 | aa83e568cbee63a3bc00de74d589e86f6c012bfd 0.7.10.14 101 | cd7939ddd3a8e7611c94ce8fb68cc6661f4e608c 0.7.10.15 102 | bae367c2c7480100189f2614e888141ca610cf5d 0.7.10.16 103 | bae367c2c7480100189f2614e888141ca610cf5d 0.7.10.16 104 | 0c522c597f9852d2b39a2746df17b75cd767ca5f 0.7.10.16 105 | 29c36253d317a175f3d72de5b4962196a70b5f38 0.7.10.17 106 | b68d4e6cf7c8eb98efe5078a0eaf6e5c8e91a862 0.7.10.18 107 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | Ivan Zakrevsky 2 | scutwukai 3 | Giang Manh 4 | Oleg Musaev 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Ivan Zakrevsky and contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of Ivan Zakrevsky nor the names of its contributors may be used 15 | to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | include CONTRIBUTORS.txt 4 | include MANIFEST.in 5 | recursive-include docs * 6 | recursive-exclude docs/_build * 7 | recursive-exclude * __pycache__ 8 | recursive-exclude * *.py[co] 9 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | SQLBuilder 3 | =========== 4 | 5 | SmartSQL - lightweight Python sql builder, follows the `KISS principle `_. Supports Python2 and Python3. 6 | 7 | You can use SmartSQL separatelly, or with Django, or with super-lightweight `Ascetic ORM `_, or with super-lightweight datamapper `Openorm `_ (`miror `__) etc. 8 | 9 | * Home Page: https://bitbucket.org/emacsway/sqlbuilder 10 | * Docs: https://sqlbuilder.readthedocs.io/ 11 | * Browse source code (canonical repo): https://bitbucket.org/emacsway/sqlbuilder/src 12 | * GitHub mirror: https://github.com/emacsway/sqlbuilder 13 | * Get source code (canonical repo): ``hg clone https://bitbucket.org/emacsway/sqlbuilder`` 14 | * Get source code (mirror): ``git clone https://github.com/emacsway/sqlbuilder.git`` 15 | * PyPI: https://pypi.python.org/pypi/sqlbuilder 16 | 17 | LICENSE: 18 | 19 | * License is BSD 20 | 21 | 22 | Quick start 23 | =========== 24 | 25 | :: 26 | 27 | >>> from sqlbuilder.smartsql import Q, T, compile 28 | >>> compile(Q().tables( 29 | ... (T.book & T.author).on(T.book.author_id == T.author.id) 30 | ... ).columns( 31 | ... T.book.name, T.author.first_name, T.author.last_name 32 | ... ).where( 33 | ... (T.author.first_name != 'Tom') & (T.author.last_name != 'Smith') 34 | ... )[20:30]) 35 | ('SELECT "book"."name", "author"."first_name", "author"."last_name" FROM "book" INNER JOIN "author" ON ("book"."author_id" = "author"."id") WHERE "author"."first_name" <> %s AND "author"."last_name" <> %s LIMIT %s OFFSET %s', ['Tom', 'Smith', 10, 20]) 36 | 37 | 38 | Django integration 39 | ================== 40 | 41 | Simple add "django_sqlbuilder" to your INSTALLED_APPS. 42 | 43 | :: 44 | 45 | >>> object_list = Book.s.q.tables( 46 | ... (Book.s & Author.s).on(Book.s.author == Author.s.pk) 47 | ... ).where( 48 | ... (Author.s.first_name != 'James') & (Author.s.last_name != 'Joyce') 49 | ... )[:10] 50 | 51 | 52 | More info 53 | ========= 54 | 55 | See docs on https://sqlbuilder.readthedocs.io/ 56 | 57 | .. 58 | 59 | P.S.: See also `article about SQLBuilder in English `__ and `in Russian `__. 60 | -------------------------------------------------------------------------------- /django_sqlbuilder/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacsway/sqlbuilder/72f32bbbfc1116550343c471dc43ef6284492a5a/django_sqlbuilder/__init__.py -------------------------------------------------------------------------------- /django_sqlbuilder/compilers/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from sqlbuilder.smartsql import warn 4 | 5 | warn('sqlbuilder.django_sqlbuilder.compilers', 'django_sqlbuilder.dialects') 6 | __path__.insert(0, os.path.join( 7 | os.path.dirname(os.path.dirname(os.path.realpath(sys.modules[__name__].__file__))), 8 | 'dialects' 9 | )) 10 | 11 | -------------------------------------------------------------------------------- /django_sqlbuilder/dialects/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacsway/sqlbuilder/72f32bbbfc1116550343c471dc43ef6284492a5a/django_sqlbuilder/dialects/__init__.py -------------------------------------------------------------------------------- /django_sqlbuilder/dialects/sqlite.py: -------------------------------------------------------------------------------- 1 | from sqlbuilder.smartsql.dialects.sqlite import compile as parent_compile 2 | 3 | compile = parent_compile.create_child() 4 | 5 | 6 | @compile.when(object) 7 | def compile_object(compile, expr, state): 8 | state.sql.append('%s') 9 | state.params.append(expr) 10 | -------------------------------------------------------------------------------- /django_sqlbuilder/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | import copy 3 | import collections 4 | from itertools import chain 5 | from django.conf import settings 6 | from django.db import connections 7 | from django.db.models import Model 8 | 9 | from sqlbuilder import smartsql 10 | from sqlbuilder.smartsql.dialects import mysql 11 | from django_sqlbuilder.dialects import sqlite 12 | from django_sqlbuilder.signals import field_conversion, field_mangling, column_mangling 13 | 14 | try: 15 | str = unicode # Python 2.* compatible 16 | string_types = (basestring,) 17 | integer_types = (int, long) 18 | except NameError: 19 | string_types = (str,) 20 | integer_types = (int,) 21 | 22 | SMARTSQL_ALIAS = getattr(settings, 'SQLBUILDER_SMARTSQL_ALIAS', 's') 23 | SMARTSQL_COMPILERS = { 24 | 'sqlite3': sqlite.compile, 25 | 'mysql': mysql.compile, 26 | 'postgresql': smartsql.compile, 27 | 'postgresql_psycopg2': smartsql.compile, 28 | 'postgis': smartsql.compile, 29 | } 30 | 31 | factory = copy.copy(smartsql.factory) 32 | 33 | 34 | class classproperty(object): 35 | """Class property decorator""" 36 | def __init__(self, getter): 37 | self.getter = getter 38 | 39 | def __get__(self, instance, owner): 40 | return self.getter(owner) 41 | 42 | 43 | class Result(smartsql.Result): 44 | 45 | _cache = None 46 | _using = 'default' 47 | _model = None 48 | 49 | def __init__(self, model): 50 | self._model = model 51 | self._using = self._model.objects.db 52 | self.set_compiler() 53 | 54 | def __len__(self): 55 | self.fill_cache() 56 | return len(self._cache) 57 | 58 | def __iter__(self): 59 | self.fill_cache() 60 | return iter(self._cache) 61 | 62 | def __getitem__(self, key): 63 | if self._cache: 64 | return self._cache[key] 65 | if isinstance(key, integer_types): 66 | self._query = super(Result, self).__getitem__(key) 67 | return list(self)[0] 68 | return super(Result, self).__getitem__(key) 69 | 70 | def execute(self): 71 | cursor = connections[self._using].cursor() 72 | cursor.execute(*self.compile(self._query)) 73 | return cursor 74 | 75 | insert = update = delete = execute 76 | 77 | def select(self): 78 | return self 79 | 80 | def count(self): 81 | if self._cache is not None: 82 | return len(self._cache) 83 | return self.execute().fetchone()[0] 84 | 85 | def clone(self): 86 | c = smartsql.Result.clone(self) 87 | c._cache = None 88 | return c 89 | 90 | def using(self, alias=None): 91 | if alias is None: 92 | return self._using 93 | self._using = alias 94 | self.set_compiler() 95 | return self._query 96 | 97 | def set_compiler(self): 98 | engine = connections.databases[self._using]['ENGINE'].rsplit('.')[-1] 99 | self.compile = SMARTSQL_COMPILERS[engine] 100 | 101 | def fill_cache(self): 102 | if self._cache is None: 103 | self._cache = list(self.iterator()) 104 | 105 | def iterator(self): 106 | return self._model.objects.raw(*self.compile(self._query)).using(self._using) 107 | 108 | 109 | @factory.register 110 | class Table(smartsql.Table): 111 | """Table class for Django model""" 112 | 113 | def __init__(self, model, q=None, *args, **kwargs): 114 | super(Table, self).__init__(model._meta.db_table, *args, **kwargs) 115 | self._model = model 116 | self._q = q 117 | 118 | def _get_q(self): 119 | if isinstance(self._q, collections.Callable): 120 | self._q = self._q(self) 121 | elif self._q is not None: 122 | return self._q.clone() 123 | else: 124 | return smartsql.factory.get(self).Query(self, result=Result(self._model)).fields(self.get_fields()) 125 | 126 | def _set_q(self, val): 127 | self._q = val 128 | 129 | qs = property(_get_q, _set_q) 130 | q = property(_get_q, _set_q) 131 | 132 | def get_fields(self, prefix=None): 133 | if prefix is None: 134 | prefix = self 135 | elif isinstance(prefix, string_types): 136 | prefix = smartsql.Table(prefix) 137 | return [prefix.get_field(f.name) for f in self._model._meta.local_fields if f.column] 138 | 139 | def get_field(self, name): 140 | opts = self._model._meta 141 | parts = name.split(smartsql.LOOKUP_SEP, 1) 142 | name = self.__mangle_field(parts[0]) 143 | # model attributes support 144 | if name == 'pk': 145 | name = opts.pk.column 146 | elif name in get_all_field_names(opts): 147 | name = opts.get_field(name).column 148 | parts[0] = self.__mangle_column(name) 149 | return super(Table, self).get_field(smartsql.LOOKUP_SEP.join(parts)) 150 | 151 | def __mangle_field(self, name): 152 | model = self._model 153 | results = field_mangling.send(sender=self, field=name, model=model) 154 | results = [i[1] for i in results if i[1]] 155 | if results: 156 | # response in format tuple(priority: int, mangled_field_name: str) 157 | results.sort(key=lambda x: x[0], reverse=True) # Sort by priority 158 | return results[0][1] 159 | 160 | # Backward compatibility. Deprecated: 161 | result = {'field': name, } 162 | field_conversion.send(sender=self, result=result, field=name, model=model) 163 | mangled_field_name = result['field'] 164 | if mangled_field_name != name: 165 | return mangled_field_name 166 | 167 | # django-multilingual-ext support 168 | if 'modeltranslation' in settings.INSTALLED_APPS: 169 | from modeltranslation.translator import translator, NotRegistered 170 | from modeltranslation.utils import get_language, build_localized_fieldname 171 | else: 172 | translator = None 173 | if translator: 174 | try: 175 | trans_opts = translator.get_options_for_model(model) 176 | if name in trans_opts.fields: 177 | return build_localized_fieldname(name, get_language()) 178 | except NotRegistered: 179 | pass 180 | 181 | if hasattr(model.objects, 'localize_fieldname'): 182 | return model.objects.localize_fieldname(name) 183 | 184 | return name 185 | 186 | def __mangle_column(self, column): 187 | results = column_mangling.send(sender=self, column=column, model=self._model) 188 | results = [i[1] for i in results if i[1]] 189 | if results: 190 | # response in format tuple(priority: int, mangled_column_name: str) 191 | results.sort(key=lambda x: x[0], reverse=True) # Sort by priority 192 | return results[0][1] 193 | return column 194 | 195 | 196 | @factory.register 197 | class TableAlias(smartsql.TableAlias, Table): 198 | @property 199 | def _model(self): 200 | return getattr(self._table, '_model', None) # self._table can be a subquery 201 | 202 | 203 | def get_all_field_names(opts): 204 | try: 205 | return list(set(chain.from_iterable( 206 | (field.name, field.attname) if hasattr(field, 'attname') else (field.name,) 207 | for field in opts.get_fields() 208 | # For complete backwards compatibility, you may want to exclude 209 | # GenericForeignKey from the results. 210 | if not (field.many_to_one and field.related_model is None) 211 | ))) 212 | except AttributeError: 213 | return opts.get_all_field_names() 214 | 215 | 216 | @classproperty 217 | def s(cls): 218 | a = '_{0}'.format(SMARTSQL_ALIAS) 219 | if a not in cls.__dict__: 220 | setattr(cls, a, factory.Table(cls)) 221 | return getattr(cls, a) 222 | 223 | setattr(Model, SMARTSQL_ALIAS, s) 224 | -------------------------------------------------------------------------------- /django_sqlbuilder/signals.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | import django.dispatch 3 | 4 | field_conversion = django.dispatch.Signal(providing_args=["result", "field", "model"]) # Deprecated 5 | field_mangling = django.dispatch.Signal(providing_args=["field", "model"]) 6 | column_mangling = django.dispatch.Signal(providing_args=["column", "model"]) 7 | -------------------------------------------------------------------------------- /django_sqlbuilder/tests.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | from django.conf import settings 3 | from django.db import models 4 | from django.test import TestCase, override_settings 5 | from sqlbuilder.smartsql import Table, TableAlias, Field, Query, compile 6 | 7 | 8 | class Author(models.Model): 9 | first_name = models.CharField(max_length=255, blank=True) 10 | last_name = models.CharField(max_length=255, blank=True) 11 | 12 | class Meta: 13 | db_table = 'sqlbuilder_author' 14 | 15 | 16 | class Book(models.Model): 17 | title = models.CharField(max_length=255, blank=True) 18 | author = models.ForeignKey(Author, blank=True, null=True) 19 | 20 | class Meta: 21 | db_table = 'sqlbuilder_book' 22 | 23 | 24 | class TestDjangoSqlbuilder(TestCase): 25 | 26 | def test_table(self): 27 | table = Book.s 28 | self.assertIsInstance(table, Table) 29 | self.assertIsInstance(table.pk, Field) 30 | self.assertIsInstance(table.title, Field) 31 | self.assertIsInstance(table.author, Field) 32 | self.assertEqual( 33 | compile(table.pk), 34 | ('"sqlbuilder_book"."id"', []) 35 | ) 36 | self.assertEqual( 37 | compile(table.title), 38 | ('"sqlbuilder_book"."title"', []) 39 | ) 40 | self.assertEqual( 41 | compile(table.author), 42 | ('"sqlbuilder_book"."author_id"', []) 43 | ) 44 | 45 | def test_tablealias(self): 46 | table = Book.s.as_('book_alias') 47 | self.assertIsInstance(table, TableAlias) 48 | self.assertIsInstance(table.pk, Field) 49 | self.assertIsInstance(table.title, Field) 50 | self.assertIsInstance(table.author, Field) 51 | self.assertEqual( 52 | compile(table.pk), 53 | ('"book_alias"."id"', []) 54 | ) 55 | self.assertEqual( 56 | compile(table.title), 57 | ('"book_alias"."title"', []) 58 | ) 59 | self.assertEqual( 60 | compile(table.author), 61 | ('"book_alias"."author_id"', []) 62 | ) 63 | 64 | @override_settings(DEBUG=True) 65 | def test_query(self): 66 | author, book = self._create_objects() 67 | self.assertIsInstance(Book.s.q, Query) 68 | q = Book.s.q.where(Book.s.pk == book.id) 69 | self.assertEqual( 70 | compile(q), 71 | ('SELECT "sqlbuilder_book"."id", "sqlbuilder_book"."title", "sqlbuilder_book"."author_id" FROM "sqlbuilder_book" WHERE "sqlbuilder_book"."id" = %s', [book.id]) 72 | ) 73 | book2 = q[0] 74 | self.assertEqual(book2.id, book.id) 75 | 76 | def _create_objects(self): 77 | author = Author.objects.create(first_name='John', last_name='Smith') 78 | book = Book.objects.create(title="Title 1", author_id=author.id) 79 | return (author, book) 80 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/sqlbuilder.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/sqlbuilder.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/sqlbuilder" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/sqlbuilder" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /docs/_static/pyparsing_examples/select_parser.py: -------------------------------------------------------------------------------- 1 | # Source: http://pyparsing.wikispaces.com/file/view/select_parser.py 2 | # select_parser.py 3 | # Copyright 2010, Paul McGuire 4 | # 5 | # a simple SELECT statement parser, taken from SQLite's SELECT statement 6 | # definition at http://www.sqlite.org/lang_select.html 7 | # 8 | from pyparsing import * 9 | 10 | LPAR,RPAR,COMMA = map(Suppress,"(),") 11 | select_stmt = Forward().setName("select statement") 12 | 13 | # keywords 14 | (UNION, ALL, AND, INTERSECT, EXCEPT, COLLATE, ASC, DESC, ON, USING, NATURAL, INNER, 15 | CROSS, LEFT, OUTER, JOIN, AS, INDEXED, NOT, SELECT, DISTINCT, FROM, WHERE, GROUP, BY, 16 | HAVING, ORDER, BY, LIMIT, OFFSET) = map(CaselessKeyword, """UNION, ALL, AND, INTERSECT, 17 | EXCEPT, COLLATE, ASC, DESC, ON, USING, NATURAL, INNER, CROSS, LEFT, OUTER, JOIN, AS, INDEXED, NOT, SELECT, 18 | DISTINCT, FROM, WHERE, GROUP, BY, HAVING, ORDER, BY, LIMIT, OFFSET""".replace(",","").split()) 19 | (CAST, ISNULL, NOTNULL, NULL, IS, BETWEEN, ELSE, END, CASE, WHEN, THEN, EXISTS, 20 | COLLATE, IN, LIKE, GLOB, REGEXP, MATCH, ESCAPE, CURRENT_TIME, CURRENT_DATE, 21 | CURRENT_TIMESTAMP) = map(CaselessKeyword, """CAST, ISNULL, NOTNULL, NULL, IS, BETWEEN, ELSE, 22 | END, CASE, WHEN, THEN, EXISTS, COLLATE, IN, LIKE, GLOB, REGEXP, MATCH, ESCAPE, 23 | CURRENT_TIME, CURRENT_DATE, CURRENT_TIMESTAMP""".replace(",","").split()) 24 | keyword = MatchFirst((UNION, ALL, INTERSECT, EXCEPT, COLLATE, ASC, DESC, ON, USING, NATURAL, INNER, 25 | CROSS, LEFT, OUTER, JOIN, AS, INDEXED, NOT, SELECT, DISTINCT, FROM, WHERE, GROUP, BY, 26 | HAVING, ORDER, BY, LIMIT, OFFSET, CAST, ISNULL, NOTNULL, NULL, IS, BETWEEN, ELSE, END, CASE, WHEN, THEN, EXISTS, 27 | COLLATE, IN, LIKE, GLOB, REGEXP, MATCH, ESCAPE, CURRENT_TIME, CURRENT_DATE, 28 | CURRENT_TIMESTAMP)) 29 | 30 | identifier = ~keyword + Word(alphas, alphanums+"_") 31 | collation_name = identifier.copy() 32 | column_name = identifier.copy() 33 | column_alias = identifier.copy() 34 | table_name = identifier.copy() 35 | table_alias = identifier.copy() 36 | index_name = identifier.copy() 37 | function_name = identifier.copy() 38 | parameter_name = identifier.copy() 39 | database_name = identifier.copy() 40 | 41 | # expression 42 | expr = Forward().setName("expression") 43 | 44 | integer = Regex(r"[+-]?\d+") 45 | numeric_literal = Regex(r"\d+(\.\d*)?([eE][+-]?\d+)?") 46 | string_literal = QuotedString("'") 47 | blob_literal = Combine(oneOf("x X") + "'" + Word(hexnums) + "'") 48 | literal_value = ( numeric_literal | string_literal | blob_literal | 49 | NULL | CURRENT_TIME | CURRENT_DATE | CURRENT_TIMESTAMP ) 50 | bind_parameter = ( 51 | Word("?",nums) | 52 | Combine(oneOf(": @ $") + parameter_name) 53 | ) 54 | type_name = oneOf("TEXT REAL INTEGER BLOB NULL") 55 | 56 | expr_term = ( 57 | CAST + LPAR + expr + AS + type_name + RPAR | 58 | EXISTS + LPAR + select_stmt + RPAR | 59 | function_name + LPAR + Optional(delimitedList(expr)) + RPAR | 60 | literal_value | 61 | bind_parameter | 62 | identifier 63 | ) 64 | 65 | UNARY,BINARY,TERNARY=1,2,3 66 | expr << operatorPrecedence(expr_term, 67 | [ 68 | (oneOf('- + ~') | NOT, UNARY, opAssoc.LEFT), 69 | ('||', BINARY, opAssoc.LEFT), 70 | (oneOf('* / %'), BINARY, opAssoc.LEFT), 71 | (oneOf('+ -'), BINARY, opAssoc.LEFT), 72 | (oneOf('<< >> & |'), BINARY, opAssoc.LEFT), 73 | (oneOf('< <= > >='), BINARY, opAssoc.LEFT), 74 | (oneOf('= == != <>') | IS | IN | LIKE | GLOB | MATCH | REGEXP, BINARY, opAssoc.LEFT), 75 | ('||', BINARY, opAssoc.LEFT), 76 | ((BETWEEN,AND), TERNARY, opAssoc.LEFT), 77 | ]) 78 | 79 | compound_operator = (UNION + Optional(ALL) | INTERSECT | EXCEPT) 80 | 81 | ordering_term = expr + Optional(COLLATE + collation_name) + Optional(ASC | DESC) 82 | 83 | join_constraint = Optional(ON + expr | USING + LPAR + Group(delimitedList(column_name)) + RPAR) 84 | 85 | join_op = COMMA | (Optional(NATURAL) + Optional(INNER | CROSS | LEFT + OUTER | LEFT | OUTER) + JOIN) 86 | 87 | join_source = Forward() 88 | single_source = ( (Group(database_name("database") + "." + table_name("table")) | table_name("table")) + 89 | Optional(Optional(AS) + table_alias("table_alias")) + 90 | Optional(INDEXED + BY + index_name("name") | NOT + INDEXED)("index") | 91 | (LPAR + select_stmt + RPAR + Optional(Optional(AS) + table_alias)) | 92 | (LPAR + join_source + RPAR) ) 93 | 94 | join_source << single_source + ZeroOrMore(join_op + single_source + join_constraint) 95 | 96 | result_column = "*" | table_name + "." + "*" | (expr + Optional(Optional(AS) + column_alias)) 97 | select_core = (SELECT + Optional(DISTINCT | ALL) + Group(delimitedList(result_column))("columns") + 98 | Optional(FROM + join_source) + 99 | Optional(WHERE + expr("where_expr")) + 100 | Optional(GROUP + BY + Group(delimitedList(ordering_term)("group_by_terms")) + 101 | Optional(HAVING + expr("having_expr")))) 102 | 103 | select_stmt << (select_core + ZeroOrMore(compound_operator + select_core) + 104 | Optional(ORDER + BY + Group(delimitedList(ordering_term))("order_by_terms")) + 105 | Optional(LIMIT + (integer + OFFSET + integer | integer + COMMA + integer))) 106 | 107 | tests = """\ 108 | select * from xyzzy where z > 100 109 | select * from xyzzy where z > 100 order by zz 110 | select * from xyzzy""".splitlines() 111 | for t in tests: 112 | print t 113 | try: 114 | print select_stmt.parseString(t).dump() 115 | except ParseException, pe: 116 | print pe.msg 117 | print 118 | -------------------------------------------------------------------------------- /docs/_static/pyparsing_examples/simpleSQL.py: -------------------------------------------------------------------------------- 1 | # Source: http://pyparsing.wikispaces.com/file/view/simpleSQL.py 2 | # simpleSQL.py 3 | # 4 | # simple demo of using the parsing library to do simple-minded SQL parsing 5 | # could be extended to include where clauses etc. 6 | # 7 | # Copyright (c) 2003, Paul McGuire 8 | # 9 | from pyparsing import Literal, CaselessLiteral, Word, Upcase, delimitedList, Optional, \ 10 | Combine, Group, alphas, nums, alphanums, ParseException, Forward, oneOf, quotedString, \ 11 | ZeroOrMore, restOfLine, Keyword 12 | 13 | def test( str ): 14 | print str,"->" 15 | try: 16 | tokens = simpleSQL.parseString( str ) 17 | print "tokens = ", tokens 18 | print "tokens.columns =", tokens.columns 19 | print "tokens.tables =", tokens.tables 20 | print "tokens.where =", tokens.where 21 | except ParseException, err: 22 | print " "*err.loc + "^\n" + err.msg 23 | print err 24 | print 25 | 26 | 27 | # define SQL tokens 28 | selectStmt = Forward() 29 | selectToken = Keyword("select", caseless=True) 30 | fromToken = Keyword("from", caseless=True) 31 | 32 | ident = Word( alphas, alphanums + "_$" ).setName("identifier") 33 | columnName = Upcase( delimitedList( ident, ".", combine=True ) ) 34 | columnNameList = Group( delimitedList( columnName ) ) 35 | tableName = Upcase( delimitedList( ident, ".", combine=True ) ) 36 | tableNameList = Group( delimitedList( tableName ) ) 37 | 38 | whereExpression = Forward() 39 | and_ = Keyword("and", caseless=True) 40 | or_ = Keyword("or", caseless=True) 41 | in_ = Keyword("in", caseless=True) 42 | 43 | E = CaselessLiteral("E") 44 | binop = oneOf("= != < > >= <= eq ne lt le gt ge", caseless=True) 45 | arithSign = Word("+-",exact=1) 46 | realNum = Combine( Optional(arithSign) + ( Word( nums ) + "." + Optional( Word(nums) ) | 47 | ( "." + Word(nums) ) ) + 48 | Optional( E + Optional(arithSign) + Word(nums) ) ) 49 | intNum = Combine( Optional(arithSign) + Word( nums ) + 50 | Optional( E + Optional("+") + Word(nums) ) ) 51 | 52 | columnRval = realNum | intNum | quotedString | columnName # need to add support for alg expressions 53 | whereCondition = Group( 54 | ( columnName + binop + columnRval ) | 55 | ( columnName + in_ + "(" + delimitedList( columnRval ) + ")" ) | 56 | ( columnName + in_ + "(" + selectStmt + ")" ) | 57 | ( "(" + whereExpression + ")" ) 58 | ) 59 | whereExpression << whereCondition + ZeroOrMore( ( and_ | or_ ) + whereExpression ) 60 | 61 | # define the grammar 62 | selectStmt << ( selectToken + 63 | ( '*' | columnNameList ).setResultsName( "columns" ) + 64 | fromToken + 65 | tableNameList.setResultsName( "tables" ) + 66 | Optional( Group( CaselessLiteral("where") + whereExpression ), "" ).setResultsName("where") ) 67 | 68 | simpleSQL = selectStmt 69 | 70 | # define Oracle comment format, and ignore them 71 | oracleSqlComment = "--" + restOfLine 72 | simpleSQL.ignore( oracleSqlComment ) 73 | 74 | 75 | test( "SELECT * from XYZZY, ABC" ) 76 | test( "select * from SYS.XYZZY" ) 77 | test( "Select A from Sys.dual" ) 78 | test( "Select A,B,C from Sys.dual" ) 79 | test( "Select A, B, C from Sys.dual" ) 80 | test( "Select A, B, C from Sys.dual, Table2 " ) 81 | test( "Xelect A, B, C from Sys.dual" ) 82 | test( "Select A, B, C frox Sys.dual" ) 83 | test( "Select" ) 84 | test( "Select &&& frox Sys.dual" ) 85 | test( "Select A from Sys.dual where a in ('RED','GREEN','BLUE')" ) 86 | test( "Select A from Sys.dual where a in ('RED','GREEN','BLUE') and b in (10,20,30)" ) 87 | test( "Select A,b from table1,table2 where table1.id eq table2.id -- test out comparison operators" ) 88 | 89 | """ 90 | Test output: 91 | >pythonw -u simpleSQL.py 92 | SELECT * from XYZZY, ABC -> 93 | tokens = ['select', '*', 'from', ['XYZZY', 'ABC']] 94 | tokens.columns = * 95 | tokens.tables = ['XYZZY', 'ABC'] 96 | 97 | select * from SYS.XYZZY -> 98 | tokens = ['select', '*', 'from', ['SYS.XYZZY']] 99 | tokens.columns = * 100 | tokens.tables = ['SYS.XYZZY'] 101 | 102 | Select A from Sys.dual -> 103 | tokens = ['select', ['A'], 'from', ['SYS.DUAL']] 104 | tokens.columns = ['A'] 105 | tokens.tables = ['SYS.DUAL'] 106 | 107 | Select A,B,C from Sys.dual -> 108 | tokens = ['select', ['A', 'B', 'C'], 'from', ['SYS.DUAL']] 109 | tokens.columns = ['A', 'B', 'C'] 110 | tokens.tables = ['SYS.DUAL'] 111 | 112 | Select A, B, C from Sys.dual -> 113 | tokens = ['select', ['A', 'B', 'C'], 'from', ['SYS.DUAL']] 114 | tokens.columns = ['A', 'B', 'C'] 115 | tokens.tables = ['SYS.DUAL'] 116 | 117 | Select A, B, C from Sys.dual, Table2 -> 118 | tokens = ['select', ['A', 'B', 'C'], 'from', ['SYS.DUAL', 'TABLE2']] 119 | tokens.columns = ['A', 'B', 'C'] 120 | tokens.tables = ['SYS.DUAL', 'TABLE2'] 121 | 122 | Xelect A, B, C from Sys.dual -> 123 | ^ 124 | Expected 'select' 125 | Expected 'select' (0), (1,1) 126 | 127 | Select A, B, C frox Sys.dual -> 128 | ^ 129 | Expected 'from' 130 | Expected 'from' (15), (1,16) 131 | 132 | Select -> 133 | ^ 134 | Expected '*' 135 | Expected '*' (6), (1,7) 136 | 137 | Select &&& frox Sys.dual -> 138 | ^ 139 | Expected '*' 140 | Expected '*' (7), (1,8) 141 | 142 | >Exit code: 0 143 | """ 144 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # sqlbuilder documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Sep 5 23:02:40 2015. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | import shlex 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | #sys.path.insert(0, os.path.abspath('.')) 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | #needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [ 33 | # 'sphinx.ext.viewcode', 34 | # 'sphinx.ext.linkcode', 35 | 'sphinx.ext.autodoc', 36 | ] 37 | 38 | def get_module_path(mod_name): 39 | __import__(mod_name) 40 | mod = sys.modules[mod_name] 41 | path = mod.__file__.split('sqlbuilder/', 1)[1].replace(".pyc", ".py").replace('\\', '/') 42 | return path 43 | 44 | def _linkcode_resolve(domain, info): 45 | if domain != 'py': 46 | return None 47 | if not info['module']: 48 | return None 49 | return "https://bitbucket.org/emacsway/sqlbuilder/src/default/%s" % get_module_path(info['module']) 50 | 51 | # Add any paths that contain templates here, relative to this directory. 52 | templates_path = ['_templates'] 53 | 54 | # The suffix(es) of source filenames. 55 | # You can specify multiple suffix as a list of string: 56 | # source_suffix = ['.rst', '.md'] 57 | source_suffix = '.rst' 58 | 59 | # The encoding of source files. 60 | #source_encoding = 'utf-8-sig' 61 | 62 | # The master toctree document. 63 | master_doc = 'index' 64 | 65 | # General information about the project. 66 | project = u'sqlbuilder' 67 | copyright = u'2015, Ivan Zakrevsky' 68 | author = u'Ivan Zakrevsky' 69 | 70 | # The version info for the project you're documenting, acts as replacement for 71 | # |version| and |release|, also used in various other places throughout the 72 | # built documents. 73 | # 74 | # The short X.Y version. 75 | version = '0' 76 | # The full version, including alpha/beta/rc tags. 77 | release = '0' 78 | 79 | # The language for content autogenerated by Sphinx. Refer to documentation 80 | # for a list of supported languages. 81 | # 82 | # This is also used if you do content translation via gettext catalogs. 83 | # Usually you set "language" from the command line for these cases. 84 | language = None 85 | 86 | # There are two options for replacing |today|: either, you set today to some 87 | # non-false value, then it is used: 88 | #today = '' 89 | # Else, today_fmt is used as the format for a strftime call. 90 | #today_fmt = '%B %d, %Y' 91 | 92 | # List of patterns, relative to source directory, that match files and 93 | # directories to ignore when looking for source files. 94 | exclude_patterns = ['_build'] 95 | 96 | # The reST default role (used for this markup: `text`) to use for all 97 | # documents. 98 | #default_role = None 99 | 100 | # If true, '()' will be appended to :func: etc. cross-reference text. 101 | #add_function_parentheses = True 102 | 103 | # If true, the current module name will be prepended to all description 104 | # unit titles (such as .. function::). 105 | #add_module_names = True 106 | 107 | # If true, sectionauthor and moduleauthor directives will be shown in the 108 | # output. They are ignored by default. 109 | #show_authors = False 110 | 111 | # The name of the Pygments (syntax highlighting) style to use. 112 | pygments_style = 'sphinx' 113 | 114 | # A list of ignored prefixes for module index sorting. 115 | #modindex_common_prefix = [] 116 | 117 | # If true, keep warnings as "system message" paragraphs in the built documents. 118 | #keep_warnings = False 119 | 120 | # If true, `todo` and `todoList` produce output, else they produce nothing. 121 | todo_include_todos = False 122 | 123 | 124 | # -- Options for HTML output ---------------------------------------------- 125 | 126 | # The theme to use for HTML and HTML Help pages. See the documentation for 127 | # a list of builtin themes. 128 | html_theme = 'nature' 129 | 130 | # Theme options are theme-specific and customize the look and feel of a theme 131 | # further. For a list of options available for each theme, see the 132 | # documentation. 133 | #html_theme_options = {} 134 | 135 | # Add any paths that contain custom themes here, relative to this directory. 136 | #html_theme_path = [] 137 | 138 | # The name for this set of Sphinx documents. If None, it defaults to 139 | # " v documentation". 140 | #html_title = None 141 | 142 | # A shorter title for the navigation bar. Default is the same as html_title. 143 | #html_short_title = None 144 | 145 | # The name of an image file (relative to this directory) to place at the top 146 | # of the sidebar. 147 | #html_logo = None 148 | 149 | # The name of an image file (within the static path) to use as favicon of the 150 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 151 | # pixels large. 152 | #html_favicon = None 153 | 154 | # Add any paths that contain custom static files (such as style sheets) here, 155 | # relative to this directory. They are copied after the builtin static files, 156 | # so a file named "default.css" will overwrite the builtin "default.css". 157 | html_static_path = ['_static'] 158 | 159 | # Add any extra paths that contain custom files (such as robots.txt or 160 | # .htaccess) here, relative to this directory. These files are copied 161 | # directly to the root of the documentation. 162 | #html_extra_path = [] 163 | 164 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 165 | # using the given strftime format. 166 | #html_last_updated_fmt = '%b %d, %Y' 167 | 168 | # If true, SmartyPants will be used to convert quotes and dashes to 169 | # typographically correct entities. 170 | #html_use_smartypants = True 171 | 172 | # Custom sidebar templates, maps document names to template names. 173 | #html_sidebars = {} 174 | 175 | # Additional templates that should be rendered to pages, maps page names to 176 | # template names. 177 | #html_additional_pages = {} 178 | 179 | # If false, no module index is generated. 180 | #html_domain_indices = True 181 | 182 | # If false, no index is generated. 183 | #html_use_index = True 184 | 185 | # If true, the index is split into individual pages for each letter. 186 | #html_split_index = False 187 | 188 | # If true, links to the reST sources are added to the pages. 189 | #html_show_sourcelink = True 190 | 191 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 192 | #html_show_sphinx = True 193 | 194 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 195 | #html_show_copyright = True 196 | 197 | # If true, an OpenSearch description file will be output, and all pages will 198 | # contain a tag referring to it. The value of this option must be the 199 | # base URL from which the finished HTML is served. 200 | #html_use_opensearch = '' 201 | 202 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 203 | #html_file_suffix = None 204 | 205 | # Language to be used for generating the HTML full-text search index. 206 | # Sphinx supports the following languages: 207 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 208 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 209 | #html_search_language = 'en' 210 | 211 | # A dictionary with options for the search language support, empty by default. 212 | # Now only 'ja' uses this config value 213 | #html_search_options = {'type': 'default'} 214 | 215 | # The name of a javascript file (relative to the configuration directory) that 216 | # implements a search results scorer. If empty, the default will be used. 217 | #html_search_scorer = 'scorer.js' 218 | 219 | # Output file base name for HTML help builder. 220 | htmlhelp_basename = 'sqlbuilderdoc' 221 | 222 | # -- Options for LaTeX output --------------------------------------------- 223 | 224 | latex_elements = { 225 | # The paper size ('letterpaper' or 'a4paper'). 226 | #'papersize': 'letterpaper', 227 | 228 | # The font size ('10pt', '11pt' or '12pt'). 229 | #'pointsize': '10pt', 230 | 231 | # Additional stuff for the LaTeX preamble. 232 | #'preamble': '', 233 | 234 | # Latex figure (float) alignment 235 | #'figure_align': 'htbp', 236 | } 237 | 238 | # Grouping the document tree into LaTeX files. List of tuples 239 | # (source start file, target name, title, 240 | # author, documentclass [howto, manual, or own class]). 241 | latex_documents = [ 242 | (master_doc, 'sqlbuilder.tex', u'sqlbuilder Documentation', 243 | u'Ivan Zakrevsky', 'manual'), 244 | ] 245 | 246 | # The name of an image file (relative to this directory) to place at the top of 247 | # the title page. 248 | #latex_logo = None 249 | 250 | # For "manual" documents, if this is true, then toplevel headings are parts, 251 | # not chapters. 252 | #latex_use_parts = False 253 | 254 | # If true, show page references after internal links. 255 | #latex_show_pagerefs = False 256 | 257 | # If true, show URL addresses after external links. 258 | #latex_show_urls = False 259 | 260 | # Documents to append as an appendix to all manuals. 261 | #latex_appendices = [] 262 | 263 | # If false, no module index is generated. 264 | #latex_domain_indices = True 265 | 266 | 267 | # -- Options for manual page output --------------------------------------- 268 | 269 | # One entry per manual page. List of tuples 270 | # (source start file, name, description, authors, manual section). 271 | man_pages = [ 272 | (master_doc, 'sqlbuilder', u'sqlbuilder Documentation', 273 | [author], 1) 274 | ] 275 | 276 | # If true, show URL addresses after external links. 277 | #man_show_urls = False 278 | 279 | 280 | # -- Options for Texinfo output ------------------------------------------- 281 | 282 | # Grouping the document tree into Texinfo files. List of tuples 283 | # (source start file, target name, title, author, 284 | # dir menu entry, description, category) 285 | texinfo_documents = [ 286 | (master_doc, 'sqlbuilder', u'sqlbuilder Documentation', 287 | author, 'sqlbuilder', 'One line description of project.', 288 | 'Miscellaneous'), 289 | ] 290 | 291 | # Documents to append as an appendix to all manuals. 292 | #texinfo_appendices = [] 293 | 294 | # If false, no module index is generated. 295 | #texinfo_domain_indices = True 296 | 297 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 298 | #texinfo_show_urls = 'footnote' 299 | 300 | # If true, do not generate a @detailmenu in the "Top" node's menu. 301 | #texinfo_no_detailmenu = False 302 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 2> nul 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\sqlbuilder.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\sqlbuilder.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import sys 4 | 5 | 6 | def main(): 7 | import django 8 | from django.conf import settings 9 | settings.configure( 10 | DATABASES = { 11 | 'default': { 12 | 'ENGINE': 'django.db.backends.sqlite3', 13 | 'NAME': ':memory:' 14 | } 15 | }, 16 | INSTALLED_APPS = [ 17 | 'django_sqlbuilder', 18 | ], 19 | MIDDLEWARE_CLASSES = [ 20 | ], 21 | STATIC_URL = '/static/', 22 | TEST_RUNNER = 'django.test.runner.DiscoverRunner', 23 | TEMPLATE_DIRS = [], 24 | DEBUG = True, 25 | TEMPLATE_DEBUG = True, 26 | ROOT_URLCONF = 'runtests', 27 | ) 28 | 29 | from django.conf.urls import include, url 30 | global urlpatterns 31 | urlpatterns = [] 32 | 33 | try: 34 | django.setup() 35 | except AttributeError: 36 | pass 37 | 38 | # Run the test suite, including the extra validation tests. 39 | from django.test.utils import get_runner 40 | TestRunner = get_runner(settings) 41 | 42 | test_runner = TestRunner(verbosity=1, interactive=False, failfast=False) 43 | failures = test_runner.run_tests([ 44 | 'django_sqlbuilder', 45 | 'sqlbuilder.smartsql', 46 | 'sqlbuilder.mini', 47 | ]) 48 | sys.exit(failures) 49 | 50 | 51 | if __name__ == "__main__": 52 | main() 53 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) 2011-2013 Ivan Zakrevsky and contributors. 4 | import os.path 5 | from io import open 6 | from setuptools import setup, find_packages 7 | 8 | app_name = os.path.basename(os.path.dirname(os.path.abspath(__file__))) 9 | 10 | setup( 11 | name = app_name, 12 | version = '0.7.10.18', 13 | 14 | packages = find_packages(), 15 | include_package_data=True, 16 | 17 | author = "Ivan Zakrevsky and contributors", 18 | author_email = "ivzak@yandex.ru", 19 | description = "SmartSQL - lightweight sql builder.", 20 | long_description=open(os.path.join(os.path.dirname(__file__), 'README.rst'), encoding='utf-8').read(), 21 | license = "BSD License", 22 | keywords = "SQL database", 23 | classifiers = [ 24 | 'Development Status :: 5 - Production/Stable', 25 | 'Intended Audience :: Developers', 26 | 'License :: OSI Approved :: BSD License', 27 | 'Operating System :: OS Independent', 28 | 'Programming Language :: Python', 29 | 'Programming Language :: Python :: 2', 30 | 'Programming Language :: Python :: 2.7', 31 | 'Programming Language :: Python :: 3', 32 | 'Programming Language :: Python :: 3.2', 33 | 'Programming Language :: Python :: 3.3', 34 | 'Programming Language :: Python :: 3.4', 35 | 'Topic :: Software Development :: Libraries :: Python Modules', 36 | ], 37 | test_suite = 'runtests.main', 38 | tests_require = [ 39 | 'Django>=1.8', 40 | ], 41 | url = "https://bitbucket.org/emacsway/{0}".format(app_name), 42 | ) 43 | -------------------------------------------------------------------------------- /sqlbuilder/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacsway/sqlbuilder/72f32bbbfc1116550343c471dc43ef6284492a5a/sqlbuilder/__init__.py -------------------------------------------------------------------------------- /sqlbuilder/django_sqlbuilder/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import warnings 4 | 5 | warnings.warn("sqlbuilder.django_sqlbuilder is deprecated. Use django_sqlbuilder instead", PendingDeprecationWarning, stacklevel=2) 6 | __path__.insert(0, os.path.join( 7 | os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(sys.modules[__name__].__file__)))), 8 | 'django_sqlbuilder' 9 | )) 10 | -------------------------------------------------------------------------------- /sqlbuilder/mini/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import re 3 | import copy 4 | import collections 5 | from weakref import WeakKeyDictionary 6 | 7 | # See also: 8 | # http://pyparsing.wikispaces.com/file/view/simpleSQL.py 9 | # http://pyparsing.wikispaces.com/file/detail/select_parser.py 10 | # https://wiki.postgresql.org/wiki/Query_Parsing 11 | # https://github.com/pganalyze/queryparser/blob/master/queryparser.c 12 | # https://github.com/lfittl/pg_query 13 | # https://code.google.com/p/php-sql-parser/ 14 | # https://pypi.python.org/pypi/sqlparse 15 | 16 | # Idea: parse SQL to DOM or JSON tree. 17 | 18 | RE_TYPE = type(re.compile("")) 19 | 20 | try: 21 | str = unicode # Python 2.* compatible 22 | string_types = (basestring,) 23 | integer_types = (int, long) 24 | range = xrange 25 | from UserList import UserList 26 | 27 | except NameError: 28 | string_types = (str,) 29 | integer_types = (int,) 30 | from collections import UserList 31 | 32 | PLACEHOLDER = "%s" # Can be re-defined by Compiler 33 | 34 | 35 | class Error(Exception): 36 | pass 37 | 38 | 39 | class State(object): 40 | 41 | def __init__(self): 42 | self.sql = [] 43 | self.params = [] 44 | self._stack = [] 45 | self.callers = ['query'] 46 | self.location = [] 47 | self.precedence = 0 48 | 49 | def push(self, attr, new_value=None): 50 | old_value = getattr(self, attr, None) 51 | self._stack.append((attr, old_value)) 52 | if new_value is None: 53 | new_value = copy.copy(old_value) 54 | setattr(self, attr, new_value) 55 | return old_value 56 | 57 | def pop(self): 58 | setattr(self, *self._stack.pop(-1)) 59 | 60 | 61 | class Compiler(object): 62 | 63 | def __init__(self, parent=None): 64 | self._children = WeakKeyDictionary() 65 | self._parents = [] 66 | 67 | self._local_registry = {} 68 | self._local_reserved_words = {} 69 | self._local_group_words = {} 70 | self._local_list_words = {} 71 | 72 | self._registry = {} 73 | self._reserved_words = {} 74 | self._group_words = {} 75 | self._list_words = {} 76 | 77 | if parent: 78 | self._parents.extend(parent._parents) 79 | self._parents.append(parent) 80 | parent._children[self] = True 81 | self._update_cache() 82 | 83 | def create_child(self): 84 | return self.__class__(self) 85 | 86 | def when(self, cls): 87 | def deco(func): 88 | self._local_registry[cls] = func 89 | self._update_cache() 90 | return func 91 | return deco 92 | 93 | def add_reserved_words(self, words): 94 | self._local_reserved_words.update((word.lower(), True) for word in words) 95 | self._update_cache() 96 | 97 | def remove_reserved_words(self, words): 98 | self._local_reserved_words.update((word.lower(), None) for word in words) 99 | self._update_cache() 100 | 101 | def is_reserved_word(self, word): 102 | return isinstance(word, string_types) and self._reserved_words.get(word.lower()) is not None 103 | 104 | def add_group_words(self, words): 105 | self._local_group_words.update((word.lower(), True) for word in words) 106 | self._update_cache() 107 | 108 | def remove_group_words(self, words): 109 | self._local_group_words.update((word.lower(), None) for word in words) 110 | self._update_cache() 111 | 112 | def is_group_word(self, word): 113 | return isinstance(word, string_types) and self._group_words.get(word.lower()) is not None 114 | 115 | def add_list_words(self, words): 116 | self._local_list_words.update((word.lower(), True) for word in words) 117 | self._update_cache() 118 | 119 | def remove_list_words(self, words): 120 | self._local_list_words.update((word.lower(), None) for word in words) 121 | self._update_cache() 122 | 123 | def is_list_word(self, word): 124 | return isinstance(word, string_types) and self._list_words.get(word.lower()) is not None 125 | 126 | def _update_cache(self): 127 | for parent in self._parents: 128 | self._registry.update(parent._local_registry) 129 | self._reserved_words.update(parent._local_reserved_words) 130 | self._group_words.update(parent._local_group_words) 131 | self._list_words.update(parent._local_list_words) 132 | self._registry.update(self._local_registry) 133 | self._reserved_words.update(self._local_reserved_words) 134 | self._group_words.update(self._local_group_words) 135 | self._list_words.update(self._local_list_words) 136 | for child in self._children: 137 | child._update_cache() 138 | 139 | def __call__(self, expr, state=None): 140 | if state is None: 141 | state = State() 142 | self(expr, state) 143 | return ''.join(state.sql), state.params 144 | 145 | cls = expr.__class__ 146 | for c in cls.mro(): 147 | if c in self._registry: 148 | self._registry[c](self, expr, state) 149 | break 150 | else: 151 | raise Error("Unknown compiler for {0}".format(cls)) 152 | 153 | compile = Compiler() 154 | 155 | 156 | @compile.when(object) 157 | def compile_object(compile, expr, state): 158 | state.sql.append(PLACEHOLDER) 159 | state.params.append(expr) 160 | 161 | 162 | @compile.when(type(None)) 163 | def compile_none(compile, expr, state): 164 | state.sql.append('NULL') 165 | 166 | 167 | def compile_list_of_params(compile, expr, state): 168 | first = True 169 | for item in expr: 170 | if first: 171 | first = False 172 | else: 173 | state.sql.append(", ") 174 | compile(item, state) 175 | 176 | 177 | @compile.when(list) 178 | @compile.when(tuple) 179 | def compile_list(compile, expr, state): 180 | if Param in state.callers: 181 | return compile_list_of_params(compile, expr, state) 182 | 183 | current_caller = state.callers[0] 184 | state.push('callers') 185 | state.callers.insert(0, 'expression') 186 | 187 | first = True 188 | for item in expr: 189 | if compile.is_reserved_word(item) and item.lower() not in ('by', 'into'): 190 | state.callers[0] = item.lower() 191 | if first: 192 | first = False 193 | else: 194 | if compile.is_list_word(current_caller): 195 | state.sql.append(", ") 196 | else: 197 | state.sql.append(" ") 198 | compile(item, state) 199 | state.pop() 200 | 201 | 202 | def compile_str(compile, expr, state): 203 | if Param in state.callers: 204 | return compile_object(compile, expr, state) 205 | state.sql.append(expr) 206 | 207 | for s in string_types: 208 | compile.when(s)(compile_str) 209 | 210 | 211 | class Param(object): 212 | __slots__ = ('_value') 213 | 214 | def __init__(self, value): 215 | self._value = value 216 | 217 | P = Param 218 | 219 | 220 | @compile.when(Param) 221 | def compile_param(compile, expr, state): 222 | state.push('callers') 223 | state.callers.insert(0, Param) 224 | compile(expr._value, state) 225 | state.pop() 226 | 227 | 228 | class Q(UserList): 229 | 230 | class NotFound(IndexError): 231 | pass 232 | 233 | def __init__(self, initlist=None): 234 | if initlist is None: 235 | initlist = [] 236 | if isinstance(initlist, Q): 237 | self.data = initlist.data 238 | else: 239 | self.data = initlist 240 | 241 | def insert_after(self, path, values): 242 | return self._insert(path, values, lambda x: x + 1) 243 | 244 | def insert_before(self, path, values): 245 | return self._insert(path, values) 246 | 247 | def append_child(self, path, values): 248 | for i in self.find(path): 249 | i.extend(values) 250 | return self 251 | 252 | def prepend_child(self, path, values): 253 | for i in self.find(path): 254 | i[0:0] = values 255 | return self 256 | 257 | def _insert(self, path, values, strategy=lambda x: x): 258 | for target in self.find(path[:-1]): 259 | for idx in self.get_matcher(path[-1])(target): 260 | idx = strategy(idx) 261 | target[idx:idx] = values 262 | return self 263 | 264 | def find(self, path): 265 | step, path_rest = path[0], path[1:] 266 | indexes = self.get_matcher(step)(self.data) 267 | if not indexes: 268 | raise self.NotFound 269 | result = [] 270 | for index in indexes: 271 | children = self.get_children_from_index(index) 272 | if path_rest: 273 | try: 274 | for i in children.find(path_rest): 275 | if i not in result: 276 | result.append(i) 277 | except self.NotFound: 278 | continue 279 | else: 280 | if children not in result: 281 | result.append(children) 282 | return result 283 | 284 | def get_children_from_index(self, idx): 285 | max_idx = len(self.data) - 1 286 | i = idx 287 | while i <= max_idx: 288 | if type(self.data[i]) is list: 289 | return type(self)(self.data[i]) 290 | i += 1 291 | raise self.NotFound 292 | 293 | @classmethod 294 | def get_matcher(cls, step): 295 | # Order is important! 296 | if isinstance(step, Matcher): 297 | return step 298 | if isinstance(step, tuple): 299 | return Filter(*map(cls.get_matcher, step)) 300 | if isinstance(step, string_types): 301 | return Exact(step) 302 | if isinstance(step, integer_types): 303 | return Index(step) 304 | if isinstance(step, slice): 305 | return Slice(step) 306 | if step is enumerate: 307 | return Each() 308 | if isinstance(step, RE_TYPE): 309 | return Re(step) 310 | if isinstance(step, type): 311 | return Type(step) 312 | if isinstance(step, collections.Callable): 313 | return Callable(step) 314 | raise Exception("Matcher not found for {!r}".format(step)) 315 | 316 | compile.when(Q)(compile_list) 317 | 318 | 319 | class Matcher(object): 320 | 321 | def __init__(self, rule=None): 322 | self._rule = rule 323 | 324 | def _match_item(self, idx, item, collection): 325 | raise NotImplementedError 326 | 327 | def __call__(self, collection): 328 | return tuple(i for i, x in enumerate(collection) if self._match_item(i, x, collection)) 329 | 330 | 331 | class Exact(Matcher): 332 | 333 | def _match_item(self, idx, item, collection): 334 | return self._rule == item 335 | 336 | 337 | class Type(Matcher): 338 | 339 | def __init__(self, rule=None): 340 | if not isinstance(rule, (list, tuple, set)): 341 | rule = (rule,) 342 | self._rule = rule 343 | 344 | def _match_item(self, idx, item, collection): 345 | return type(item) in self._rule 346 | 347 | 348 | class Index(Matcher): 349 | 350 | def _match_item(self, idx, item, collection): 351 | return idx == self._rule 352 | 353 | 354 | class Slice(Matcher): 355 | 356 | def __init__(self, start, stop=None, step=None): 357 | if isinstance(start, slice): 358 | self._rule = start 359 | else: 360 | self._rule = slice(start, stop, step) 361 | 362 | def __call__(self, collection): 363 | return tuple(range(len(collection)))[self._rule] 364 | 365 | 366 | class Each(Matcher): 367 | 368 | def _match_item(self, idx, item, collection): 369 | return True 370 | 371 | 372 | class Re(Matcher): 373 | 374 | def __init__(self, pattern, flags=0): 375 | if isinstance(pattern, RE_TYPE): 376 | self._rule = pattern 377 | else: 378 | self._rule = re.compile(pattern, flags) 379 | 380 | def _match_item(self, idx, item, collection): 381 | return isinstance(item, string_types) and self._rule.search(item) 382 | 383 | 384 | class Callable(Matcher): 385 | 386 | def _match_item(self, idx, item, collection): 387 | return self._rule(idx, item, collection) 388 | 389 | 390 | class HasChild(Matcher): 391 | 392 | def _match_item(self, idx, item, collection): 393 | try: 394 | child = Q(collection).get_children_from_index(idx) 395 | except Q.NotFound: 396 | return False 397 | else: 398 | return bool(self._rule(child)) 399 | 400 | 401 | class HasDescendant(Matcher): 402 | 403 | def _match_item(self, idx, item, collection): 404 | if HasChild(self._rule): 405 | return True 406 | try: 407 | child = Q(collection).get_children_from_index(idx) 408 | except Q.NotFound: 409 | return False 410 | else: 411 | return bool(self(child)) 412 | 413 | 414 | class HasPrevSibling(Matcher): 415 | 416 | def _match_item(self, idx, item, collection): 417 | if idx == 0: 418 | return False 419 | return idx - 1 in self._rule(collection) 420 | 421 | 422 | class HasNextSibling(Matcher): 423 | 424 | def _match_item(self, idx, item, collection): 425 | max_idx = len(collection) - 1 426 | if idx == max_idx: 427 | return False 428 | return idx + 1 in self._rule(collection) 429 | 430 | 431 | class HasPrev(Matcher): 432 | 433 | def _match_item(self, idx, item, collection): 434 | if idx == 0: 435 | return False 436 | return bool([i for i in self._rule(collection) if i < idx]) 437 | 438 | 439 | class HasNext(Matcher): 440 | 441 | def _match_item(self, idx, item, collection): 442 | max_idx = len(collection) - 1 443 | if idx == max_idx: 444 | return False 445 | return bool([i for i in self._rule(collection) if i > idx]) 446 | 447 | # We don't need HasParent and HasAncestor, because it can be handled by previous steps. 448 | # Subquery should not depend on context of usage. We don't need pass ancestors to Matcher. 449 | 450 | 451 | class AnyLevel(Matcher): 452 | 453 | def _match_item(self, idx, item, collection): 454 | try: 455 | Q(collection).get_children_from_index(idx) 456 | except Q.NotFound: 457 | return idx in self._rule(collection) 458 | else: 459 | return True 460 | 461 | 462 | class Composite(Matcher): 463 | 464 | def __init__(self, *matchers): 465 | self._rule = matchers 466 | 467 | 468 | class Filter(Composite): 469 | 470 | def __call__(self, collection): 471 | matcher, matchers_rest = self._rule[0], self._rule[1:] 472 | indexes = matcher(collection) 473 | if matchers_rest: 474 | sub_collection = [collection[i] for i in indexes] 475 | sub_matcher = type(self)(*matchers_rest) 476 | sub_indexes = sub_matcher(sub_collection) 477 | indexes = tuple(indexes[i] for i in sub_indexes) 478 | return indexes 479 | 480 | 481 | class Intersect(Composite): 482 | 483 | def __call__(self, collection): 484 | matcher, matchers_rest = self._rule[0], self._rule[1:] 485 | indexes = matcher(collection) 486 | if matchers_rest: 487 | next_matcher = type(self)(*matchers_rest) 488 | next_indexes = next_matcher(collection) 489 | indexes = sorted(set(indexes) & set(next_indexes)) 490 | return indexes 491 | 492 | 493 | class Union(Composite): 494 | 495 | def __call__(self, collection): 496 | matcher, matchers_rest = self._rule[0], self._rule[1:] 497 | indexes = matcher(collection) 498 | if matchers_rest: 499 | next_matcher = type(self)(*matchers_rest) 500 | next_indexes = next_matcher(collection) 501 | indexes = sorted(set(indexes) | set(next_indexes)) 502 | return indexes 503 | 504 | 505 | compile.add_reserved_words( 506 | """ 507 | absolute action add all allocate alter and any are as asc assertion at 508 | authorization avg begin between bit bit_length both by cascade cascaded 509 | case cast catalog char character char_ length character_length check close 510 | coalesce collate collation column commit connect connection constraint 511 | constraints continue convert corresponding count create cross current 512 | current_date current_time current_timestamp current_ user cursor date day 513 | deallocate dec decimal declare default deferrable deferred delete desc 514 | describe descriptor diagnostics disconnect distinct domain double drop 515 | else end end-exec escape except exception exec execute exists external 516 | extract false fetch first float for foreign found from full get global go 517 | goto grant group having hour identity immediate in indicator initially 518 | inner input insensitive insert int integer intersect interval into is 519 | isolation join key language last leading left level like local lower 520 | match max min minute module month names national natural nchar next no 521 | not null nullif numeric octet_length of on only open option or order 522 | outer output overlaps pad partial position precision prepare preserve 523 | primary prior privileges procedure public read real references relative 524 | restrict revoke right rollback rows schema scroll second section select 525 | session session_ user set size smallint some space sql sqlcode sqlerror 526 | sqlstate substring sum system_user table temporary then time timestamp 527 | timezone_ hour timezone_minute to trailing transaction translate 528 | translation trim true union unique unknown update upper usage user using 529 | value values varchar varying view when whenever where with work write 530 | year zone 531 | """.split() + ['order by', 'group by'] 532 | ) 533 | 534 | # TODO: Should "SET" to be list words? 535 | compile.add_list_words( 536 | """ 537 | group insert order select values 538 | """.split() + ['order by', 'group by', 'insert into'] 539 | ) 540 | -------------------------------------------------------------------------------- /sqlbuilder/mini/parser.py: -------------------------------------------------------------------------------- 1 | import sqlparse 2 | 3 | 4 | class Parser(object): 5 | 6 | def __init__(self, sql, list_words=()): 7 | self._sql = sql 8 | self._list_words = list_words 9 | 10 | def _handle_level(self, stmt): 11 | result = [] 12 | for token in stmt.tokens: 13 | if token.is_group(): 14 | result.append(self._handle_level(token)) 15 | else: 16 | if token.is_whitespace(): 17 | continue 18 | if token.match(sqlparse.tokens.Punctuation, ','): 19 | if isinstance(token.parent, sqlparse.sql.IdentifierList): 20 | continue 21 | result.append(str(token)) 22 | return result 23 | 24 | def to_hierarchical_list(self): 25 | parsed = sqlparse.parse(self._sql) 26 | stmt = parsed[0] 27 | return self._handle_level(stmt) 28 | 29 | 30 | if __name__ == '__main__': 31 | sql = """select f1, f2 from "someschema"."mytable" where id = 1 and f2 = 3 group by f2, f3 order by id, f2 DESC""" 32 | import pprint 33 | pprint.pprint(Parser(sql).to_hierarchical_list()) 34 | # print sqlparse.format(sql, reindent=True, keyword_case='upper') 35 | -------------------------------------------------------------------------------- /sqlbuilder/mini/tests.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import re 3 | import unittest 4 | from sqlbuilder.mini import P, Q, compile 5 | 6 | __all__ = ('TestMini', 'TestMiniQ') 7 | 8 | 9 | class TestCase(unittest.TestCase): 10 | 11 | maxDiff = None 12 | 13 | 14 | class TestMini(TestCase): 15 | 16 | def test_mini(self): 17 | 18 | sql = [ 19 | 'SELECT', [ 20 | 'author.id', 'author.first_name', 'author.last_name' 21 | ], 22 | 'FROM', [ 23 | 'author', 'INNER JOIN', ['book as b', 'ON', 'b.author_id = author.id'] 24 | ], 25 | 'WHERE', [ 26 | 'b.status', '==', P('new') 27 | ], 28 | 'ORDER BY', [ 29 | 'author.first_name', 'author.last_name' 30 | ] 31 | ] 32 | 33 | # Let change query 34 | sql[sql.index('SELECT') + 1].append('author.age') 35 | 36 | self.assertEqual( 37 | compile(sql), 38 | ('SELECT author.id, author.first_name, author.last_name, author.age FROM author INNER JOIN book as b ON b.author_id = author.id WHERE b.status == %s ORDER BY author.first_name, author.last_name', ['new']) 39 | ) 40 | 41 | def test_mini_precompiled(self): 42 | 43 | sql = [ 44 | 'SELECT', [ 45 | 'author.id', 'author.first_name', 'author.last_name' 46 | ], 47 | 'FROM', [ 48 | 'author', 'INNER JOIN', ['book as b', 'ON', 'b.author_id = author.id'] 49 | ], 50 | 'WHERE', [ 51 | 'b.status == %(status)s' 52 | ], 53 | 'ORDER BY', [ 54 | 'author.first_name', 'author.last_name' 55 | ] 56 | ] 57 | 58 | # Let change query 59 | sql[sql.index('SELECT') + 1].append('author.age') 60 | 61 | sql_str = compile(sql)[0] 62 | self.assertEqual( 63 | (sql_str, {'status': 'new'}), 64 | ('SELECT author.id, author.first_name, author.last_name, author.age FROM author INNER JOIN book as b ON b.author_id = author.id WHERE b.status == %(status)s ORDER BY author.first_name, author.last_name', {'status': 'new'}) 65 | ) 66 | 67 | 68 | class TestMiniQ(TestCase): 69 | 70 | def setUp(self): 71 | self._sql = [ 72 | 'SELECT', [ 73 | 'author.id', 'author.first_name', 'author.last_name' 74 | ], 75 | 'FROM', [ 76 | 'author', 'INNER JOIN', [ 77 | '(', 'SELECT', [ 78 | 'book.title' 79 | ], 80 | 'FROM', [ 81 | 'book' 82 | ], 83 | ')', 'AS b', 'ON', 'b.author_id = author.id' 84 | ], 85 | ], 86 | 'WHERE', [ 87 | 'b.status', '==', P('new') 88 | ], 89 | 'ORDER BY', [ 90 | 'author.first_name', 'author.last_name' 91 | ] 92 | ] 93 | 94 | def test_mini_q(self): 95 | 96 | sql = Q(self._sql) 97 | sql.prepend_child( 98 | ['FROM', 'INNER JOIN', 'SELECT'], 99 | ['book.id', 'book.pages'] 100 | ) 101 | sql.append_child( 102 | ['FROM', 'INNER JOIN', 'SELECT'], 103 | ['book.date'] 104 | ) 105 | sql.insert_after( 106 | ['FROM', 'INNER JOIN', (list, 1), ], 107 | ['WHERE', ['b.pages', '>', P(100)]] 108 | ) 109 | sql.insert_before( 110 | ['FROM', 'INNER JOIN', 'WHERE', 'b.pages'], 111 | ['b.pages', '<', P(500), 'AND'] 112 | ) 113 | 114 | sql.append_child( 115 | ['FROM', 'INNER JOIN', (lambda i, item, collection: item == 'SELECT')], 116 | ['book.added_by_callable'] 117 | ) 118 | sql.append_child( 119 | ['FROM', 'INNER JOIN', ('SELECT', 0)], 120 | ['book.added_by_tuple'] 121 | ) 122 | sql.append_child( 123 | ['FROM', enumerate, 'SELECT'], 124 | ['book.added_by_each'] 125 | ) 126 | sql.append_child( 127 | ['FROM', 'INNER JOIN', 1], 128 | ['book.added_by_index'] 129 | ) 130 | sql.append_child( 131 | ['FROM', 'INNER JOIN', re.compile("^SELECT$")], 132 | ['book.added_by_re'] 133 | ) 134 | 135 | self.assertEqual( 136 | compile(sql), 137 | ('SELECT author.id, author.first_name, author.last_name FROM author INNER JOIN ( SELECT book.id, book.pages, book.title, book.date, book.added_by_callable, book.added_by_tuple, book.added_by_each, book.added_by_index, book.added_by_re FROM book WHERE b.pages < %s AND b.pages > %s ) AS b ON b.author_id = author.id WHERE b.status == %s ORDER BY author.first_name, author.last_name', [500, 100, 'new']) 138 | ) 139 | -------------------------------------------------------------------------------- /sqlbuilder/smartsql/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Some ideas from http://code.google.com/p/py-smart-sql-constructor/ , but implementation another. 3 | # Pay attention also to excellent lightweight SQLBuilder 4 | # of Storm ORM http://bazaar.launchpad.net/~storm/storm/trunk/view/head:/storm/expr.py 5 | from __future__ import absolute_import 6 | 7 | from sqlbuilder.smartsql.compiler import Compiler, State, cached_compile, compile 8 | from sqlbuilder.smartsql.constants import CONTEXT, DEFAULT_DIALECT, LOOKUP_SEP, MAX_PRECEDENCE, OPERATOR, PLACEHOLDER 9 | from sqlbuilder.smartsql.exceptions import Error, MaxLengthError, OperatorNotFound 10 | from sqlbuilder.smartsql.expressions import ( 11 | Operable, Expr, ExprList, CompositeExpr, Param, Parentheses, OmitParentheses, 12 | Callable, NamedCallable, Constant, ConstantSpace, Case, Cast, Concat, 13 | Alias, Name, NameCompiler, Value, ValueCompiler, Array, ArrayItem, 14 | expr_repr, datatypeof, const, func, compile_exprlist 15 | ) 16 | from sqlbuilder.smartsql.factory import factory, Factory 17 | from sqlbuilder.smartsql.fields import MetaFieldSpace, F, MetaField, Field, Subfield, FieldList 18 | from sqlbuilder.smartsql.operator_registry import OperatorRegistry, operator_registry 19 | from sqlbuilder.smartsql.operators import ( 20 | Binary, NamedBinary, NamedCompound, Add, Sub, Mul, Div, Gt, Lt, Ge, Le, And, Or, 21 | Eq, Ne, Is, IsNot, In, NotIn, RShift, LShift, EscapeForLike, Like, ILike, 22 | Ternary, NamedTernary, Between, NotBetween, 23 | Prefix, NamedPrefix, Not, All, Distinct, Exists, 24 | Unary, NamedUnary, Pos, Neg, 25 | Postfix, NamedPostfix, OrderDirection, Asc, Desc, 26 | compile_binary 27 | ) 28 | from sqlbuilder.smartsql.pycompat import str, string_types 29 | from sqlbuilder.smartsql.tables import ( 30 | MetaTableSpace, T, MetaTable, FieldProxy, Table, TableAlias, TableJoin, 31 | Join, InnerJoin, LeftJoin, RightJoin, FullJoin, CrossJoin, ModelRegistry, model_registry 32 | ) 33 | from sqlbuilder.smartsql.utils import Undef, UndefType, AutoName, auto_name, is_allowed_attr, is_list, opt_checker, same, warn 34 | 35 | from sqlbuilder.smartsql.queries import ( 36 | Result, Executable, Select, Query, SelectCount, Raw, 37 | Modify, Insert, Update, Delete, 38 | Set, Union, Intersect, Except, 39 | ) 40 | 41 | SPACE = " " 42 | Placeholder = Param 43 | Condition = Binary 44 | NamedCondition = NamedBinary 45 | 46 | 47 | def qn(name, compile): 48 | return compile(Name(name))[0] 49 | 50 | A, C, E, P, TA, Q, QS = Alias, Condition, Expr, Placeholder, TableAlias, Query, Query 51 | 52 | # compile.set_precedence(270, '.') 53 | # compile.set_precedence(260, '::') 54 | # compile.set_precedence(250, '[', ']') # array element selection 55 | compile.set_precedence(240, Pos, Neg, (Unary, '+'), (Unary, '-'), '~') # unary minus 56 | compile.set_precedence(230, '^') 57 | compile.set_precedence(220, Mul, Div, (Binary, '*'), (Binary, '/'), (Binary, '%')) 58 | compile.set_precedence(210, Add, Sub, (Binary, '+'), (Binary, '-')) 59 | compile.set_precedence(200, LShift, RShift, '<<', '>>') 60 | compile.set_precedence(190, '&') 61 | compile.set_precedence(180, '#') 62 | compile.set_precedence(170, '|') 63 | compile.set_precedence(160, Is, 'IS') 64 | compile.set_precedence(150, (Postfix, 'ISNULL'), (Postfix, 'NOTNULL')) 65 | compile.set_precedence(140, '(any other)') # all other native and user-defined operators 66 | compile.set_precedence(130, In, NotIn, 'IN') 67 | compile.set_precedence(120, Between, 'BETWEEN') 68 | compile.set_precedence(110, 'OVERLAPS') 69 | compile.set_precedence(100, Like, ILike, 'LIKE', 'ILIKE', 'SIMILAR') 70 | compile.set_precedence(90, Lt, Gt, '<', '>') 71 | compile.set_precedence(80, Le, Ge, Ne, '<=', '>=', '<>', '!=') 72 | compile.set_precedence(70, Eq, '=') 73 | compile.set_precedence(60, Not, 'NOT') 74 | compile.set_precedence(50, And, 'AND') 75 | compile.set_precedence(40, Or, 'OR') 76 | compile.set_precedence(30, Set, Union, Intersect, Except) 77 | compile.set_precedence(20, Select, Query, SelectCount, Raw, Insert, Update, Delete) 78 | compile.set_precedence(10, Expr) 79 | compile.set_precedence(None, All, Distinct) 80 | 81 | 82 | from sqlbuilder.smartsql.datatypes import AbstractType, BaseType 83 | operator_registry.register(OPERATOR.ADD, (BaseType, BaseType), BaseType, Add) 84 | operator_registry.register(OPERATOR.SUB, (BaseType, BaseType), BaseType, Sub) 85 | operator_registry.register(OPERATOR.MUL, (BaseType, BaseType), BaseType, Mul) 86 | operator_registry.register(OPERATOR.DIV, (BaseType, BaseType), BaseType, Div) 87 | operator_registry.register(OPERATOR.GT, (BaseType, BaseType), BaseType, Gt) 88 | operator_registry.register(OPERATOR.LT, (BaseType, BaseType), BaseType, Lt) 89 | operator_registry.register(OPERATOR.GE, (BaseType, BaseType), BaseType, Ge) 90 | operator_registry.register(OPERATOR.LE, (BaseType, BaseType), BaseType, Le) 91 | operator_registry.register(OPERATOR.AND, (BaseType, BaseType), BaseType, And) 92 | operator_registry.register(OPERATOR.OR, (BaseType, BaseType), BaseType, Or) 93 | operator_registry.register(OPERATOR.EQ, (BaseType, BaseType), BaseType, Eq) 94 | operator_registry.register(OPERATOR.NE, (BaseType, BaseType), BaseType, Ne) 95 | operator_registry.register(OPERATOR.IS, (BaseType, BaseType), BaseType, Is) 96 | operator_registry.register(OPERATOR.IS_NOT, (BaseType, BaseType), BaseType, IsNot) 97 | operator_registry.register(OPERATOR.IN, (BaseType, BaseType), BaseType, In) 98 | operator_registry.register(OPERATOR.NOT_IN, (BaseType, BaseType), BaseType, NotIn) 99 | operator_registry.register(OPERATOR.RSHIFT, (BaseType, BaseType), BaseType, RShift) 100 | operator_registry.register(OPERATOR.LSHIFT, (BaseType, BaseType), BaseType, LShift) 101 | operator_registry.register(OPERATOR.LIKE, (BaseType, BaseType), BaseType, Like) 102 | operator_registry.register(OPERATOR.ILIKE, (BaseType, BaseType), BaseType, ILike) 103 | -------------------------------------------------------------------------------- /sqlbuilder/smartsql/compiler.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import weakref 3 | from functools import wraps 4 | from sqlbuilder.smartsql.constants import CONTEXT, MAX_PRECEDENCE 5 | from sqlbuilder.smartsql.exceptions import Error 6 | 7 | __all__ = ('Compiler', 'State', 'cached_compile', 'compile', ) 8 | 9 | 10 | class Compiler(object): 11 | 12 | def __init__(self, parent=None): 13 | self._children = weakref.WeakKeyDictionary() 14 | self._parents = [] 15 | self._local_registry = {} 16 | self._local_precedence = {} 17 | self._registry = {} 18 | self._precedence = {} 19 | if parent: 20 | self._parents.extend(parent._parents) 21 | self._parents.append(parent) 22 | parent._children[self] = True 23 | self._update_cache() 24 | 25 | def create_child(self): 26 | return self.__class__(self) 27 | 28 | def when(self, cls): 29 | def deco(func): 30 | self._local_registry[cls] = func 31 | self._update_cache() 32 | return func 33 | return deco 34 | 35 | def set_precedence(self, precedence, *types): 36 | for type in types: 37 | self._local_precedence[type] = precedence 38 | self._update_cache() 39 | 40 | def _update_cache(self): 41 | for parent in self._parents: 42 | self._registry.update(parent._local_registry) 43 | self._precedence.update(parent._local_precedence) 44 | self._registry.update(self._local_registry) 45 | self._precedence.update(self._local_precedence) 46 | for child in self._children: 47 | child._update_cache() 48 | 49 | def __call__(self, expr, state=None): 50 | if state is None: 51 | state = State() 52 | self(expr, state) 53 | return ''.join(state.sql), state.params 54 | 55 | cls = expr.__class__ 56 | parentheses = None 57 | outer_precedence = state.precedence 58 | inner_precedence = self.get_inner_precedence(expr) 59 | if inner_precedence is None: 60 | # pass current precedence 61 | # FieldList, ExprList, All, Distinct...? 62 | inner_precedence = outer_precedence 63 | state.precedence = inner_precedence 64 | if inner_precedence < outer_precedence: 65 | parentheses = True 66 | 67 | if parentheses: 68 | state.sql.append('(') 69 | 70 | for c in cls.__mro__: 71 | if c in self._registry: 72 | self._registry[c](self, expr, state) 73 | break 74 | else: 75 | raise Error("Unknown compiler for {0}".format(cls)) 76 | 77 | if parentheses: 78 | state.sql.append(')') 79 | state.precedence = outer_precedence 80 | 81 | def get_inner_precedence(self, cls_or_expr): 82 | if isinstance(cls_or_expr, type): 83 | cls = cls_or_expr 84 | if cls in self._precedence: 85 | return self._precedence[cls] 86 | else: 87 | expr = cls_or_expr 88 | cls = expr.__class__ 89 | if hasattr(expr, 'sql'): 90 | try: 91 | if (cls, expr.sql) in self._precedence: 92 | return self._precedence[(cls, expr.sql)] 93 | elif expr.sql in self._precedence: 94 | return self._precedence[expr.sql] 95 | except TypeError: 96 | # For case when expr.sql is unhashable, for example we can allow T('tablename').sql (in future). 97 | pass 98 | return self.get_inner_precedence(cls) 99 | 100 | return MAX_PRECEDENCE # self._precedence.get('(any other)', MAX_PRECEDENCE) 101 | 102 | 103 | class State(object): 104 | 105 | def __init__(self): 106 | self.sql = [] 107 | self.params = [] 108 | self._stack = [] 109 | self.auto_tables = [] 110 | self.auto_join_tables = [] 111 | self.joined_table_statements = set() 112 | self.context = CONTEXT.QUERY 113 | self.precedence = 0 114 | 115 | def push(self, attr, new_value=None): 116 | old_value = getattr(self, attr, None) 117 | self._stack.append((attr, old_value)) 118 | if new_value is None: 119 | new_value = copy.copy(old_value) 120 | setattr(self, attr, new_value) 121 | return old_value 122 | 123 | def pop(self): 124 | setattr(self, *self._stack.pop(-1)) 125 | 126 | 127 | def cached_compile(f): 128 | @wraps(f) 129 | def deco(compile, expr, state): 130 | cache_key = (compile, state.context) 131 | if cache_key not in expr.__cached__: 132 | state.push('sql', []) 133 | f(compile, expr, state) 134 | # TODO: also cache state.tables? 135 | expr.__cached__[cache_key] = ''.join(state.sql) 136 | state.pop() 137 | state.sql.append(expr.__cached__[cache_key]) 138 | return deco 139 | 140 | 141 | def querify(compile, expr, state): 142 | state.push('sql', []) 143 | state.push('params', []) 144 | compile(expr, state) 145 | try: 146 | return (state.sql, state.params) 147 | finally: 148 | state.pop() 149 | state.pop() 150 | 151 | 152 | compile = Compiler() 153 | -------------------------------------------------------------------------------- /sqlbuilder/smartsql/compilers/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from sqlbuilder.smartsql import warn 4 | 5 | warn('sqlbuilder.smartsql.compilers', 'sqlbuilder.smartsql.dialects') 6 | __path__.insert(0, os.path.join( 7 | os.path.dirname(os.path.dirname(os.path.realpath(sys.modules[__name__].__file__))), 8 | 'dialects' 9 | )) 10 | -------------------------------------------------------------------------------- /sqlbuilder/smartsql/constants.py: -------------------------------------------------------------------------------- 1 | __all__ = ('CONTEXT', 'DEFAULT_DIALECT', 'LOOKUP_SEP', 'MAX_PRECEDENCE', 'OPERATOR', 'PLACEHOLDER',) 2 | 3 | 4 | LOOKUP_SEP = '__' 5 | MAX_PRECEDENCE = 1000 6 | DEFAULT_DIALECT = 'postgres' 7 | PLACEHOLDER = "%s" # Can be re-defined by registered dialect. 8 | 9 | 10 | class CONTEXT: 11 | QUERY = 'QUERY' 12 | FIELD = 'FIELD' 13 | FIELD_PREFIX = 'FIELD_PREFIX' 14 | FIELD_NAME = 'FIELD_NAME' 15 | TABLE = 'TABLE' 16 | EXPR = 'EXPR' 17 | SELECT = 'SELECT' 18 | 19 | 20 | class OPERATOR: 21 | ADD = '+' 22 | SUB = '-' 23 | MUL = '*' 24 | DIV = '/' 25 | GT = '>' 26 | LT = '<' 27 | GE = '>=' 28 | LE = '<=' 29 | AND = 'AND' 30 | OR = 'OR' 31 | EQ = '=' 32 | NE = '<>' 33 | IS = 'IS' 34 | IS_NOT = 'IS NOT' 35 | IN = 'IN' 36 | NOT_IN = 'NOT IN' 37 | RSHIFT = '>>' 38 | LSHIFT = '<<' 39 | LIKE = 'LIKE' 40 | ILIKE = 'ILIKE' 41 | -------------------------------------------------------------------------------- /sqlbuilder/smartsql/contrib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacsway/sqlbuilder/72f32bbbfc1116550343c471dc43ef6284492a5a/sqlbuilder/smartsql/contrib/__init__.py -------------------------------------------------------------------------------- /sqlbuilder/smartsql/contrib/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacsway/sqlbuilder/72f32bbbfc1116550343c471dc43ef6284492a5a/sqlbuilder/smartsql/contrib/tests/__init__.py -------------------------------------------------------------------------------- /sqlbuilder/smartsql/contrib/tests/test_evaluate.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacsway/sqlbuilder/72f32bbbfc1116550343c471dc43ef6284492a5a/sqlbuilder/smartsql/contrib/tests/test_evaluate.py -------------------------------------------------------------------------------- /sqlbuilder/smartsql/datatypes.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from sqlbuilder.smartsql.constants import OPERATOR 3 | from sqlbuilder.smartsql.expressions import Alias, Concat, Value, datatypeof, func 4 | from sqlbuilder.smartsql.operator_registry import operator_registry 5 | from sqlbuilder.smartsql.operators import ( 6 | Binary, EscapeForLike, Like, ILike, All, Asc, Desc, Between, Distinct, Neg, Not, Pos 7 | ) 8 | from sqlbuilder.smartsql.utils import Undef, is_list, warn 9 | 10 | __all__ = ('AbstractType', 'BaseType', ) 11 | 12 | 13 | # TODO: Datatype should be aware about its scheme/operator_registry. Pass operator_registry to constructor? 14 | class AbstractType(object): 15 | __slots__ = ('_expr',) 16 | 17 | def __init__(self, expr): 18 | self._expr = expr # weakref.ref(expr) 19 | 20 | def _op(self, operator, operands, *args, **kwargs): 21 | expression_factory = operator_registry.get(operator, tuple(map(datatypeof, operands)))[1] 22 | return expression_factory(*(operands + args), **kwargs) 23 | 24 | 25 | class BaseType(AbstractType): 26 | 27 | def __add__(self, other): 28 | return self._op(OPERATOR.ADD, (self._expr, other)) 29 | 30 | def __radd__(self, other): 31 | return self._op(OPERATOR.ADD, (other, self._expr)) 32 | 33 | def __sub__(self, other): 34 | return self._op(OPERATOR.SUB, (self._expr, other)) 35 | 36 | def __rsub__(self, other): 37 | return self._op(OPERATOR.SUB, (other, self._expr)) 38 | 39 | def __mul__(self, other): 40 | return self._op(OPERATOR.MUL, (self._expr, other)) 41 | 42 | def __rmul__(self, other): 43 | return self._op(OPERATOR.MUL, (other, self._expr)) 44 | 45 | def __div__(self, other): 46 | return self._op(OPERATOR.DIV, (self._expr, other)) 47 | 48 | def __rdiv__(self, other): 49 | return self._op(OPERATOR.DIV, (other, self._expr)) 50 | 51 | def __truediv__(self, other): 52 | return self._op(OPERATOR.DIV, (self._expr, other)) 53 | 54 | def __rtruediv__(self, other): 55 | return self._op(OPERATOR.DIV, (other, self._expr)) 56 | 57 | def __floordiv__(self, other): 58 | return self._op(OPERATOR.DIV, (self._expr, other)) 59 | 60 | def __rfloordiv__(self, other): 61 | return self._op(OPERATOR.DIV, (other, self._expr)) 62 | 63 | def __and__(self, other): 64 | return self._op(OPERATOR.AND, (self._expr, other)) 65 | 66 | def __rand__(self, other): 67 | return self._op(OPERATOR.AND, (other, self._expr)) 68 | 69 | def __or__(self, other): 70 | return self._op(OPERATOR.OR, (self._expr, other)) 71 | 72 | def __ror__(self, other): 73 | return self._op(OPERATOR.OR, (other, self._expr)) 74 | 75 | def __gt__(self, other): 76 | return self._op(OPERATOR.GT, (self._expr, other)) 77 | 78 | def __lt__(self, other): 79 | return self._op(OPERATOR.LT, (self._expr, other)) 80 | 81 | def __ge__(self, other): 82 | return self._op(OPERATOR.GE, (self._expr, other)) 83 | 84 | def __le__(self, other): 85 | return self._op(OPERATOR.LE, (self._expr, other)) 86 | 87 | def __eq__(self, other): 88 | if other is None: 89 | return self.is_(None) 90 | if is_list(other): 91 | return self.in_(other) 92 | return self._op(OPERATOR.EQ, (self._expr, other)) 93 | 94 | def __ne__(self, other): 95 | if other is None: 96 | return self.is_not(None) 97 | if is_list(other): 98 | return self.not_in(other) 99 | return self._op(OPERATOR.NE, (self._expr, other)) 100 | 101 | def __rshift__(self, other): 102 | return self._op(OPERATOR.RSHIFT, (self._expr, other)) 103 | 104 | def __rrshift__(self, other): 105 | return self._op(OPERATOR.RSHIFT, (other, self._expr)) 106 | 107 | def __lshift__(self, other): 108 | return self._op(OPERATOR.LSHIFT, (self._expr, other)) 109 | 110 | def __rlshift__(self, other): 111 | return self._op(OPERATOR.LSHIFT, (other, self._expr)) 112 | 113 | def is_(self, other): 114 | return self._op(OPERATOR.IS, (self._expr, other)) 115 | 116 | def is_not(self, other): 117 | return self._op(OPERATOR.IS_NOT, (self._expr, other)) 118 | 119 | def in_(self, other): 120 | return self._op(OPERATOR.IN, (self._expr, other)) 121 | 122 | def not_in(self, other): 123 | return self._op(OPERATOR.NOT_IN, (self._expr, other)) 124 | 125 | def like(self, other, escape=Undef): 126 | return self._op(OPERATOR.LIKE, (self._expr, other), escape=escape) 127 | 128 | def ilike(self, other, escape=Undef): 129 | return ILike(self._expr, other, escape=escape) 130 | 131 | def rlike(self, other, escape=Undef): 132 | return Like(other, self._expr, escape=escape) 133 | 134 | def rilike(self, other, escape=Undef): 135 | return ILike(other, self._expr, escape=escape) 136 | 137 | def startswith(self, other): 138 | pattern = EscapeForLike(other) 139 | return Like(self._expr, Concat(pattern, Value('%')), escape=pattern.escape) 140 | 141 | def istartswith(self, other): 142 | pattern = EscapeForLike(other) 143 | return ILike(self._expr, Concat(pattern, Value('%')), escape=pattern.escape) 144 | 145 | def contains(self, other): # TODO: ambiguous with "@>" operator of postgresql. 146 | pattern = EscapeForLike(other) 147 | return Like(self._expr, Concat(Value('%'), pattern, Value('%')), escape=pattern.escape) 148 | 149 | def icontains(self, other): 150 | pattern = EscapeForLike(other) 151 | return ILike(self._expr, Concat(Value('%'), pattern, Value('%')), escape=pattern.escape) 152 | 153 | def endswith(self, other): 154 | pattern = EscapeForLike(other) 155 | return Like(self._expr, Concat(Value('%'), pattern), escape=pattern.escape) 156 | 157 | def iendswith(self, other): 158 | pattern = EscapeForLike(other) 159 | return ILike(self._expr, Concat(Value('%'), pattern), escape=pattern.escape) 160 | 161 | def rstartswith(self, other): 162 | pattern = EscapeForLike(self._expr) 163 | return Like(other, Concat(pattern, Value('%')), escape=pattern.escape) 164 | 165 | def ristartswith(self, other): 166 | pattern = EscapeForLike(self._expr) 167 | return ILike(other, Concat(pattern, Value('%')), escape=pattern.escape) 168 | 169 | def rcontains(self, other): 170 | pattern = EscapeForLike(self._expr) 171 | return Like(other, Concat(Value('%'), pattern, Value('%')), escape=pattern.escape) 172 | 173 | def ricontains(self, other): 174 | pattern = EscapeForLike(self._expr) 175 | return ILike(other, Concat(Value('%'), pattern, Value('%')), escape=pattern.escape) 176 | 177 | def rendswith(self, other): 178 | pattern = EscapeForLike(self._expr) 179 | return Like(other, Concat(Value('%'), pattern), escape=pattern.escape) 180 | 181 | def riendswith(self, other): 182 | pattern = EscapeForLike(self._expr) 183 | return ILike(other, Concat(Value('%'), pattern), escape=pattern.escape) 184 | 185 | def __pos__(self): 186 | return Pos(self._expr) 187 | 188 | def __neg__(self): 189 | return Neg(self._expr) 190 | 191 | def __invert__(self): 192 | return Not(self._expr) 193 | 194 | def all(self): 195 | return All(self._expr) 196 | 197 | def distinct(self): 198 | return Distinct(self._expr) 199 | 200 | def __pow__(self, other): 201 | return func.Power(self._expr, other) 202 | 203 | def __rpow__(self, other): 204 | return func.Power(other, self._expr) 205 | 206 | def __mod__(self, other): 207 | return func.Mod(self._expr, other) 208 | 209 | def __rmod__(self, other): 210 | return func.Mod(other, self._expr) 211 | 212 | def __abs__(self): 213 | return func.Abs(self._expr) 214 | 215 | def count(self): 216 | return func.Count(self._expr) 217 | 218 | def as_(self, alias): 219 | return Alias(self._expr, alias) 220 | 221 | def between(self, start, end): 222 | return Between(self._expr, start, end) 223 | 224 | def concat(self, *args): 225 | return Concat(self._expr, *args) 226 | 227 | def concat_ws(self, sep, *args): 228 | return Concat(self._expr, *args).ws(sep) 229 | 230 | def op(self, op): 231 | return lambda other: Binary(self._expr, op, other) 232 | 233 | def rop(self, op): # useless, can be P('lookingfor').op('=')(expr) 234 | return lambda other: Binary(other, op, self._expr) 235 | 236 | def asc(self): 237 | return Asc(self._expr) 238 | 239 | def desc(self): 240 | return Desc(self._expr) 241 | 242 | def __getitem__(self, key): 243 | """Returns self.between()""" 244 | # Is it should return ArrayItem(key) or Subfield(self._expr, key)? 245 | # Ambiguity with Query and ExprList!!! 246 | # Name conflict with Query.__getitem__(). Query can returns a single array. 247 | # We also may want to apply Between() or Eq() to subquery. 248 | if isinstance(key, slice): 249 | warn('__getitem__(slice(...))', 'between(start, end)') 250 | start = key.start or 0 251 | end = key.stop or sys.maxsize 252 | return Between(self._expr, start, end) 253 | else: 254 | warn('__getitem__(key)', '__eq__(key)') 255 | return self.__eq__(key) 256 | 257 | __hash__ = object.__hash__ 258 | -------------------------------------------------------------------------------- /sqlbuilder/smartsql/dialects/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacsway/sqlbuilder/72f32bbbfc1116550343c471dc43ef6284492a5a/sqlbuilder/smartsql/dialects/__init__.py -------------------------------------------------------------------------------- /sqlbuilder/smartsql/dialects/cassandra.py: -------------------------------------------------------------------------------- 1 | from .. import compile as parent_compile, Name, Field, Value, ValueCompiler 2 | 3 | try: 4 | str = unicode # Python 2.* compatible 5 | string_types = (basestring,) 6 | integer_types = (int, long) 7 | 8 | except NameError: 9 | string_types = (str,) 10 | integer_types = (int,) 11 | 12 | compile = parent_compile.create_child() 13 | 14 | 15 | @compile.when(Field) 16 | def compile_field(compile, expr, state): 17 | compile(expr.name, state) 18 | 19 | 20 | compile_value = ValueCompiler(escape_delimiter="\\") 21 | compile.when(Value)(compile_value) 22 | -------------------------------------------------------------------------------- /sqlbuilder/smartsql/dialects/mongodb.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import operator 3 | import weakref 4 | from sqlbuilder.smartsql.constants import CONTEXT, OPERATOR 5 | from sqlbuilder.smartsql.expressions import Param, Name 6 | from sqlbuilder.smartsql.exceptions import Error 7 | from sqlbuilder.smartsql.fields import Field 8 | from sqlbuilder.smartsql.operators import Binary 9 | from sqlbuilder.smartsql.tables import Table 10 | from sqlbuilder.smartsql.queries import Select 11 | 12 | __all__ = ('Compiler', 'State', 'compile') 13 | 14 | 15 | OPERATOR_MAPPING = { 16 | OPERATOR.GT: '$gt', 17 | OPERATOR.LT: '$lt', 18 | OPERATOR.GE: '$gte', 19 | OPERATOR.LE: '$lte', 20 | OPERATOR.EQ: '$eq', 21 | OPERATOR.NE: '$ne', 22 | } 23 | 24 | COMPOUND_OPERATOR_MAPPING = { 25 | OPERATOR.AND: '$and', 26 | OPERATOR.OR: '$or', 27 | } 28 | 29 | 30 | class Compiler(object): 31 | 32 | def __init__(self, parent=None): 33 | self._children = weakref.WeakKeyDictionary() 34 | self._parents = [] 35 | self._local_registry = {} 36 | self._registry = {} 37 | if parent: 38 | self._parents.extend(parent._parents) 39 | self._parents.append(parent) 40 | parent._children[self] = True 41 | self._update_cache() 42 | 43 | def create_child(self): 44 | return self.__class__(self) 45 | 46 | def when(self, cls): 47 | def deco(func): 48 | self._local_registry[cls] = func 49 | self._update_cache() 50 | return func 51 | return deco 52 | 53 | def _update_cache(self): 54 | for parent in self._parents: 55 | self._registry.update(parent._local_registry) 56 | self._registry.update(self._local_registry) 57 | for child in self._children: 58 | child._update_cache() 59 | 60 | def __call__(self, expr, state=None): 61 | if state is None: 62 | state = State() 63 | 64 | cls = expr.__class__ 65 | for c in cls.__mro__: 66 | if c in self._registry: 67 | return self._registry[c](self, expr, state) 68 | else: 69 | raise Error("Unknown executor for {0}".format(cls)) 70 | 71 | compile = Compiler() 72 | 73 | 74 | class State(object): 75 | 76 | def __init__(self): 77 | self.collection_name = None 78 | self._stack = [] 79 | self.context = CONTEXT.QUERY 80 | 81 | def push(self, attr, new_value=None): 82 | old_value = getattr(self, attr, None) 83 | self._stack.append((attr, old_value)) 84 | if new_value is None: 85 | new_value = copy.copy(old_value) 86 | setattr(self, attr, new_value) 87 | return old_value 88 | 89 | def pop(self): 90 | setattr(self, *self._stack.pop(-1)) 91 | 92 | 93 | def execute(query, database): 94 | """ 95 | @type database: pymongo.collection.Collection 96 | @rtype: pymongo.cursor.CursorType 97 | """ 98 | collection = database[query['$collectionName']] 99 | cursor = collection.find(query['$query']) 100 | if query.get('$orderby'): 101 | cursor = cursor.sort(query['$orderby']) 102 | return cursor 103 | 104 | 105 | @compile.when(object) 106 | def compile_python_builtin(compile, expr, state): 107 | return expr 108 | 109 | 110 | @compile.when(Name) 111 | def compile_field(compile, expr, state): 112 | return expr.name 113 | 114 | 115 | @compile.when(Table) 116 | def compile_field(compile, expr, state): 117 | if not expr._parent: 118 | collection_name = compile(expr._name, state) 119 | if state.collection_name is not None: 120 | assert state.collection_name == collection_name, "Different collections inside query" 121 | else: 122 | state.collection_name = collection_name 123 | return '' 124 | else: 125 | return compile(expr._name, state) 126 | 127 | 128 | @compile.when(Field) 129 | def compile_field(compile, expr, state): 130 | result = None 131 | if expr._prefix is not None and state.context != CONTEXT.FIELD_NAME: 132 | state.push("context", CONTEXT.FIELD_PREFIX) 133 | result = compile(expr._prefix, state) 134 | state.pop() 135 | if result: 136 | result += '.' 137 | result += compile(expr._name, state) 138 | return result 139 | 140 | 141 | @compile.when(Param) 142 | def compile_field(compile, expr, state): 143 | return expr.params 144 | 145 | 146 | @compile.when(Binary) 147 | def compile_field(compile, expr, state): 148 | if expr.sql in OPERATOR_MAPPING: 149 | return {compile(expr.left, state): {OPERATOR_MAPPING[expr.sql]: compile(expr.right, state)}} 150 | elif expr.sql in COMPOUND_OPERATOR_MAPPING: 151 | return {OPERATOR_MAPPING[expr.sql]: [compile(expr.left, state), compile(expr.right, state)]} 152 | else: 153 | raise Error("Unknown operator {0}".format(expr.sql)) 154 | 155 | 156 | @compile.when(Select) 157 | def compile_field(compile, expr, state): 158 | result = {} 159 | state.push("context") 160 | state.context = CONTEXT.EXPR 161 | if expr.where(): 162 | result['$query'] = compile(expr.where(), state) 163 | if expr.order_by(): 164 | result['$orderby'] = compile(expr.order_by(), state) 165 | result['$collectionName'] = state.collection_name 166 | state.pop() 167 | return result 168 | -------------------------------------------------------------------------------- /sqlbuilder/smartsql/dialects/mysql.py: -------------------------------------------------------------------------------- 1 | from .. import ( 2 | compile as parent_compile, SPACE, Binary, Concat, ExprList, Insert, Name, 3 | NameCompiler, Parentheses, Query, Value, ValueCompiler 4 | ) 5 | 6 | try: 7 | str = unicode # Python 2.* compatible 8 | string_types = (basestring,) 9 | integer_types = (int, long) 10 | 11 | except NameError: 12 | string_types = (str,) 13 | integer_types = (int,) 14 | 15 | compile = parent_compile.create_child() 16 | 17 | TRANSLATION_MAP = { 18 | 'LIKE': 'LIKE BINARY', 19 | 'ILIKE': 'LIKE', 20 | } 21 | 22 | compile_name = NameCompiler(delimiter='`', escape_delimiter='`', max_length=64) 23 | compile.when(Name)(compile_name) 24 | 25 | compile_value = ValueCompiler(escape_delimiter="\\") 26 | compile.when(Value)(compile_value) 27 | 28 | 29 | @compile.when(Binary) 30 | def compile_condition(compile, expr, state): 31 | compile(expr.left, state) 32 | state.sql.append(SPACE) 33 | state.sql.append(TRANSLATION_MAP.get(expr.sql, expr.sql)) 34 | state.sql.append(SPACE) 35 | compile(expr.right, state) 36 | 37 | 38 | @compile.when(Concat) 39 | def compile_concat(compile, expr, state): 40 | if not expr.ws(): 41 | state.sql.append('CONCAT(') 42 | first = True 43 | for a in expr: 44 | if first: 45 | first = False 46 | else: 47 | state.sql.append(', ') 48 | compile(a, state) 49 | state.sql.append(')') 50 | else: 51 | state.sql.append('CONCAT_WS(') 52 | compile(expr.ws(), state) 53 | for a in expr: 54 | state.sql.append(expr.sql) 55 | compile(a, state) 56 | state.sql.append(')') 57 | 58 | 59 | @compile.when(Insert) 60 | def compile_insert(compile, expr, state): 61 | state.sql.append("INSERT ") 62 | if expr.ignore: 63 | state.sql.append("IGNORE ") 64 | state.sql.append("INTO ") 65 | compile(expr.table, state) 66 | state.sql.append(SPACE) 67 | compile(Parentheses(expr.fields), state) 68 | if isinstance(expr.values, Query): 69 | state.sql.append(SPACE) 70 | compile(expr.values, state) 71 | else: 72 | state.sql.append(" VALUES ") 73 | compile(ExprList(*expr.values).join(', '), state) 74 | if expr.on_duplicate_key_update: 75 | state.sql.append(" ON DUPLICATE KEY UPDATE ") 76 | first = True 77 | for f, v in expr.on_duplicate_key_update: 78 | if first: 79 | first = False 80 | else: 81 | state.sql.append(", ") 82 | compile(f, state) 83 | state.sql.append(" = ") 84 | compile(v, state) 85 | -------------------------------------------------------------------------------- /sqlbuilder/smartsql/dialects/python.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import operator 3 | import weakref 4 | from sqlbuilder.smartsql.constants import CONTEXT, OPERATOR 5 | from sqlbuilder.smartsql.expressions import Param 6 | from sqlbuilder.smartsql.exceptions import Error 7 | from sqlbuilder.smartsql.compiler import compile 8 | from sqlbuilder.smartsql.fields import Field 9 | from sqlbuilder.smartsql.operators import Binary 10 | 11 | __all__ = ('Executor', 'State', 'execute') 12 | 13 | 14 | OPERATOR_MAPPING = { 15 | OPERATOR.ADD: operator.add, 16 | OPERATOR.SUB: operator.sub, 17 | OPERATOR.MUL: operator.mul, 18 | OPERATOR.DIV: operator.truediv, 19 | OPERATOR.GT: operator.gt, 20 | OPERATOR.LT: operator.lt, 21 | OPERATOR.GE: operator.ge, 22 | OPERATOR.LE: operator.le, 23 | OPERATOR.AND: operator.and_, 24 | OPERATOR.OR: operator.or_, 25 | OPERATOR.EQ: operator.eq, 26 | OPERATOR.NE: operator.ne, 27 | OPERATOR.IS: operator.is_, 28 | OPERATOR.IS_NOT: operator.is_not, 29 | OPERATOR.RSHIFT: operator.rshift, 30 | OPERATOR.LSHIFT: operator.lshift, 31 | } 32 | 33 | 34 | class Executor(object): 35 | compile = compile 36 | 37 | def __init__(self, parent=None): 38 | self._children = weakref.WeakKeyDictionary() 39 | self._parents = [] 40 | self._local_registry = {} 41 | self._registry = {} 42 | if parent: 43 | self._parents.extend(parent._parents) 44 | self._parents.append(parent) 45 | parent._children[self] = True 46 | self._update_cache() 47 | 48 | def create_child(self): 49 | return self.__class__(self) 50 | 51 | def when(self, cls): 52 | def deco(func): 53 | self._local_registry[cls] = func 54 | self._update_cache() 55 | return func 56 | return deco 57 | 58 | def _update_cache(self): 59 | for parent in self._parents: 60 | self._registry.update(parent._local_registry) 61 | self._registry.update(self._local_registry) 62 | for child in self._children: 63 | child._update_cache() 64 | 65 | def get_row_key(self, field): 66 | return self.compile(field)[0] 67 | 68 | def __call__(self, expr, state=None): 69 | cls = expr.__class__ 70 | for c in cls.__mro__: 71 | if c in self._registry: 72 | return self._registry[c](self, expr, state) 73 | else: 74 | raise Error("Unknown executor for {0}".format(cls)) 75 | 76 | execute = Executor() 77 | 78 | 79 | class State(object): 80 | 81 | def __init__(self): 82 | # For join we simple add joined objects to the row 83 | self.row = {} 84 | self.rows_factory = lambda table: () # for joins 85 | self._stack = [] 86 | self.auto_tables = [] 87 | self.auto_join_tables = [] 88 | self.joined_table_statements = set() 89 | self.context = CONTEXT.QUERY 90 | 91 | def push(self, attr, new_value=None): 92 | old_value = getattr(self, attr, None) 93 | self._stack.append((attr, old_value)) 94 | if new_value is None: 95 | new_value = copy.copy(old_value) 96 | setattr(self, attr, new_value) 97 | return old_value 98 | 99 | def pop(self): 100 | setattr(self, *self._stack.pop(-1)) 101 | 102 | 103 | @execute.when(object) 104 | def execute_python_builtin(execute, expr, state): 105 | return expr 106 | 107 | 108 | @execute.when(Field) 109 | def execute_field(execute, expr, state): 110 | return state.row[execute.get_row_key(expr)] 111 | 112 | 113 | @execute.when(Param) 114 | def execute_field(execute, expr, state): 115 | return expr.params 116 | 117 | 118 | @execute.when(Binary) 119 | def execute_field(execute, expr, state): 120 | return OPERATOR_MAPPING[expr.sql](execute(expr.left, state), execute(expr.right, state)) 121 | -------------------------------------------------------------------------------- /sqlbuilder/smartsql/dialects/sqlite.py: -------------------------------------------------------------------------------- 1 | from .. import compile as parent_compile, SPACE, Name, NameCompiler, Binary 2 | 3 | compile = parent_compile.create_child() 4 | 5 | TRANSLATION_MAP = { 6 | 'LIKE': 'GLOB', 7 | 'ILIKE': 'LIKE', 8 | } 9 | 10 | 11 | @compile.when(object) 12 | def compile_object(compile, expr, state): 13 | state.sql.append('?') 14 | state.params.append(expr) 15 | 16 | 17 | compile_name = NameCompiler(delimiter='`', escape_delimiter='`') 18 | compile.when(Name)(compile_name) 19 | 20 | 21 | @compile.when(Binary) 22 | def compile_condition(compile, expr, state): 23 | compile(expr.left, state) 24 | state.sql.append(SPACE) 25 | state.sql.append(TRANSLATION_MAP.get(expr.sql, expr.sql)) 26 | state.sql.append(SPACE) 27 | compile(expr.right, state) 28 | -------------------------------------------------------------------------------- /sqlbuilder/smartsql/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | __all__ = ('Error', 'MaxLengthError', 'OperatorNotFound', ) 3 | 4 | 5 | class Error(Exception): 6 | pass 7 | 8 | 9 | class MaxLengthError(Error): 10 | pass 11 | 12 | 13 | class OperatorNotFound(Error): 14 | pass 15 | -------------------------------------------------------------------------------- /sqlbuilder/smartsql/factory.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from sqlbuilder.smartsql.pycompat import string_types 3 | 4 | __all__ = ('Factory', 'factory',) 5 | 6 | 7 | class Factory(object): 8 | 9 | def register(self, name_or_callable): 10 | name = name_or_callable if isinstance(name_or_callable, string_types) else name_or_callable.__name__ 11 | 12 | def deco(callable_obj): 13 | 14 | def wrapped_obj(*a, **kw): 15 | instance = callable_obj(*a, **kw) 16 | instance.__factory__ = self 17 | return instance 18 | 19 | setattr(self, name, wrapped_obj) 20 | return callable_obj 21 | 22 | return deco if isinstance(name_or_callable, string_types) else deco(name_or_callable) 23 | 24 | @classmethod 25 | def get(cls, instance): 26 | try: 27 | return instance.__factory__ 28 | except AttributeError: 29 | return cls.default() 30 | 31 | @staticmethod 32 | def default(): 33 | cls = Factory 34 | if not hasattr(cls, '_default'): 35 | cls._default = cls() 36 | return cls._default 37 | 38 | factory = Factory.default() 39 | -------------------------------------------------------------------------------- /sqlbuilder/smartsql/fields.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from sqlbuilder.smartsql.compiler import compile, cached_compile 3 | from sqlbuilder.smartsql.constants import LOOKUP_SEP, CONTEXT 4 | from sqlbuilder.smartsql.expressions import Operable, Expr, Constant, ExprList, Parentheses, Name, compile_exprlist 5 | from sqlbuilder.smartsql.pycompat import string_types 6 | 7 | __all__ = ('MetaFieldSpace', 'F', 'MetaField', 'Field', 'Subfield', 'FieldList', ) 8 | 9 | 10 | class MetaFieldSpace(type): 11 | 12 | def __instancecheck__(cls, instance): 13 | return isinstance(instance, Field) 14 | 15 | def __subclasscheck__(cls, subclass): 16 | return issubclass(subclass, Field) 17 | 18 | def __getattr__(cls, key): 19 | if key[:2] == '__': 20 | raise AttributeError 21 | parts = key.split(LOOKUP_SEP, 2) 22 | prefix, name, alias = parts + [None] * (3 - len(parts)) 23 | if name is None: 24 | prefix, name = name, prefix 25 | f = cls(name, prefix) 26 | return f.as_(alias) if alias else f 27 | 28 | def __call__(cls, *a, **kw): 29 | return Field(*a, **kw) 30 | 31 | 32 | class F(MetaFieldSpace("NewBase", (object, ), {})): 33 | pass 34 | 35 | 36 | class MetaField(type): 37 | 38 | def __getattr__(cls, key): 39 | if key[:2] == '__': 40 | raise AttributeError 41 | parts = key.split(LOOKUP_SEP, 2) 42 | prefix, name, alias = parts + [None] * (3 - len(parts)) 43 | if name is None: 44 | prefix, name = name, prefix 45 | f = cls(name, prefix) 46 | return f.as_(alias) if alias else f 47 | 48 | 49 | class Field(MetaField("NewBase", (Expr,), {})): 50 | # It's a field, not column, because prefix can be alias of subquery. 51 | # It also can be a field of composite column. 52 | __slots__ = ('_name', '_prefix', '__cached__') # TODO: m_* prefix instead of _* prefix? 53 | 54 | def __init__(self, name, prefix=None, datatype=None): 55 | Operable.__init__(self, datatype) 56 | if isinstance(name, string_types): 57 | if name == '*': 58 | name = Constant(name) 59 | else: 60 | name = Name(name) 61 | self._name = name 62 | if isinstance(prefix, string_types): 63 | from sqlbuilder.smartsql.tables import Table 64 | prefix = Table(prefix) 65 | self._prefix = prefix 66 | self.__cached__ = {} 67 | 68 | 69 | @compile.when(Field) 70 | @cached_compile # The cache depends on state.context 71 | def compile_field(compile, expr, state): 72 | if expr._prefix is not None and state.context != CONTEXT.FIELD_NAME: 73 | state.auto_tables.append(expr._prefix) # it's important to know the concrete alias of table. 74 | state.push("context", CONTEXT.FIELD_PREFIX) 75 | compile(expr._prefix, state) 76 | state.pop() 77 | state.sql.append('.') 78 | compile(expr._name, state) 79 | 80 | 81 | class Subfield(Expr): 82 | 83 | __slots__ = ('parent', 'name', ) 84 | 85 | # TODO: Should we have here the same order of arguments as Alias, TaleAlias or the same as Field? 86 | # This variant Subfield(T.author.address, street) looks better than Subfield(street, T.author.address) 87 | def __init__(self, parent, name, datatype=None): 88 | Operable.__init__(self, datatype) 89 | self.parent = parent 90 | if isinstance(name, string_types): 91 | name = Name(name) 92 | self.name = name 93 | 94 | 95 | @compile.when(Subfield) 96 | def compile_subfield(compile, expr, state): 97 | parent = expr.parent 98 | if True: # get me from context 99 | parent = Parentheses(parent) 100 | compile(parent) 101 | state.sql.append('.') 102 | compile(expr.name, state) 103 | 104 | 105 | class FieldList(ExprList): 106 | __slots__ = () 107 | 108 | def __init__(self, *args): 109 | # if args and is_list(args[0]): 110 | # return self.__init__(*args[0]) 111 | Operable.__init__(self) 112 | self.sql, self.data = ", ", list(args) 113 | -------------------------------------------------------------------------------- /sqlbuilder/smartsql/operator_registry.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import weakref 3 | 4 | __all__ = ('OperatorRegistry', 'operator_registry', ) 5 | 6 | 7 | class OperatorRegistry(object): 8 | 9 | def __init__(self, parent=None): 10 | self._children = weakref.WeakKeyDictionary() 11 | self._parents = [] 12 | self._local_registry = {} 13 | self._registry = {} 14 | if parent: 15 | self._parents.extend(parent._parents) 16 | self._parents.append(parent) 17 | parent._children[self] = True 18 | self._update_cache() 19 | 20 | def create_child(self): 21 | return self.__class__(self) 22 | 23 | def register(self, operator, operands, result_type, expression_factory): 24 | self._registry[(operator, operands)] = (result_type, expression_factory) 25 | self._update_cache() 26 | 27 | def get(self, operator, operands): 28 | try: 29 | return self._registry[(operator, operands)] 30 | except KeyError: 31 | # raise OperatorNotFound(operator, operands) 32 | from sqlbuilder.smartsql.datatypes import BaseType 33 | from sqlbuilder.smartsql.operators import Binary 34 | return (BaseType, lambda l, r: Binary(l, operator, r)) 35 | 36 | def _update_cache(self): 37 | for parent in self._parents: 38 | self._registry.update(parent._local_registry) 39 | self._registry.update(self._local_registry) 40 | for child in self._children: 41 | child._update_cache() 42 | 43 | operator_registry = OperatorRegistry() 44 | -------------------------------------------------------------------------------- /sqlbuilder/smartsql/operators.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from functools import reduce 3 | from sqlbuilder.smartsql.compiler import compile 4 | from sqlbuilder.smartsql.expressions import Expr, Operable, Value, datatypeof, func 5 | from sqlbuilder.smartsql.operator_registry import operator_registry 6 | from sqlbuilder.smartsql.pycompat import string_types 7 | from sqlbuilder.smartsql.utils import Undef 8 | 9 | __all__ = ( 10 | 'Binary', 'NamedBinary', 'NamedCompound', 'Add', 'Sub', 'Mul', 'Div', 'Gt', 'Lt', 'Ge', 'Le', 'And', 'Or', 11 | 'Eq', 'Ne', 'Is', 'IsNot', 'In', 'NotIn', 'RShift', 'LShift', 'EscapeForLike', 'Like', 'ILike', 12 | 'Ternary', 'NamedTernary', 'Between', 'NotBetween', 13 | 'Prefix', 'NamedPrefix', 'Not', 'All', 'Distinct', 'Exists', 14 | 'Unary', 'NamedUnary', 'Pos', 'Neg', 15 | 'Postfix', 'NamedPostfix', 'OrderDirection', 'Asc', 'Desc' 16 | ) 17 | 18 | SPACE = " " 19 | 20 | 21 | class Binary(Expr): 22 | __slots__ = ('left', 'right') 23 | 24 | def __init__(self, left, op, right): 25 | op = op.upper() 26 | datatype = operator_registry.get(op, (datatypeof(left), datatypeof(right)))[0] 27 | Expr.__init__(self, op, datatype=datatype) 28 | self.left = left 29 | self.right = right 30 | 31 | 32 | @compile.when(Binary) 33 | def compile_binary(compile, expr, state): 34 | compile(expr.left, state) 35 | state.sql.append(SPACE) 36 | state.sql.append(expr.sql) 37 | state.sql.append(SPACE) 38 | compile(expr.right, state) 39 | 40 | 41 | class NamedBinary(Binary): 42 | __slots__ = () 43 | 44 | def __init__(self, left, right): 45 | # Don't use multi-arguments form like And(*args) 46 | # Use reduce(operator.and_, args) or reduce(And, args) instead. SRP. 47 | datatype = operator_registry.get(self.sql, (datatypeof(left), datatypeof(right)))[0] 48 | Operable.__init__(self, datatype) 49 | self.left = left 50 | self.right = right 51 | 52 | 53 | class NamedCompound(NamedBinary): 54 | __slots__ = () 55 | 56 | def __init__(self, *exprs): 57 | self.left = reduce(self.__class__, exprs[:-1]) 58 | self.right = exprs[-1] 59 | datatype = operator_registry.get(self.sql, (datatypeof(self.left), datatypeof(self.right)))[0] 60 | Operable.__init__(self, datatype) 61 | 62 | 63 | class Add(NamedCompound): 64 | sql = '+' 65 | 66 | 67 | class Sub(NamedBinary): 68 | __slots__ = () 69 | sql = '-' 70 | 71 | 72 | class Mul(NamedCompound): 73 | __slots__ = () 74 | sql = '*' 75 | 76 | 77 | class Div(NamedBinary): 78 | __slots__ = () 79 | sql = '/' 80 | 81 | 82 | class Gt(NamedBinary): 83 | __slots__ = () 84 | sql = '>' 85 | 86 | 87 | class Lt(NamedBinary): 88 | __slots__ = () 89 | sql = '<' 90 | 91 | 92 | class Ge(NamedBinary): 93 | __slots__ = () 94 | sql = '>=' 95 | 96 | 97 | class Le(NamedBinary): 98 | __slots__ = () 99 | sql = '<=' 100 | 101 | 102 | class And(NamedCompound): 103 | __slots__ = () 104 | sql = 'AND' 105 | 106 | 107 | class Or(NamedCompound): 108 | __slots__ = () 109 | sql = 'OR' 110 | 111 | 112 | class Eq(NamedBinary): 113 | __slots__ = () 114 | sql = '=' 115 | 116 | 117 | class Ne(NamedBinary): 118 | __slots__ = () 119 | sql = '<>' 120 | 121 | 122 | class Is(NamedBinary): 123 | __slots__ = () 124 | sql = 'IS' 125 | 126 | 127 | class IsNot(NamedBinary): 128 | __slots__ = () 129 | sql = 'IS NOT' 130 | 131 | 132 | class In(NamedBinary): 133 | __slots__ = () 134 | sql = 'IN' 135 | 136 | 137 | class NotIn(NamedBinary): 138 | __slots__ = () 139 | sql = 'NOT IN' 140 | 141 | 142 | class RShift(NamedBinary): 143 | __slots__ = () 144 | sql = ">>" 145 | 146 | 147 | class LShift(NamedBinary): 148 | __slots__ = () 149 | sql = "<<" 150 | 151 | 152 | class EscapeForLike(Expr): 153 | 154 | __slots__ = ('expr',) 155 | 156 | escape = "!" 157 | escape_map = tuple( # Ordering is important! 158 | (i, "!{0}".format(i)) for i in ('!', '_', '%') 159 | ) 160 | 161 | def __init__(self, expr): 162 | Operable.__init__(self) 163 | self.expr = expr 164 | 165 | 166 | @compile.when(EscapeForLike) 167 | def compile_escapeforlike(compile, expr, state): 168 | escaped = expr.expr 169 | for k, v in expr.escape_map: 170 | escaped = func.Replace(escaped, Value(k), Value(v)) 171 | compile(escaped, state) 172 | 173 | 174 | class Like(NamedBinary): 175 | __slots__ = ('escape',) 176 | sql = 'LIKE' 177 | 178 | def __init__(self, left, right, escape=Undef): 179 | """ 180 | :type escape: str | Undef 181 | """ 182 | Operable.__init__(self) 183 | self.left = left 184 | self.right = right 185 | if isinstance(right, EscapeForLike): 186 | self.escape = right.escape 187 | else: 188 | self.escape = escape 189 | 190 | 191 | class ILike(Like): 192 | __slots__ = () 193 | sql = 'ILIKE' 194 | 195 | 196 | @compile.when(Like) 197 | def compile_like(compile, expr, state): 198 | compile_binary(compile, expr, state) 199 | if expr.escape is not Undef: 200 | state.sql.append(' ESCAPE ') 201 | compile(Value(expr.escape) if isinstance(expr.escape, string_types) else expr.escape, state) 202 | 203 | 204 | # Ternary 205 | 206 | class Ternary(Expr): 207 | __slots__ = ('second_sql', 'first', 'second', 'third') 208 | 209 | def __init__(self, first, sql, second, second_sql, third): 210 | Expr.__init__(self, sql) 211 | self.first = first 212 | self.second = second 213 | self.second_sql = second_sql 214 | self.third = third 215 | 216 | 217 | @compile.when(Ternary) 218 | def compile_ternary(compile, expr, state): 219 | compile(expr.first, state) 220 | state.sql.append(SPACE) 221 | state.sql.append(expr.sql) 222 | state.sql.append(SPACE) 223 | compile(expr.second, state) 224 | state.sql.append(SPACE) 225 | state.sql.append(expr.second_sql) 226 | state.sql.append(SPACE) 227 | compile(expr.third, state) 228 | 229 | 230 | class NamedTernary(Ternary): 231 | __slots__ = () 232 | 233 | def __init__(self, first, second, third): 234 | Operable.__init__(self) 235 | self.first = first 236 | self.second = second 237 | self.third = third 238 | 239 | 240 | class Between(NamedTernary): 241 | __slots__ = () 242 | sql = 'BETWEEN' 243 | second_sql = 'AND' 244 | 245 | 246 | class NotBetween(Between): 247 | __slots__ = () 248 | sql = 'NOT BETWEEN' 249 | 250 | 251 | # Prefix 252 | 253 | class Prefix(Expr): 254 | 255 | __slots__ = ('expr', ) 256 | 257 | def __init__(self, prefix, expr): 258 | Expr.__init__(self, prefix) 259 | self.expr = expr 260 | 261 | 262 | @compile.when(Prefix) 263 | def compile_prefix(compile, expr, state): 264 | state.sql.append(expr.sql) 265 | state.sql.append(SPACE) 266 | compile(expr.expr, state) 267 | 268 | 269 | class NamedPrefix(Prefix): 270 | __slots__ = () 271 | 272 | def __init__(self, expr): 273 | Operable.__init__(self) 274 | self.expr = expr 275 | 276 | 277 | class Not(NamedPrefix): 278 | __slots__ = () 279 | sql = 'NOT' 280 | 281 | 282 | class All(NamedPrefix): 283 | __slots__ = () 284 | sql = 'ALL' 285 | 286 | 287 | class Distinct(NamedPrefix): 288 | __slots__ = () 289 | sql = 'DISTINCT' 290 | 291 | 292 | class Exists(NamedPrefix): 293 | __slots__ = () 294 | sql = 'EXISTS' 295 | 296 | 297 | # Unary 298 | 299 | class Unary(Prefix): 300 | __slots__ = () 301 | 302 | 303 | @compile.when(Unary) 304 | def compile_unary(compile, expr, state): 305 | state.sql.append(expr.sql) 306 | compile(expr.expr, state) 307 | 308 | 309 | class NamedUnary(Unary): 310 | __slots__ = () 311 | 312 | def __init__(self, expr): 313 | self.expr = expr 314 | 315 | 316 | class Pos(NamedUnary): 317 | __slots__ = () 318 | sql = '+' 319 | 320 | 321 | class Neg(NamedUnary): 322 | __slots__ = () 323 | sql = '-' 324 | 325 | 326 | # Postfix 327 | 328 | class Postfix(Expr): 329 | __slots__ = ('expr', ) 330 | 331 | def __init__(self, expr, postfix): 332 | Expr.__init__(self, postfix) 333 | self.expr = expr 334 | 335 | 336 | @compile.when(Postfix) 337 | def compile_postfix(compile, expr, state): 338 | compile(expr.expr, state) 339 | state.sql.append(SPACE) 340 | state.sql.append(expr.sql) 341 | 342 | 343 | class NamedPostfix(Postfix): 344 | __slots__ = () 345 | 346 | def __init__(self, expr): 347 | Operable.__init__(self) 348 | self.expr = expr 349 | 350 | 351 | class OrderDirection(NamedPostfix): 352 | __slots__ = () 353 | 354 | def __init__(self, expr): 355 | Operable.__init__(self) 356 | if isinstance(expr, OrderDirection): 357 | expr = expr.expr 358 | self.expr = expr 359 | 360 | 361 | class Asc(OrderDirection): 362 | __slots__ = () 363 | sql = 'ASC' 364 | 365 | 366 | class Desc(OrderDirection): 367 | __slots__ = () 368 | sql = 'DESC' 369 | -------------------------------------------------------------------------------- /sqlbuilder/smartsql/pycompat.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | __all__ = ('str', 'string_types', 'integer_types') 4 | 5 | try: 6 | str = unicode # Python 2.* compatible 7 | string_types = (basestring,) 8 | integer_types = (int, long) 9 | 10 | except NameError: 11 | str = str 12 | string_types = (str,) 13 | integer_types = (int,) 14 | -------------------------------------------------------------------------------- /sqlbuilder/smartsql/tables.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import copy 3 | import collections 4 | from sqlbuilder.smartsql.compiler import compile 5 | from sqlbuilder.smartsql.constants import LOOKUP_SEP, CONTEXT 6 | from sqlbuilder.smartsql.expressions import CompositeExpr, Expr, ExprList, OmitParentheses, Name, expr_repr 7 | from sqlbuilder.smartsql.factory import factory 8 | from sqlbuilder.smartsql.fields import Field 9 | from sqlbuilder.smartsql.pycompat import string_types 10 | from sqlbuilder.smartsql.utils import same, warn 11 | 12 | __all__ = ( 13 | 'MetaTableSpace', 'T', 'MetaTable', 'FieldProxy', 'Table', 'TableAlias', 'TableJoin', 14 | 'Join', 'InnerJoin', 'LeftJoin', 'RightJoin', 'FullJoin', 'CrossJoin', 'ModelRegistry', 'model_registry', 15 | ) 16 | 17 | SPACE = " " 18 | 19 | 20 | @factory.register 21 | class MetaTableSpace(type): 22 | 23 | def __instancecheck__(cls, instance): 24 | return isinstance(instance, Table) 25 | 26 | def __subclasscheck__(cls, subclass): 27 | return issubclass(subclass, Table) 28 | 29 | def __getattr__(cls, key): 30 | if key.startswith('__'): 31 | raise AttributeError 32 | parts = key.split(LOOKUP_SEP, 1) 33 | name, alias = parts + [None] * (2 - len(parts)) 34 | table = cls(name) 35 | return table.as_(alias) if alias else table 36 | 37 | def __call__(cls, name, *a, **kw): 38 | return cls.__factory__.Table(name, *a, **kw) 39 | 40 | 41 | @factory.register 42 | class T(factory.MetaTableSpace("NewBase", (object, ), {})): 43 | pass 44 | 45 | 46 | class MetaTable(type): 47 | 48 | def __getattr__(cls, key): 49 | if key[:2] == '__': 50 | raise AttributeError 51 | parts = key.split(LOOKUP_SEP, 1) 52 | name, alias = parts + [None] * (2 - len(parts)) 53 | table = cls(name) 54 | return table.as_(alias) if alias else table 55 | 56 | 57 | class FieldProxy(object): 58 | 59 | def __init__(self, table): 60 | self.__table = table 61 | 62 | def __getattr__(self, key): 63 | if key[:2] == '__': 64 | raise AttributeError 65 | return self.__table.get_field(key) 66 | 67 | __call__ = __getattr__ 68 | __getitem__ = __getattr__ 69 | 70 | def __repr__(self): 71 | return "<{0}: {1}>".format(type(self).__name__, expr_repr(self.id._prefix)) 72 | 73 | 74 | @compile.when(FieldProxy) 75 | def compile_fieldproxy(compile, expr, state): 76 | compile(expr.id._prefix, state) 77 | 78 | 79 | # TODO: Schema support. Not only for table. 80 | # A database contains one or more named schemas, which in turn contain tables. 81 | # Schemas also contain other kinds of named objects, including data types, functions, and operators. 82 | # http://www.postgresql.org/docs/9.4/static/ddl-schemas.html 83 | # Ideas: S.public(T.user), S('public', T.user) 84 | 85 | 86 | @factory.register 87 | class Table(MetaTable("NewBase", (object, ), {})): 88 | # Variants: 89 | # tb.as_ => Field(); tb().as_ => instancemethod() ??? 90 | # author + InnerJoin + book + On + author.id == book.author_id 91 | # Add __call__() method to Field/Alias 92 | # Use sys._getframe(), compiler.visitor.ASTVisitor or tokenize.generate_tokens() to get context for Table.__getattr__() 93 | 94 | __slots__ = ('_name', '_parent', '__cached__', 'f', '_fields', '__factory__') 95 | 96 | # "fields" should be the single argument, 97 | # see the "q" argument of django_sqlbuilder.models.Table for more info. 98 | def __init__(self, name, fields=(), parent=None): 99 | if isinstance(name, string_types): 100 | name = Name(name) 101 | self._name = name 102 | self._parent = parent 103 | self.__cached__ = {} 104 | self.f = FieldProxy(self) 105 | self._fields = collections.OrderedDict() 106 | for f in fields: 107 | self._append_field(f) 108 | 109 | def as_(self, alias): 110 | return factory.get(self).TableAlias(self, alias) 111 | 112 | def inner_join(self, right): 113 | return factory.get(self).TableJoin(self).inner_join(right) 114 | 115 | def left_join(self, right): 116 | return factory.get(self).TableJoin(self).left_join(right) 117 | 118 | def right_join(self, right): 119 | return factory.get(self).TableJoin(self).right_join(right) 120 | 121 | def full_join(self, right): 122 | return factory.get(self).TableJoin(self).full_join(right) 123 | 124 | def cross_join(self, right): 125 | return factory.get(self).TableJoin(self).cross_join(right) 126 | 127 | def join(self, join_type, obj): 128 | return factory.get(self).TableJoin(self).join(join_type, obj) 129 | 130 | def on(self, cond): 131 | return factory.get(self).TableJoin(self).on(cond) 132 | 133 | def hint(self, expr): 134 | return factory.get(self).TableJoin(self).hint(expr) 135 | 136 | def natural(self): 137 | return factory.get(self).TableJoin(self).natural() 138 | 139 | def using(self, *fields): 140 | return factory.get(self).TableJoin(self).using(*fields) 141 | 142 | def _append_field(self, field): 143 | self._fields[field._name] = field 144 | field.prefix = self 145 | 146 | def get_field(self, key): 147 | cache = self.f.__dict__ 148 | if key in cache: 149 | return cache[key] 150 | 151 | if type(key) == tuple: 152 | cache[key] = CompositeExpr(*(self.get_field(k) for k in key)) 153 | return cache[key] 154 | 155 | parts = key.split(LOOKUP_SEP, 1) 156 | name, alias = parts + [None] * (2 - len(parts)) 157 | 158 | if name in cache: 159 | f = cache[name] 160 | else: 161 | f = self._fields[name] if name in self._fields else Field(name, self) 162 | cache[name] = f 163 | if alias: 164 | f = f.as_(alias) 165 | cache[key] = f 166 | return f 167 | 168 | def __getattr__(self, key): 169 | if key[:2] == '__' or key in Table.__slots__: 170 | raise AttributeError 171 | return self.get_field(key) 172 | 173 | def __getitem__(self, key): 174 | return self.get_field(key) 175 | 176 | def __repr__(self): 177 | return expr_repr(self) 178 | 179 | __and__ = same('inner_join') 180 | __add__ = same('left_join') 181 | __sub__ = same('right_join') 182 | __or__ = same('full_join') 183 | __mul__ = same('cross_join') 184 | 185 | 186 | @compile.when(Table) 187 | def compile_table(compile, expr, state): 188 | compile(expr._name, state) 189 | 190 | 191 | @factory.register 192 | class TableAlias(Table): 193 | 194 | __slots__ = ('_table',) 195 | 196 | def __init__(self, table, name, fields=()): 197 | # We neet to have possibility accept fields in constructor because the "table" arg can be a query. 198 | if isinstance(table, string_types): 199 | warn('TableAlias(name, table, fields)', 'TableAlias(table, name, fields)') 200 | table, name = name, table 201 | Table.__init__(self, name, fields) 202 | self._table = table 203 | if not fields and isinstance(table, Table): 204 | for f in table._fields.values(): 205 | self._append_field(copy.copy(f)) 206 | 207 | def as_(self, alias): 208 | return type(self)(alias, self._table) 209 | 210 | 211 | @compile.when(TableAlias) 212 | def compile_tablealias(compile, expr, state): 213 | if expr._table is not None and state.context == CONTEXT.TABLE: 214 | compile(expr._table, state) 215 | state.sql.append(' AS ') 216 | compile(expr._name, state) 217 | 218 | 219 | @factory.register 220 | class TableJoin(object): 221 | 222 | __slots__ = ('_table', '_join_type', '_on', '_left', '_hint', '_nested', '_natural', '_using', '__factory__') 223 | 224 | # TODO: support for ONLY http://www.postgresql.org/docs/9.4/static/tutorial-inheritance.html 225 | 226 | def __init__(self, table_or_alias, join_type=None, on=None, left=None): 227 | self._table = table_or_alias 228 | self._join_type = join_type 229 | self._on = on 230 | self._left = left 231 | self._hint = None 232 | self._nested = False 233 | self._natural = False 234 | self._using = None 235 | 236 | def inner_join(self, right): 237 | return self.join("INNER JOIN", right) 238 | 239 | def left_join(self, right): 240 | return self.join("LEFT OUTER JOIN", right) 241 | 242 | def right_join(self, right): 243 | return self.join("RIGHT OUTER JOIN", right) 244 | 245 | def full_join(self, right): 246 | return self.join("FULL OUTER JOIN", right) 247 | 248 | def cross_join(self, right): 249 | return self.join("CROSS JOIN", right) 250 | 251 | def join(self, join_type, right): 252 | if not isinstance(right, TableJoin) or right.left(): 253 | right = type(self)(right, left=self) 254 | else: 255 | right = copy.copy(right) 256 | right = right.left(self).join_type(join_type) 257 | return right 258 | 259 | def left(self, left=None): 260 | if left is None: 261 | return self._left 262 | self._left = left 263 | return self 264 | 265 | def join_type(self, join_type): 266 | self._join_type = join_type 267 | return self 268 | 269 | def on(self, cond): 270 | if self._on is not None: 271 | c = self.__class__(self) # TODO: Test me. 272 | else: 273 | c = self 274 | c._on = cond 275 | return c 276 | 277 | def natural(self): 278 | self._natural = True 279 | return self 280 | 281 | def using(self, *fields): 282 | self._using = ExprList(*fields).join(", ") 283 | return self 284 | 285 | def __call__(self): 286 | self._nested = True 287 | c = self.__class__(self) 288 | return c 289 | 290 | def hint(self, expr): 291 | if isinstance(expr, string_types): 292 | expr = Expr(expr) 293 | self._hint = OmitParentheses(expr) 294 | return self 295 | 296 | def __copy__(self): 297 | dup = copy.copy(super(TableJoin, self)) 298 | for a in ['_hint', ]: 299 | setattr(dup, a, copy.copy(getattr(dup, a, None))) 300 | return dup 301 | 302 | def __repr__(self): 303 | return expr_repr(self) 304 | 305 | as_nested = same('__call__') 306 | group = same('__call__') 307 | __and__ = same('inner_join') 308 | __add__ = same('left_join') 309 | __sub__ = same('right_join') 310 | __or__ = same('full_join') 311 | __mul__ = same('cross_join') 312 | 313 | 314 | @compile.when(TableJoin) 315 | def compile_tablejoin(compile, expr, state): 316 | if expr._nested: 317 | state.sql.append('(') 318 | if expr._left is not None: 319 | compile(expr._left, state) 320 | if expr._join_type: 321 | state.sql.append(SPACE) 322 | if expr._natural: 323 | state.sql.append('NATURAL ') 324 | state.sql.append(expr._join_type) 325 | state.sql.append(SPACE) 326 | state.push('context', CONTEXT.TABLE) 327 | compile(expr._table, state) 328 | state.pop() 329 | if expr._on is not None: 330 | state.sql.append(' ON ') 331 | state.push("context", CONTEXT.EXPR) 332 | compile(expr._on, state) 333 | state.pop() 334 | elif expr._using is not None: 335 | state.sql.append(' USING ') 336 | compile(expr._using, state) 337 | if expr._hint is not None: 338 | state.sql.append(SPACE) 339 | compile(expr._hint, state) 340 | if expr._nested: 341 | state.sql.append(')') 342 | 343 | 344 | # Model based table 345 | 346 | class NamedJoin(TableJoin): 347 | __slots__ = () 348 | 349 | def __init__(self, left, right, on=None): 350 | self._table = right 351 | self._on = on 352 | self._left = left 353 | self._hint = None 354 | self._nested = False 355 | self._natural = False 356 | self._using = None 357 | 358 | 359 | class Join(NamedJoin): 360 | __slots__ = () 361 | _join_type = "JOIN" 362 | 363 | 364 | class InnerJoin(NamedJoin): 365 | __slots__ = () 366 | _join_type = "INNER JOIN" 367 | 368 | 369 | class LeftJoin(NamedJoin): 370 | __slots__ = () 371 | _join_type = "LEFT OUTER JOIN" 372 | 373 | 374 | class RightJoin(NamedJoin): 375 | __slots__ = () 376 | _join_type = "RIGHT OUTER JOIN" 377 | 378 | 379 | class FullJoin(NamedJoin): 380 | __slots__ = () 381 | _join_type = "FULL OUTER JOIN" 382 | 383 | 384 | class CrossJoin(NamedJoin): 385 | __slots__ = () 386 | _join_type = "CROSS JOIN" 387 | 388 | 389 | class ModelRegistry(dict): 390 | def __setitem__(self, key, value): 391 | super(ModelRegistry, self).__setitem__(key, Name(value)) 392 | 393 | def register(self, table_name): 394 | def _inner(cls): 395 | self[cls] = table_name 396 | return cls 397 | return _inner 398 | 399 | 400 | @compile.when(type) 401 | def compile_type(compile, model, state): 402 | """ Any class can be used as Table """ 403 | compile(model_registry[model], state) 404 | 405 | 406 | model_registry = ModelRegistry() 407 | -------------------------------------------------------------------------------- /sqlbuilder/smartsql/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacsway/sqlbuilder/72f32bbbfc1116550343c471dc43ef6284492a5a/sqlbuilder/smartsql/tests/__init__.py -------------------------------------------------------------------------------- /sqlbuilder/smartsql/tests/base.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | __all__ = ('TestCase', ) 4 | 5 | class TestCase(unittest.TestCase): 6 | 7 | maxDiff = None 8 | -------------------------------------------------------------------------------- /sqlbuilder/smartsql/tests/dialects/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacsway/sqlbuilder/72f32bbbfc1116550343c471dc43ef6284492a5a/sqlbuilder/smartsql/tests/dialects/__init__.py -------------------------------------------------------------------------------- /sqlbuilder/smartsql/tests/dialects/test_mongodb.py: -------------------------------------------------------------------------------- 1 | from sqlbuilder import smartsql 2 | from sqlbuilder.smartsql.dialects import mongodb 3 | from sqlbuilder.smartsql.tests.base import TestCase 4 | 5 | 6 | class TestField(TestCase): 7 | 8 | def test_field(self): 9 | pass 10 | 11 | 12 | class TestBinary(TestCase): 13 | 14 | def test_eq(self): 15 | self.assertDictEqual( 16 | mongodb.compile(smartsql.T.author.name == 'Ivan'), 17 | {'name': {'$eq': 'Ivan'}} 18 | ) 19 | 20 | def test_ne(self): 21 | self.assertDictEqual( 22 | mongodb.compile(smartsql.T.author.name != 'Ivan'), 23 | {'name': {'$ne': 'Ivan'}} 24 | ) 25 | 26 | def test_gt(self): 27 | self.assertDictEqual( 28 | mongodb.compile(smartsql.T.author.age > 30), 29 | {'age': {'$gt': 30}} 30 | ) 31 | 32 | def test_lt(self): 33 | self.assertDictEqual( 34 | mongodb.compile(smartsql.T.author.age < 30), 35 | {'age': {'$lt': 30}} 36 | ) 37 | 38 | def test_le(self): 39 | self.assertDictEqual( 40 | mongodb.compile(smartsql.T.author.age <= 30), 41 | {'age': {'$lte': 30}} 42 | ) 43 | -------------------------------------------------------------------------------- /sqlbuilder/smartsql/tests/dialects/test_python.py: -------------------------------------------------------------------------------- 1 | import collections 2 | from sqlbuilder import smartsql 3 | from sqlbuilder.smartsql import pycompat 4 | from sqlbuilder.smartsql.dialects import python 5 | from sqlbuilder.smartsql.tests.base import TestCase 6 | 7 | 8 | class Field(object): 9 | 10 | def __init__(self, name, column): 11 | self.name = name 12 | self.column = column 13 | 14 | def get_value(self, obj): 15 | return getattr(obj, self.name, None) 16 | 17 | def set_value(self, obj, value): 18 | return setattr(obj, self.name, value) 19 | 20 | 21 | class Author(object): 22 | def __init__(self, first_name, last_name): 23 | self.first_name = first_name 24 | self.last_name = last_name 25 | 26 | 27 | class Mapper(object): 28 | db_table = None 29 | sql_table = None 30 | model = Author 31 | fields = collections.OrderedDict() 32 | 33 | def __init__(self, model, db_table, fields): 34 | self.model = model 35 | self.db_table = db_table 36 | self.sql_table = smartsql.Table(db_table) 37 | self.fields = collections.OrderedDict() 38 | for field in fields: 39 | self.fields[field.name] = field 40 | 41 | @property 42 | def query(self): 43 | return smartsql.Query( 44 | self.sql_table 45 | ).fields( 46 | self.get_sql_fields() 47 | ) 48 | 49 | def get_sql_fields(self, prefix=None): 50 | if prefix is None: 51 | prefix = self.sql_table 52 | elif isinstance(prefix, pycompat.string_types): 53 | prefix = smartsql.Table(prefix) 54 | return [prefix.get_field(f.name) for f in self.fields.values()] 55 | 56 | def get_sql_values(self, obj): 57 | row = {} 58 | for field in self.fields.values(): 59 | key = python.execute.get_row_key(self.sql_table.get_field(field.column)) 60 | row[key] = field.get_value(obj) 61 | return row 62 | 63 | 64 | author_mapper = Mapper(Author, 'author', ( 65 | Field('first_name', 'first_name'), 66 | Field('last_name', 'last_name') 67 | )) 68 | 69 | 70 | class TestMapper(TestCase): 71 | 72 | def test_mapper(self): 73 | obj = Author('Ivan', 'Zakrevsky') 74 | data = author_mapper.get_sql_values(obj) 75 | self.assertEqual(data['"author"."first_name"'], 'Ivan') 76 | self.assertEqual(data['"author"."last_name"'], 'Zakrevsky') 77 | 78 | 79 | class TestField(TestCase): 80 | 81 | def test_field(self): 82 | obj = Author('Ivan', 'Zakrevsky') 83 | state = python.State() 84 | state.row.update(author_mapper.get_sql_values(obj)) 85 | first_name = python.execute(author_mapper.sql_table.f.first_name, state) 86 | self.assertEqual(first_name, 'Ivan') 87 | last_name = python.execute(author_mapper.sql_table.f.last_name, state) 88 | self.assertEqual(last_name, 'Zakrevsky') 89 | 90 | 91 | class TestBinary(TestCase): 92 | 93 | def test_add(self): 94 | self.assertEqual(python.execute(smartsql.Param(2) + 3, python.State()), 5) 95 | self.assertNotEqual(python.execute(smartsql.Param(2) + 3, python.State()), 6) 96 | 97 | def test_sub(self): 98 | self.assertEqual(python.execute(smartsql.Param(5) - 2, python.State()), 3) 99 | self.assertNotEqual(python.execute(smartsql.Param(5) - 2, python.State()), 4) 100 | 101 | def test_mul(self): 102 | self.assertEqual(python.execute(smartsql.Param(3) * 2, python.State()), 6) 103 | self.assertNotEqual(python.execute(smartsql.Param(3) * 2, python.State()), 7) 104 | 105 | def test_div(self): 106 | self.assertAlmostEqual(python.execute(smartsql.Param(5.0) / 2, python.State()), 2.5) 107 | self.assertNotAlmostEquals(python.execute(smartsql.Param(5.0) / 2, python.State()), 3) 108 | 109 | def test_gt(self): 110 | self.assertTrue(python.execute(smartsql.Param(3) > 2, python.State())) 111 | self.assertFalse(python.execute(smartsql.Param(2) > 3, python.State())) 112 | 113 | def test_ge(self): 114 | self.assertTrue(python.execute(smartsql.Param(3) >= 2, python.State())) 115 | self.assertTrue(python.execute(smartsql.Param(3) >= 3, python.State())) 116 | self.assertFalse(python.execute(smartsql.Param(2) >= 3, python.State())) 117 | 118 | def test_lt(self): 119 | self.assertTrue(python.execute(smartsql.Param(2) < 3, python.State())) 120 | self.assertFalse(python.execute(smartsql.Param(3) < 2, python.State())) 121 | 122 | def test_le(self): 123 | self.assertTrue(python.execute(smartsql.Param(2) <= 3, python.State())) 124 | self.assertTrue(python.execute(smartsql.Param(2) <= 3, python.State())) 125 | self.assertFalse(python.execute(smartsql.Param(3) <= 2, python.State())) 126 | 127 | def test_eq(self): 128 | self.assertTrue(python.execute(smartsql.Param(3) == 3, python.State())) 129 | self.assertFalse(python.execute(smartsql.Param(3) == 2, python.State())) 130 | 131 | def test_ne(self): 132 | self.assertTrue(python.execute(smartsql.Param(3) != 2, python.State())) 133 | self.assertFalse(python.execute(smartsql.Param(3) != 3, python.State())) 134 | 135 | def test_and(self): 136 | self.assertTrue(python.execute(smartsql.Param(True) & True, python.State())) 137 | self.assertFalse(python.execute(smartsql.Param(True) & False, python.State())) 138 | self.assertFalse(python.execute(smartsql.Param(False) & False, python.State())) 139 | 140 | def test_or(self): 141 | self.assertTrue(python.execute(smartsql.Param(True) | False, python.State())) 142 | self.assertTrue(python.execute(smartsql.Param(True) | True, python.State())) 143 | self.assertFalse(python.execute(smartsql.Param(False) | False, python.State())) 144 | 145 | def test_is(self): 146 | obj1 = object() 147 | obj2 = object() 148 | self.assertTrue(python.execute(smartsql.Param(obj1).is_(obj1), python.State())) 149 | self.assertFalse(python.execute(smartsql.Param(obj1).is_(obj2), python.State())) 150 | 151 | def test_is_not(self): 152 | obj1 = object() 153 | obj2 = object() 154 | self.assertTrue(python.execute(smartsql.Param(obj1).is_not(obj2), python.State())) 155 | self.assertFalse(python.execute(smartsql.Param(obj1).is_not(obj1), python.State())) 156 | 157 | def test_lshift(self): 158 | self.assertEqual(python.execute(smartsql.Param(2) << 1, python.State()), 4) 159 | self.assertNotEqual(python.execute(smartsql.Param(2) << 1, python.State()), 5) 160 | 161 | def test_rshift(self): 162 | self.assertEqual(python.execute(smartsql.Param(4) >> 1, python.State()), 2) 163 | self.assertNotEqual(python.execute(smartsql.Param(4) >> 1, python.State()), 3) 164 | -------------------------------------------------------------------------------- /sqlbuilder/smartsql/tests/test_expressions.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from sqlbuilder.smartsql.tests.base import TestCase 3 | from sqlbuilder.smartsql import Q, T, F, P, CompositeExpr, Case, Cast, compile 4 | 5 | __all__ = ('TestExpr', 'TestCaseExpr', 'TestCallable', 'TestCompositeExpr', ) 6 | 7 | 8 | class TestExpr(TestCase): 9 | 10 | def test_expr(self): 11 | tb = T.author 12 | self.assertEqual( 13 | compile(tb.name == 'Tom'), 14 | (('"author"."name" = %s'), ['Tom']) 15 | ) 16 | self.assertEqual( 17 | compile(tb.name != 'Tom'), 18 | (('"author"."name" <> %s'), ['Tom']) 19 | ) 20 | self.assertEqual( 21 | compile(tb.counter + 1), 22 | ('"author"."counter" + %s', [1]) 23 | ) 24 | self.assertEqual( 25 | compile(1 + tb.counter), 26 | ('%s + "author"."counter"', [1]) 27 | ) 28 | self.assertEqual( 29 | compile(tb.counter - 1), 30 | ('"author"."counter" - %s', [1]) 31 | ) 32 | self.assertEqual( 33 | compile(10 - tb.counter), 34 | ('%s - "author"."counter"', [10]) 35 | ) 36 | self.assertEqual( 37 | compile(tb.counter * 2), 38 | ('"author"."counter" * %s', [2]) 39 | ) 40 | self.assertEqual( 41 | compile(2 * tb.counter), 42 | ('%s * "author"."counter"', [2]) 43 | ) 44 | self.assertEqual( 45 | compile(tb.counter / 2), 46 | ('"author"."counter" / %s', [2]) 47 | ) 48 | self.assertEqual( 49 | compile(10 / tb.counter), 50 | ('%s / "author"."counter"', [10]) 51 | ) 52 | self.assertEqual( 53 | compile(tb.is_staff & tb.is_admin), 54 | ('"author"."is_staff" AND "author"."is_admin"', []) 55 | ) 56 | self.assertEqual( 57 | compile(tb.is_staff | tb.is_admin), 58 | ('"author"."is_staff" OR "author"."is_admin"', []) 59 | ) 60 | self.assertEqual( 61 | compile(tb.counter > 10), 62 | ('"author"."counter" > %s', [10]) 63 | ) 64 | self.assertEqual( 65 | compile(10 > tb.counter), 66 | ('"author"."counter" < %s', [10]) 67 | ) 68 | self.assertEqual( 69 | compile(tb.counter >= 10), 70 | ('"author"."counter" >= %s', [10]) 71 | ) 72 | self.assertEqual( 73 | compile(10 >= tb.counter), 74 | ('"author"."counter" <= %s', [10]) 75 | ) 76 | self.assertEqual( 77 | compile(tb.counter < 10), 78 | ('"author"."counter" < %s', [10]) 79 | ) 80 | self.assertEqual( 81 | compile(10 < tb.counter), 82 | ('"author"."counter" > %s', [10]) 83 | ) 84 | self.assertEqual( 85 | compile(tb.counter <= 10), 86 | ('"author"."counter" <= %s', [10]) 87 | ) 88 | self.assertEqual( 89 | compile(10 <= tb.counter), 90 | ('"author"."counter" >= %s', [10]) 91 | ) 92 | self.assertEqual( 93 | compile(tb.mask << 1), 94 | ('"author"."mask" << %s', [1]) 95 | ) 96 | self.assertEqual( 97 | compile(tb.mask >> 1), 98 | ('"author"."mask" >> %s', [1]) 99 | ) 100 | self.assertEqual( 101 | compile(tb.is_staff.is_(True)), 102 | ('"author"."is_staff" IS %s', [True]) 103 | ) 104 | self.assertEqual( 105 | compile(tb.is_staff.is_not(True)), 106 | ('"author"."is_staff" IS NOT %s', [True]) 107 | ) 108 | self.assertEqual( 109 | compile(tb.status.in_(('new', 'approved'))), 110 | ('"author"."status" IN (%s, %s)', ['new', 'approved']) 111 | ) 112 | self.assertEqual( 113 | compile(tb.status.not_in(('new', 'approved'))), 114 | ('"author"."status" NOT IN (%s, %s)', ['new', 'approved']) 115 | ) 116 | self.assertEqual( 117 | compile(tb.last_name.like('mi')), 118 | ('"author"."last_name" LIKE %s', ['mi']) 119 | ) 120 | self.assertEqual( 121 | compile(tb.last_name.ilike('mi')), 122 | ('"author"."last_name" ILIKE %s', ['mi']) 123 | ) 124 | self.assertEqual( 125 | compile(P('mi').like(tb.last_name)), 126 | ('%s LIKE "author"."last_name"', ['mi']) 127 | ) 128 | self.assertEqual( 129 | compile(tb.last_name.rlike('mi')), 130 | ('%s LIKE "author"."last_name"', ['mi']) 131 | ) 132 | self.assertEqual( 133 | compile(tb.last_name.rilike('mi')), 134 | ('%s ILIKE "author"."last_name"', ['mi']) 135 | ) 136 | self.assertEqual( 137 | compile(tb.last_name.startswith('Sm')), 138 | ('"author"."last_name" LIKE REPLACE(REPLACE(REPLACE(%s, \'!\', \'!!\'), \'_\', \'!_\'), \'%%\', \'!%%\') || \'%%\' ESCAPE \'!\'', ['Sm']) 139 | ) 140 | self.assertEqual( 141 | compile(tb.last_name.istartswith('Sm')), 142 | ('"author"."last_name" ILIKE REPLACE(REPLACE(REPLACE(%s, \'!\', \'!!\'), \'_\', \'!_\'), \'%%\', \'!%%\') || \'%%\' ESCAPE \'!\'', ['Sm']) 143 | ) 144 | self.assertEqual( 145 | compile(tb.last_name.contains('mi')), 146 | ('"author"."last_name" LIKE \'%%\' || REPLACE(REPLACE(REPLACE(%s, \'!\', \'!!\'), \'_\', \'!_\'), \'%%\', \'!%%\') || \'%%\' ESCAPE \'!\'', ['mi']) 147 | ) 148 | self.assertEqual( 149 | compile(tb.last_name.icontains('mi')), 150 | ('"author"."last_name" ILIKE \'%%\' || REPLACE(REPLACE(REPLACE(%s, \'!\', \'!!\'), \'_\', \'!_\'), \'%%\', \'!%%\') || \'%%\' ESCAPE \'!\'', ['mi']) 151 | ) 152 | self.assertEqual( 153 | compile(tb.last_name.endswith('th')), 154 | ('"author"."last_name" LIKE \'%%\' || REPLACE(REPLACE(REPLACE(%s, \'!\', \'!!\'), \'_\', \'!_\'), \'%%\', \'!%%\') ESCAPE \'!\'', ['th']) 155 | ) 156 | self.assertEqual( 157 | compile(tb.last_name.iendswith('th')), 158 | ('"author"."last_name" ILIKE \'%%\' || REPLACE(REPLACE(REPLACE(%s, \'!\', \'!!\'), \'_\', \'!_\'), \'%%\', \'!%%\') ESCAPE \'!\'', ['th']) 159 | ) 160 | 161 | self.assertEqual( 162 | compile(tb.last_name.rstartswith('Sm')), 163 | ('%s LIKE REPLACE(REPLACE(REPLACE("author"."last_name", \'!\', \'!!\'), \'_\', \'!_\'), \'%%\', \'!%%\') || \'%%\' ESCAPE \'!\'', ['Sm']) 164 | ) 165 | self.assertEqual( 166 | compile(tb.last_name.ristartswith('Sm')), 167 | ('%s ILIKE REPLACE(REPLACE(REPLACE("author"."last_name", \'!\', \'!!\'), \'_\', \'!_\'), \'%%\', \'!%%\') || \'%%\' ESCAPE \'!\'', ['Sm']) 168 | ) 169 | self.assertEqual( 170 | compile(tb.last_name.rcontains('mi')), 171 | ('%s LIKE \'%%\' || REPLACE(REPLACE(REPLACE("author"."last_name", \'!\', \'!!\'), \'_\', \'!_\'), \'%%\', \'!%%\') || \'%%\' ESCAPE \'!\'', ['mi']) 172 | ) 173 | self.assertEqual( 174 | compile(tb.last_name.ricontains('mi')), 175 | ('%s ILIKE \'%%\' || REPLACE(REPLACE(REPLACE("author"."last_name", \'!\', \'!!\'), \'_\', \'!_\'), \'%%\', \'!%%\') || \'%%\' ESCAPE \'!\'', ['mi']) 176 | ) 177 | self.assertEqual( 178 | compile(tb.last_name.rendswith('th')), 179 | ('%s LIKE \'%%\' || REPLACE(REPLACE(REPLACE("author"."last_name", \'!\', \'!!\'), \'_\', \'!_\'), \'%%\', \'!%%\') ESCAPE \'!\'', ['th']) 180 | ) 181 | self.assertEqual( 182 | compile(tb.last_name.riendswith('th')), 183 | ('%s ILIKE \'%%\' || REPLACE(REPLACE(REPLACE("author"."last_name", \'!\', \'!!\'), \'_\', \'!_\'), \'%%\', \'!%%\') ESCAPE \'!\'', ['th']) 184 | ) 185 | self.assertEqual( 186 | compile(+tb.counter), 187 | ('+"author"."counter"', []) 188 | ) 189 | self.assertEqual( 190 | compile(-tb.counter), 191 | ('-"author"."counter"', []) 192 | ) 193 | self.assertEqual( 194 | compile(~tb.counter), 195 | ('NOT "author"."counter"', []) 196 | ) 197 | self.assertEqual( 198 | compile(tb.name.distinct()), 199 | ('DISTINCT "author"."name"', []) 200 | ) 201 | self.assertEqual( 202 | compile(tb.counter ** 2), 203 | ('POWER("author"."counter", %s)', [2]) 204 | ) 205 | self.assertEqual( 206 | compile(2 ** tb.counter), 207 | ('POWER(%s, "author"."counter")', [2]) 208 | ) 209 | self.assertEqual( 210 | compile(tb.counter % 2), 211 | ('MOD("author"."counter", %s)', [2]) 212 | ) 213 | self.assertEqual( 214 | compile(2 % tb.counter), 215 | ('MOD(%s, "author"."counter")', [2]) 216 | ) 217 | self.assertEqual( 218 | compile(abs(tb.counter)), 219 | ('ABS("author"."counter")', []) 220 | ) 221 | self.assertEqual( 222 | compile(tb.counter.count()), 223 | ('COUNT("author"."counter")', []) 224 | ) 225 | self.assertEqual( 226 | compile(tb.age.between(20, 30)), 227 | ('"author"."age" BETWEEN %s AND %s', [20, 30]) 228 | ) 229 | self.assertEqual( 230 | compile(tb.age[20:30]), 231 | ('"author"."age" BETWEEN %s AND %s', [20, 30]) 232 | ) 233 | self.assertEqual( 234 | compile(T.tb.cl[T.tb.cl2:T.tb.cl3]), 235 | ('"tb"."cl" BETWEEN "tb"."cl2" AND "tb"."cl3"', []) 236 | ) 237 | self.assertEqual( 238 | compile(tb.age[20]), 239 | ('"author"."age" = %s', [20]) 240 | ) 241 | self.assertEqual( 242 | compile(tb.name.concat(' staff', ' admin')), 243 | ('"author"."name" || %s || %s', [' staff', ' admin']) 244 | ) 245 | self.assertEqual( 246 | compile(tb.name.concat_ws(' ', 'staff', 'admin')), 247 | ('concat_ws(%s, "author"."name", %s, %s)', [' ', 'staff', 'admin']) 248 | ) 249 | self.assertEqual( 250 | compile(tb.name.op('MY_EXTRA_OPERATOR')(10)), 251 | ('"author"."name" MY_EXTRA_OPERATOR %s', [10]) 252 | ) 253 | self.assertEqual( 254 | compile(tb.name.rop('MY_EXTRA_OPERATOR')(10)), 255 | ('%s MY_EXTRA_OPERATOR "author"."name"', [10]) 256 | ) 257 | self.assertEqual( 258 | compile(tb.name.asc()), 259 | ('"author"."name" ASC', []) 260 | ) 261 | self.assertEqual( 262 | compile(tb.name.desc()), 263 | ('"author"."name" DESC', []) 264 | ) 265 | self.assertEqual( 266 | compile(((tb.age > 25) | (tb.answers > 10)) & (tb.is_staff | tb.is_admin)), 267 | ('("author"."age" > %s OR "author"."answers" > %s) AND ("author"."is_staff" OR "author"."is_admin")', [25, 10]) 268 | ) 269 | self.assertEqual( 270 | compile((T.author.first_name != 'Tom') & (T.author.last_name.in_(('Smith', 'Johnson')))), 271 | ('"author"."first_name" <> %s AND "author"."last_name" IN (%s, %s)', ['Tom', 'Smith', 'Johnson']) 272 | ) 273 | self.assertEqual( 274 | compile((T.author.first_name != 'Tom') | (T.author.last_name.in_(('Smith', 'Johnson')))), 275 | ('"author"."first_name" <> %s OR "author"."last_name" IN (%s, %s)', ['Tom', 'Smith', 'Johnson']) 276 | ) 277 | 278 | 279 | class TestCaseExpr(TestCase): 280 | 281 | def test_case(self): 282 | self.assertEqual( 283 | compile(Case([ 284 | (F.a == 1, 'one'), 285 | (F.b == 2, 'two'), 286 | ])), 287 | ('CASE WHEN ("a" = %s) THEN %s WHEN ("b" = %s) THEN %s END ', [1, 'one', 2, 'two']) 288 | ) 289 | 290 | def test_case_with_default(self): 291 | self.assertEqual( 292 | compile(Case([ 293 | (F.a == 1, 'one'), 294 | (F.b == 2, 'two'), 295 | ], default='other')), 296 | ('CASE WHEN ("a" = %s) THEN %s WHEN ("b" = %s) THEN %s ELSE %s END ', [1, 'one', 2, 'two', 'other']) 297 | ) 298 | 299 | def test_case_with_expr(self): 300 | self.assertEqual( 301 | compile(Case([ 302 | (1, 'one'), 303 | (2, 'two'), 304 | ], F.a)), 305 | ('CASE "a" WHEN %s THEN %s WHEN %s THEN %s END ', [1, 'one', 2, 'two']) 306 | ) 307 | 308 | def test_case_with_expr_and_default(self): 309 | self.assertEqual( 310 | compile(Case([ 311 | (1, 'one'), 312 | (2, 'two'), 313 | ], F.a, 'other')), 314 | ('CASE "a" WHEN %s THEN %s WHEN %s THEN %s ELSE %s END ', [1, 'one', 2, 'two', 'other']) 315 | ) 316 | 317 | def test_case_in_query(self): 318 | self.assertEqual( 319 | compile(Q().tables(T.t1).fields('*').where(F.c == Case([ 320 | (F.a == 1, 'one'), 321 | (F.b == 2, 'two'), 322 | ], default='other'))), 323 | ('SELECT * FROM "t1" WHERE "c" = CASE WHEN ("a" = %s) THEN %s WHEN ("b" = %s) THEN %s ELSE %s END ', [1, 'one', 2, 'two', 'other']) 324 | ) 325 | 326 | 327 | class TestCallable(TestCase): 328 | 329 | def test_case(self): 330 | self.assertEqual( 331 | compile(Cast(F.field_name, 'text')), 332 | ('CAST("field_name" AS text)', []) 333 | ) 334 | 335 | 336 | class TestCompositeExpr(TestCase): 337 | 338 | def test_compositeexpr(self): 339 | pk = CompositeExpr(T.tb.obj_id, T.tb.land_id, T.tb.date) 340 | today = datetime.datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) 341 | self.assertEqual( 342 | Q(T.tb).fields(pk, T.tb.title).where(pk == (1, 'en', today)).select(), 343 | ('SELECT "tb"."obj_id", "tb"."land_id", "tb"."date", "tb"."title" FROM "tb" WHERE "tb"."obj_id" = %s AND "tb"."land_id" = %s AND "tb"."date" = %s', [1, 'en', today]) 344 | ) 345 | self.assertEqual( 346 | Q(T.tb).fields(pk, T.tb.title).where(pk != (1, 'en', today)).select(), 347 | ('SELECT "tb"."obj_id", "tb"."land_id", "tb"."date", "tb"."title" FROM "tb" WHERE "tb"."obj_id" <> %s AND "tb"."land_id" <> %s AND "tb"."date" <> %s', [1, 'en', today]) 348 | ) 349 | self.assertEqual( 350 | Q(T.tb).fields(pk, T.tb.title).where(pk.in_(((1, 'en', today), (2, 'en', today)))).select(), 351 | ('SELECT "tb"."obj_id", "tb"."land_id", "tb"."date", "tb"."title" FROM "tb" WHERE "tb"."obj_id" = %s AND "tb"."land_id" = %s AND "tb"."date" = %s OR "tb"."obj_id" = %s AND "tb"."land_id" = %s AND "tb"."date" = %s', [1, 'en', today, 2, 'en', today]) 352 | ) 353 | 354 | self.assertEqual( 355 | Q(T.tb).fields(pk, T.tb.title).where(pk.not_in(((1, 'en', today), (2, 'en', today)))).select(), 356 | ('SELECT "tb"."obj_id", "tb"."land_id", "tb"."date", "tb"."title" FROM "tb" WHERE NOT ("tb"."obj_id" = %s AND "tb"."land_id" = %s AND "tb"."date" = %s OR "tb"."obj_id" = %s AND "tb"."land_id" = %s AND "tb"."date" = %s)', [1, 'en', today, 2, 'en', today]) 357 | ) 358 | 359 | def test_compositeexpr_as_alias(self): 360 | pk = CompositeExpr(T.tb.obj_id, T.tb.land_id, T.tb.date).as_(('al1', 'al2', 'al3')) 361 | today = datetime.datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) 362 | self.assertEqual( 363 | Q(T.tb).fields(pk, T.tb.title).where(pk == (1, 'en', today)).select(), 364 | ('SELECT "tb"."obj_id" AS "al1", "tb"."land_id" AS "al2", "tb"."date" AS "al3", "tb"."title" FROM "tb" WHERE "al1" = %s AND "al2" = %s AND "al3" = %s', [1, 'en', today]) 365 | ) 366 | self.assertEqual( 367 | Q(T.tb).fields(pk, T.tb.title).where(pk != (1, 'en', today)).select(), 368 | ('SELECT "tb"."obj_id" AS "al1", "tb"."land_id" AS "al2", "tb"."date" AS "al3", "tb"."title" FROM "tb" WHERE "al1" <> %s AND "al2" <> %s AND "al3" <> %s', [1, 'en', today]) 369 | ) 370 | self.assertEqual( 371 | Q(T.tb).fields(pk, T.tb.title).where(pk.in_(((1, 'en', today), (2, 'en', today)))).select(), 372 | ('SELECT "tb"."obj_id" AS "al1", "tb"."land_id" AS "al2", "tb"."date" AS "al3", "tb"."title" FROM "tb" WHERE "al1" = %s AND "al2" = %s AND "al3" = %s OR "al1" = %s AND "al2" = %s AND "al3" = %s', [1, 'en', today, 2, 'en', today]) 373 | ) 374 | 375 | self.assertEqual( 376 | Q(T.tb).fields(pk, T.tb.title).where(pk.not_in(((1, 'en', today), (2, 'en', today)))).select(), 377 | ('SELECT "tb"."obj_id" AS "al1", "tb"."land_id" AS "al2", "tb"."date" AS "al3", "tb"."title" FROM "tb" WHERE NOT ("al1" = %s AND "al2" = %s AND "al3" = %s OR "al1" = %s AND "al2" = %s AND "al3" = %s)', [1, 'en', today, 2, 'en', today]) 378 | ) 379 | -------------------------------------------------------------------------------- /sqlbuilder/smartsql/tests/test_fields.py: -------------------------------------------------------------------------------- 1 | from sqlbuilder.smartsql.tests.base import TestCase 2 | from sqlbuilder.smartsql import Q, T, F, Field, A, compile 3 | 4 | __all__ = ('TestField', ) 5 | 6 | 7 | class TestField(TestCase): 8 | 9 | def test_field(self): 10 | 11 | # Get field as table attribute 12 | self.assertEqual( 13 | type(T.book.title), 14 | Field 15 | ) 16 | self.assertEqual( 17 | compile(T.book.title), 18 | ('"book"."title"', []) 19 | ) 20 | 21 | self.assertEqual( 22 | type(T.book.title.as_('a')), 23 | A 24 | ) 25 | self.assertEqual( 26 | compile(T.book.title.as_('a')), 27 | ('"a"', []) 28 | ) 29 | 30 | self.assertEqual( 31 | type(T.book.title__a), 32 | A 33 | ) 34 | self.assertEqual( 35 | compile(T.book.title__a), 36 | ('"a"', []) 37 | ) 38 | 39 | # Get field as class F attribute (Legacy) 40 | self.assertEqual( 41 | type(F.book__title), 42 | Field 43 | ) 44 | self.assertEqual( 45 | compile(F.book__title), 46 | ('"book"."title"', []) 47 | ) 48 | 49 | self.assertEqual( 50 | type(F.book__title.as_('a')), 51 | A 52 | ) 53 | self.assertEqual( 54 | compile(F.book__title.as_('a')), 55 | ('"a"', []) 56 | ) 57 | 58 | self.assertEqual( 59 | type(F.book__title__a), 60 | A 61 | ) 62 | self.assertEqual( 63 | compile(F.book__title__a), 64 | ('"a"', []) 65 | ) 66 | 67 | # Test with context 68 | al = T.book.status.as_('a') 69 | self.assertEqual( 70 | compile(Q().tables(T.book).fields(T.book.id, al).where(al.in_(('new', 'approved')))), 71 | ('SELECT "book"."id", "book"."status" AS "a" FROM "book" WHERE "a" IN (%s, %s)', ['new', 'approved']) 72 | ) 73 | -------------------------------------------------------------------------------- /sqlbuilder/smartsql/tests/test_legacy.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import datetime 3 | from sqlbuilder.smartsql.tests.base import TestCase 4 | 5 | from sqlbuilder.smartsql import PLACEHOLDER, Q, T, F, A, E, Not, func, const, Result 6 | from sqlbuilder.smartsql.dialects.mysql import compile as mysql_compile 7 | 8 | __all__ = ('TestSmartSQLLegacy',) 9 | 10 | 11 | class TestSmartSQLLegacy(TestCase): 12 | 13 | def test_prefix(self): 14 | self.assertEqual( 15 | Q(T.tb).where(~(T.tb.cl == 3)).select('*'), 16 | ('SELECT * FROM "tb" WHERE NOT "tb"."cl" = %s', [3, ], ) 17 | ) 18 | self.assertEqual( 19 | Q(T.tb).where(Not(T.tb.cl == 3)).select('*'), 20 | ('SELECT * FROM "tb" WHERE NOT "tb"."cl" = %s', [3, ], ) 21 | ) 22 | 23 | def test_function(self): 24 | self.assertEqual( 25 | Q(T.tb).where(func.FUNC_NAME(T.tb.cl) == 5).select('*'), 26 | ('SELECT * FROM "tb" WHERE FUNC_NAME("tb"."cl") = %s', [5, ], ) 27 | ) 28 | self.assertEqual( 29 | Q(T.tb).where(T.tb.cl == func.RAND()).select('*'), 30 | ('SELECT * FROM "tb" WHERE "tb"."cl" = RAND()', [], ) 31 | ) 32 | 33 | def test_constant(self): 34 | self.assertEqual( 35 | Q(T.tb).where(const.CONST_NAME == 5).select('*'), 36 | ('SELECT * FROM "tb" WHERE CONST_NAME = %s', [5, ], ) 37 | ) 38 | 39 | def test_in(self): 40 | self.assertEqual( 41 | Q(T.tb).where(T.tb.cl == [1, T.tb.cl3, 5, ]).where(T.tb.cl2 == [1, T.tb.cl4, ]).select('*'), 42 | ('SELECT * FROM "tb" WHERE "tb"."cl" IN (%s, "tb"."cl3", %s) AND "tb"."cl2" IN (%s, "tb"."cl4")', [1, 5, 1, ], ) 43 | ) 44 | self.assertEqual( 45 | Q(T.tb).where(T.tb.cl != [1, 3, 5, ]).select('*'), 46 | ('SELECT * FROM "tb" WHERE "tb"."cl" NOT IN (%s, %s, %s)', [1, 3, 5, ], ) 47 | ) 48 | self.assertEqual( 49 | Q(T.tb).where(T.tb.cl.in_([1, 3, 5, ])).select('*'), 50 | ('SELECT * FROM "tb" WHERE "tb"."cl" IN (%s, %s, %s)', [1, 3, 5, ], ) 51 | ) 52 | self.assertEqual( 53 | Q(T.tb).where(T.tb.cl.not_in([1, 3, 5, ])).select('*'), 54 | ('SELECT * FROM "tb" WHERE "tb"."cl" NOT IN (%s, %s, %s)', [1, 3, 5, ], ) 55 | ) 56 | 57 | def test_concat(self): 58 | self.assertEqual( 59 | Q(T.tb).where(T.tb.cl.concat(1, 2, 'str', T.tb.cl2) != 'str2').select('*'), 60 | ('SELECT * FROM "tb" WHERE "tb"."cl" || %s || %s || %s || "tb"."cl2" <> %s', [1, 2, 'str', 'str2'], ) 61 | ) 62 | self.assertEqual( 63 | Q(T.tb).where(T.tb.cl.concat_ws(' + ', 1, 2, 'str', T.tb.cl2) != 'str2').select('*'), 64 | ('SELECT * FROM "tb" WHERE concat_ws(%s, "tb"."cl", %s, %s, %s, "tb"."cl2") <> %s', [' + ', 1, 2, 'str', 'str2'], ) 65 | ) 66 | self.assertEqual( 67 | Q(T.tb, result=Result(compile=mysql_compile)).where(T.tb.cl.concat(1, 2, 'str', T.tb.cl2) != 'str2').select('*'), 68 | ('SELECT * FROM `tb` WHERE CONCAT(`tb`.`cl`, %s, %s, %s, `tb`.`cl2`) <> %s', [1, 2, 'str', 'str2'], ) 69 | ) 70 | self.assertEqual( 71 | Q(T.tb, result=Result(compile=mysql_compile)).where(T.tb.cl.concat_ws(' + ', 1, 2, 'str', T.tb.cl2) != 'str2').select('*'), 72 | ('SELECT * FROM `tb` WHERE CONCAT_WS(%s, `tb`.`cl`, %s, %s, %s, `tb`.`cl2`) <> %s', [' + ', 1, 2, 'str', 'str2'], ) 73 | ) 74 | 75 | def test_alias(self): 76 | self.assertEqual( 77 | Q(T.tb).where(F.tb__cl__al == 5).select(F.tb__cl__al), 78 | ('SELECT "tb"."cl" AS "al" FROM "tb" WHERE "al" = %s', [5, ], ) 79 | ) 80 | self.assertEqual( 81 | Q(T.tb).where(T.tb.cl__al == 5).select(T.tb.cl__al), 82 | ('SELECT "tb"."cl" AS "al" FROM "tb" WHERE "al" = %s', [5, ], ) 83 | ) 84 | self.assertEqual( 85 | Q(T.tb).where(T.tb.cl.as_('al') == 5).select(T.tb.cl.as_('al')), 86 | ('SELECT "tb"."cl" AS "al" FROM "tb" WHERE "al" = %s', [5, ], ) 87 | ) 88 | 89 | def test_complex(self): 90 | self.assertEqual( 91 | Q((T.base + T.grade).on((T.base.type == T.grade.item_type) & (F.base__type == 1)) + T.lottery).on( 92 | F.base__type == F.lottery__item_type 93 | ).where( 94 | (F.name == "name") & (F.status == 0) | (F.name == None) 95 | ).group_by(T.base.type).having(E("count(*)") > 1).select(F.type, F.grade__grade, F.lottery__grade), 96 | ('SELECT "type", "grade"."grade", "lottery"."grade" FROM "base" LEFT OUTER JOIN "grade" ON ("base"."type" = "grade"."item_type" AND "base"."type" = %s) LEFT OUTER JOIN "lottery" ON ("base"."type" = "lottery"."item_type") WHERE "name" = %s AND "status" = %s OR "name" IS NULL GROUP BY "base"."type" HAVING (count(*)) > %s', [1, 'name', 0, 1, ], ) 97 | ) 98 | t = T.grade 99 | self.assertEqual( 100 | Q(t).limit(0, 100).select(F.name), 101 | ('SELECT "name" FROM "grade" LIMIT %s', [100], ) 102 | ) 103 | t = (t & T.base).on(F.grade__item_type == F.base__type) 104 | self.assertEqual( 105 | Q(t).order_by(F.grade__name, F.base__name, desc=True).select(F.grade__name, F.base__img), 106 | ('SELECT "grade"."name", "base"."img" FROM "grade" INNER JOIN "base" ON ("grade"."item_type" = "base"."type") ORDER BY "grade"."name" DESC, "base"."name" DESC', [], ) 107 | ) 108 | t = (t + T.lottery).on(F.base__type == F.lottery__item_type) 109 | self.assertEqual( 110 | Q(t).group_by(F.grade__grade).having(F.grade__grade > 0).select(F.grade__name, F.base__img, F.lottery__price), 111 | ('SELECT "grade"."name", "base"."img", "lottery"."price" FROM "grade" INNER JOIN "base" ON ("grade"."item_type" = "base"."type") LEFT OUTER JOIN "lottery" ON ("base"."type" = "lottery"."item_type") GROUP BY "grade"."grade" HAVING "grade"."grade" > %s', [0, ], ) 112 | ) 113 | w = (F.base__type == 1) 114 | self.assertEqual( 115 | Q(t).where(w).select(F.grade__name, for_update=True), 116 | ('SELECT "grade"."name" FROM "grade" INNER JOIN "base" ON ("grade"."item_type" = "base"."type") LEFT OUTER JOIN "lottery" ON ("base"."type" = "lottery"."item_type") WHERE "base"."type" = %s FOR UPDATE', [1, ], ) 117 | ) 118 | w = w & (F.grade__status == [0, 1]) 119 | now = datetime.datetime.now() 120 | w = w | (F.lottery__add_time > "2009-01-01") & (F.lottery__add_time <= now) 121 | self.assertEqual( 122 | Q(t).where(w).limit(1).select(F.grade__name, F.base__img, F.lottery__price), 123 | ('SELECT "grade"."name", "base"."img", "lottery"."price" FROM "grade" INNER JOIN "base" ON ("grade"."item_type" = "base"."type") LEFT OUTER JOIN "lottery" ON ("base"."type" = "lottery"."item_type") WHERE "base"."type" = %s AND "grade"."status" IN (%s, %s) OR "lottery"."add_time" > %s AND "lottery"."add_time" <= %s LIMIT %s', [1, 0, 1, '2009-01-01', now, 1], ) 124 | ) 125 | w = w & (F.base__status != [1, 2]) 126 | self.assertEqual( 127 | Q(t).where(w).select(F.grade__name, F.base__img, F.lottery__price, E("CASE 1 WHEN 1")), 128 | ('SELECT "grade"."name", "base"."img", "lottery"."price", (CASE 1 WHEN 1) FROM "grade" INNER JOIN "base" ON ("grade"."item_type" = "base"."type") LEFT OUTER JOIN "lottery" ON ("base"."type" = "lottery"."item_type") WHERE ("base"."type" = %s AND "grade"."status" IN (%s, %s) OR "lottery"."add_time" > %s AND "lottery"."add_time" <= %s) AND "base"."status" NOT IN (%s, %s)', [1, 0, 1, '2009-01-01', now, 1, 2, ], ) 129 | ) 130 | self.assertEqual( 131 | Q(t).where(w).select(F.grade__name, F.base__img, F.lottery__price, E("CASE 1 WHEN " + PLACEHOLDER, 'exp_value').as_("exp_result")), 132 | ('SELECT "grade"."name", "base"."img", "lottery"."price", (CASE 1 WHEN %s) AS "exp_result" FROM "grade" INNER JOIN "base" ON ("grade"."item_type" = "base"."type") LEFT OUTER JOIN "lottery" ON ("base"."type" = "lottery"."item_type") WHERE ("base"."type" = %s AND "grade"."status" IN (%s, %s) OR "lottery"."add_time" > %s AND "lottery"."add_time" <= %s) AND "base"."status" NOT IN (%s, %s)', ['exp_value', 1, 0, 1, '2009-01-01', now, 1, 2, ], ) 133 | ) 134 | q = Q(T.user) 135 | self.assertEqual( 136 | q.select(F.name), 137 | ('SELECT "name" FROM "user"', [], ) 138 | ) 139 | q = q.tables((q.tables() & T.address).on(F.user__id == F.address__user_id)) 140 | self.assertEqual( 141 | q.select(F.user__name, F.address__street), 142 | ('SELECT "user"."name", "address"."street" FROM "user" INNER JOIN "address" ON ("user"."id" = "address"."user_id")', [], ) 143 | ) 144 | q = q.where(F.id == 1) 145 | self.assertEqual( 146 | q.select(F.name, F.id), 147 | ('SELECT "name", "id" FROM "user" INNER JOIN "address" ON ("user"."id" = "address"."user_id") WHERE "id" = %s', [1, ], ) 148 | ) 149 | q = q.where((F.address__city_id == [111, 112]) | E("address.city_id IS NULL")) 150 | self.assertEqual( 151 | q.select(F.user__name, F.address__street, func.COUNT(F("*")).as_("count")), 152 | ('SELECT "user"."name", "address"."street", COUNT(*) AS "count" FROM "user" INNER JOIN "address" ON ("user"."id" = "address"."user_id") WHERE "id" = %s AND ("address"."city_id" IN (%s, %s) OR (address.city_id IS NULL))', [1, 111, 112, ], ) 153 | ) 154 | 155 | def test_subquery(self): 156 | sub_q = Q(T.tb2).fields(T.tb2.id2).where(T.tb2.id == T.tb1.tb2_id).limit(1) 157 | self.assertEqual( 158 | Q(T.tb1).where(T.tb1.tb2_id == sub_q).select(T.tb1.id), 159 | ('SELECT "tb1"."id" FROM "tb1" WHERE "tb1"."tb2_id" = (SELECT "tb2"."id2" FROM "tb2" WHERE "tb2"."id" = "tb1"."tb2_id" LIMIT %s)', [1], ) 160 | ) 161 | self.assertEqual( 162 | Q(T.tb1).where(T.tb1.tb2_id.in_(sub_q)).select(T.tb1.id), 163 | ('SELECT "tb1"."id" FROM "tb1" WHERE "tb1"."tb2_id" IN (SELECT "tb2"."id2" FROM "tb2" WHERE "tb2"."id" = "tb1"."tb2_id" LIMIT %s)', [1], ) 164 | ) 165 | self.assertEqual( 166 | Q(T.tb1).select(sub_q.as_('sub_value')), 167 | ('SELECT (SELECT "tb2"."id2" FROM "tb2" WHERE "tb2"."id" = "tb1"."tb2_id" LIMIT %s) AS "sub_value" FROM "tb1"', [1], ) 168 | ) 169 | 170 | def test_expression(self): 171 | self.assertEqual( 172 | Q(T.tb1).select(E('5 * 3 - 2*8').as_('sub_value')), 173 | ('SELECT (5 * 3 - 2*8) AS "sub_value" FROM "tb1"', [], ) 174 | ) 175 | self.assertEqual( 176 | Q(T.tb1).select(E('(5 - 2) * 8 + (6 - 3) * 8').as_('sub_value')), 177 | ('SELECT ((5 - 2) * 8 + (6 - 3) * 8) AS "sub_value" FROM "tb1"', [], ) 178 | ) 179 | 180 | def test_union(self): 181 | a = Q(T.item).where(T.item.status != -1).fields(T.item.type, T.item.name, T.item.img) 182 | b = Q(T.gift).where(T.gift.storage > 0).columns(T.gift.type, T.gift.name, T.gift.img) 183 | self.assertEqual( 184 | (a.as_set(True) | b).order_by("type", "name", desc=True).limit(100, 10).select(), 185 | ('(SELECT "item"."type", "item"."name", "item"."img" FROM "item" WHERE "item"."status" <> %s) UNION ALL (SELECT "gift"."type", "gift"."name", "gift"."img" FROM "gift" WHERE "gift"."storage" > %s) ORDER BY %s DESC, %s DESC LIMIT %s OFFSET %s', [-1, 0, 'type', 'name', 10, 100], ) 186 | ) 187 | -------------------------------------------------------------------------------- /sqlbuilder/smartsql/tests/test_tables.py: -------------------------------------------------------------------------------- 1 | from sqlbuilder.smartsql.tests.base import TestCase 2 | from sqlbuilder.smartsql import ( 3 | Q, T, Table, TableJoin, TA, E, Field, 4 | Join, InnerJoin, LeftJoin, RightJoin, FullJoin, CrossJoin, State, CONTEXT, 5 | model_registry, compile 6 | ) 7 | from sqlbuilder.smartsql.dialects.mysql import compile as mysql_compile 8 | 9 | __all__ = ('TestTable', 'TestModelBasedTable', 'TestFieldProxy', ) 10 | 11 | 12 | class TestTable(TestCase): 13 | 14 | def test_table(self): 15 | self.assertEqual( 16 | type(T.book), 17 | Table 18 | ) 19 | self.assertEqual( 20 | compile(T.book), 21 | ('"book"', []) 22 | ) 23 | self.assertEqual( 24 | compile(T.author.get_field(('first_name', 'last_name')) == ('fn1', 'ln2')), 25 | ('"author"."first_name" = %s AND "author"."last_name" = %s', ['fn1', 'ln2']) 26 | ) 27 | self.assertEqual( 28 | compile(T.author.get_field(('first_name__a', 'last_name__b')) == ('fn1', 'ln2')), 29 | ('"a" = %s AND "b" = %s', ['fn1', 'ln2']) 30 | ) 31 | self.assertEqual( 32 | type(T.book__a), 33 | TA 34 | ) 35 | state = State() 36 | state.push("context", CONTEXT.FIELD_PREFIX) 37 | compile(T.book__a, state) 38 | self.assertEqual( 39 | (''.join(state.sql), state.params), 40 | ('"a"', []) 41 | ) 42 | self.assertEqual( 43 | compile(T.book__a), 44 | ('"a"', []) 45 | ) 46 | self.assertEqual( 47 | type(T.book.as_('a')), 48 | TA 49 | ) 50 | state = State() 51 | state.push("context", CONTEXT.FIELD_PREFIX) 52 | compile(T.book.as_('a'), state) 53 | self.assertEqual( 54 | (''.join(state.sql), state.params), 55 | ('"a"', []) 56 | ) 57 | self.assertEqual( 58 | compile(T.book.as_('a')), 59 | ('"a"', []) 60 | ) 61 | ta = T.book.as_('a') 62 | self.assertEqual( 63 | compile(Q().tables(ta).fields(ta.id, ta.status).where(ta.status.in_(('new', 'approved')))), 64 | ('SELECT "a"."id", "a"."status" FROM "book" AS "a" WHERE "a"."status" IN (%s, %s)', ['new', 'approved']) 65 | ) 66 | t = T.book 67 | self.assertIs(t.status, t.status) 68 | self.assertIs(t.status, t.f.status) 69 | self.assertIs(t.status, t.f('status')) 70 | self.assertIs(t.status, t.f['status']) 71 | self.assertIs(t.status, t['status']) 72 | self.assertIs(t.status, t.__getattr__('status')) 73 | self.assertIs(t.status, t.get_field('status')) 74 | 75 | def test_join(self): 76 | self.assertEqual( 77 | compile((T.book & T.author).on(T.book.author_id == T.author.id)), 78 | ('"book" INNER JOIN "author" ON ("book"."author_id" = "author"."id")', []) 79 | ) 80 | self.assertEqual( 81 | compile((T.book + T.author).on(T.book.author_id == T.author.id)), 82 | ('"book" LEFT OUTER JOIN "author" ON ("book"."author_id" = "author"."id")', []) 83 | ) 84 | self.assertEqual( 85 | compile((T.book - T.author).on(T.book.author_id == T.author.id)), 86 | ('"book" RIGHT OUTER JOIN "author" ON ("book"."author_id" = "author"."id")', []) 87 | ) 88 | self.assertEqual( 89 | compile((T.book | T.author).on(T.book.author_id == T.author.id)), 90 | ('"book" FULL OUTER JOIN "author" ON ("book"."author_id" = "author"."id")', []) 91 | ) 92 | self.assertEqual( 93 | compile((T.book * T.author).on(T.book.author_id == T.author.id)), 94 | ('"book" CROSS JOIN "author" ON ("book"."author_id" = "author"."id")', []) 95 | ) 96 | 97 | def test_join_priorities(self): 98 | t1, t2, t3, t4, t5 = T.t1, T.t2, T.t3, T.t4, T.t5 99 | self.assertEqual( 100 | compile(t1 | t2.on(t2.t1_id == t1.id) * t3.on(t3.t1_id == t1.id) + t4.on(t4.t1_id == t1.id) - t5.on(t5.t1_id == t5.id)), 101 | ('"t1" FULL OUTER JOIN "t2" ON ("t2"."t1_id" = "t1"."id") CROSS JOIN "t3" ON ("t3"."t1_id" = "t1"."id") LEFT OUTER JOIN "t4" ON ("t4"."t1_id" = "t1"."id") RIGHT OUTER JOIN "t5" ON ("t5"."t1_id" = "t5"."id")', []) 102 | ) 103 | self.assertEqual( 104 | compile(((((t1 | t2).on(t2.t1_id == t1.id) * t3).on(t3.t1_id == t1.id) + t4).on(t4.t1_id == t1.id) - t5.on(t5.t1_id == t5.id))), 105 | ('"t1" FULL OUTER JOIN "t2" ON ("t2"."t1_id" = "t1"."id") CROSS JOIN "t3" ON ("t3"."t1_id" = "t1"."id") LEFT OUTER JOIN "t4" ON ("t4"."t1_id" = "t1"."id") RIGHT OUTER JOIN "t5" ON ("t5"."t1_id" = "t5"."id")', []) 106 | ) 107 | 108 | def test_join_nested(self): 109 | t1, t2, t3, t4 = T.t1, T.t2, T.t3, T.t4 110 | self.assertEqual( 111 | compile(t1 + (t2 * t3 * t4)().on((t2.a == t1.a) & (t3.b == t1.b) & (t4.c == t1.c))), 112 | ('"t1" LEFT OUTER JOIN ("t2" CROSS JOIN "t3" CROSS JOIN "t4") ON ("t2"."a" = "t1"."a" AND "t3"."b" = "t1"."b" AND "t4"."c" = "t1"."c")', []) 113 | ) 114 | self.assertEqual( 115 | compile((t1 + (t2 * t3 * t4)()).on((t2.a == t1.a) & (t3.b == t1.b) & (t4.c == t1.c))), 116 | ('"t1" LEFT OUTER JOIN ("t2" CROSS JOIN "t3" CROSS JOIN "t4") ON ("t2"."a" = "t1"."a" AND "t3"."b" = "t1"."b" AND "t4"."c" = "t1"."c")', []) 117 | ) 118 | self.assertEqual( 119 | compile(t1 + (t2 + t3).on((t2.b == t3.b) | t2.b.is_(None))()), 120 | ('"t1" LEFT OUTER JOIN ("t2" LEFT OUTER JOIN "t3" ON ("t2"."b" = "t3"."b" OR "t2"."b" IS NULL))', []) 121 | ) 122 | self.assertEqual( 123 | compile((t1 + t2.on(t1.a == t2.a))() + t3.on((t2.b == t3.b) | t2.b.is_(None))), 124 | ('("t1" LEFT OUTER JOIN "t2" ON ("t1"."a" = "t2"."a")) LEFT OUTER JOIN "t3" ON ("t2"."b" = "t3"."b" OR "t2"."b" IS NULL)', []) 125 | ) 126 | 127 | def test_join_nested_old(self): 128 | t1, t2, t3, t4 = T.t1, T.t2.as_('al2'), T.t3, T.t4 129 | self.assertEqual( 130 | Q((t1 + t2.on(t2.t1_id == t1.id)) * t3.on(t3.t2_id == t2.id) - t4.on(t4.t3_id == t3.id)).select(t1.id), 131 | ('SELECT "t1"."id" FROM "t1" LEFT OUTER JOIN "t2" AS "al2" ON ("al2"."t1_id" = "t1"."id") CROSS JOIN "t3" ON ("t3"."t2_id" = "al2"."id") RIGHT OUTER JOIN "t4" ON ("t4"."t3_id" = "t3"."id")', [], ) 132 | ) 133 | self.assertEqual( 134 | Q((t1 + t2).on(t2.t1_id == t1.id) * t3.on(t3.t2_id == t2.id) - t4.on(t4.t3_id == t3.id)).select(t1.id), 135 | ('SELECT "t1"."id" FROM "t1" LEFT OUTER JOIN "t2" AS "al2" ON ("al2"."t1_id" = "t1"."id") CROSS JOIN "t3" ON ("t3"."t2_id" = "al2"."id") RIGHT OUTER JOIN "t4" ON ("t4"."t3_id" = "t3"."id")', [], ) 136 | ) 137 | self.assertEqual( 138 | Q((t1 + ((t2 * t3).on(t3.t2_id == t2.id))()).on(t2.t1_id == t1.id) - t4.on(t4.t3_id == t3.id)).select(t1.id), 139 | ('SELECT "t1"."id" FROM "t1" LEFT OUTER JOIN ("t2" AS "al2" CROSS JOIN "t3" ON ("t3"."t2_id" = "al2"."id")) ON ("al2"."t1_id" = "t1"."id") RIGHT OUTER JOIN "t4" ON ("t4"."t3_id" = "t3"."id")', [], ) 140 | ) 141 | self.assertEqual( 142 | Q(((t1 + t2) * t3 - t4)().on((t2.t1_id == t1.id) & (t3.t2_id == t2.id) & (t4.t3_id == t3.id))).select(t1.id), 143 | ('SELECT "t1"."id" FROM ("t1" LEFT OUTER JOIN "t2" AS "al2" CROSS JOIN "t3" RIGHT OUTER JOIN "t4") ON ("al2"."t1_id" = "t1"."id" AND "t3"."t2_id" = "al2"."id" AND "t4"."t3_id" = "t3"."id")', [], ) 144 | ) 145 | self.assertEqual( 146 | Q((t1 & t2.on(t2.t1_id == t1.id) & (t3 & t4.on(t4.t3_id == t3.id))()).on(t3.t2_id == t2.id)).select(t1.id), 147 | ('SELECT "t1"."id" FROM "t1" INNER JOIN "t2" AS "al2" ON ("al2"."t1_id" = "t1"."id") INNER JOIN ("t3" INNER JOIN "t4" ON ("t4"."t3_id" = "t3"."id")) ON ("t3"."t2_id" = "al2"."id")', [], ) 148 | ) 149 | self.assertEqual( 150 | Q(t1 & t2.on(t2.t1_id == t1.id) & (t3 & t4.on(t4.t3_id == t3.id)).as_nested().on(t3.t2_id == t2.id)).select(t1.id), 151 | ('SELECT "t1"."id" FROM "t1" INNER JOIN "t2" AS "al2" ON ("al2"."t1_id" = "t1"."id") INNER JOIN ("t3" INNER JOIN "t4" ON ("t4"."t3_id" = "t3"."id")) ON ("t3"."t2_id" = "al2"."id")', [], ) 152 | ) 153 | self.assertEqual( 154 | Q((t1 & t2.on(t2.t1_id == t1.id))() & (t3 & t4.on(t4.t3_id == t3.id))().on(t3.t2_id == t2.id)).select(t1.id), 155 | ('SELECT "t1"."id" FROM ("t1" INNER JOIN "t2" AS "al2" ON ("al2"."t1_id" = "t1"."id")) INNER JOIN ("t3" INNER JOIN "t4" ON ("t4"."t3_id" = "t3"."id")) ON ("t3"."t2_id" = "al2"."id")', [], ) 156 | ) 157 | 158 | def test_hint(self): 159 | t1, t2 = T.tb1, T.tb1.as_('al2') 160 | q = Q(t1 & t2.hint(E('USE INDEX (`index1`, `index2`)')).on(t2.parent_id == t1.id)) 161 | q.result.compile = mysql_compile 162 | self.assertEqual( 163 | q.select(t2.id), 164 | ('SELECT `al2`.`id` FROM `tb1` INNER JOIN `tb1` AS `al2` ON (`al2`.`parent_id` = `tb1`.`id`) USE INDEX (`index1`, `index2`)', [], ) 165 | ) 166 | 167 | def test_issue_20(self): 168 | t1, t2 = T.tb1, T.tb2 169 | tj = t2.on(t1.id == t2.id) 170 | self.assertEqual( 171 | compile(tj), 172 | ('"tb2" ON ("tb1"."id" = "tb2"."id")', []) 173 | ) 174 | self.assertEqual( 175 | compile(t1 + tj), 176 | ('"tb1" LEFT OUTER JOIN "tb2" ON ("tb1"."id" = "tb2"."id")', []) 177 | ) 178 | self.assertEqual( 179 | compile(t1 + tj), 180 | ('"tb1" LEFT OUTER JOIN "tb2" ON ("tb1"."id" = "tb2"."id")', []) 181 | ) 182 | 183 | 184 | class PropertyDescriptor(object): 185 | _field = None 186 | _value = None 187 | 188 | def _get_name(self, owner): 189 | for k, v in owner.__dict__.items(): 190 | if v is self: 191 | return k 192 | 193 | def __get__(self, instance, owner): 194 | if instance is None: 195 | if self._field is None: 196 | self._field = Field(self._get_name(owner), owner) 197 | return self._field 198 | else: 199 | return self._value 200 | 201 | def __set__(self, instance, value): 202 | self._value = value 203 | 204 | 205 | @model_registry.register('author') 206 | class Author(object): 207 | id = PropertyDescriptor() 208 | first_name = PropertyDescriptor() 209 | last_name = PropertyDescriptor() 210 | 211 | 212 | @model_registry.register('post') 213 | class Post(object): 214 | id = PropertyDescriptor() 215 | title = PropertyDescriptor() 216 | text = PropertyDescriptor() 217 | author_id = PropertyDescriptor() 218 | 219 | 220 | class TestModelBasedTable(TestCase): 221 | 222 | def test_model(self): 223 | self.assertIsInstance(Author.first_name, Field) 224 | self.assertEqual( 225 | compile(Author), 226 | ('"author"', []) 227 | ) 228 | self.assertEqual( 229 | compile(Author.first_name), 230 | ('"author"."first_name"', []) 231 | ) 232 | self.assertEqual( 233 | compile((TableJoin(Author) & Post).on(Post.author_id == Author.id)), 234 | ('"author" INNER JOIN "post" ON ("post"."author_id" = "author"."id")', []) 235 | ) 236 | self.assertEqual( 237 | compile(Join(Author, Post, on=(Post.author_id == Author.id))), 238 | ('"author" JOIN "post" ON ("post"."author_id" = "author"."id")', []) 239 | ) 240 | self.assertEqual( 241 | compile(InnerJoin(Author, Post, on=(Post.author_id == Author.id))), 242 | ('"author" INNER JOIN "post" ON ("post"."author_id" = "author"."id")', []) 243 | ) 244 | self.assertEqual( 245 | compile(LeftJoin(Author, Post, on=(Post.author_id == Author.id))), 246 | ('"author" LEFT OUTER JOIN "post" ON ("post"."author_id" = "author"."id")', []) 247 | ) 248 | self.assertEqual( 249 | compile(RightJoin(Author, Post, on=(Post.author_id == Author.id))), 250 | ('"author" RIGHT OUTER JOIN "post" ON ("post"."author_id" = "author"."id")', []) 251 | ) 252 | self.assertEqual( 253 | compile(FullJoin(Author, Post, on=(Post.author_id == Author.id))), 254 | ('"author" FULL OUTER JOIN "post" ON ("post"."author_id" = "author"."id")', []) 255 | ) 256 | self.assertEqual( 257 | compile(CrossJoin(Author, Post, on=(Post.author_id == Author.id))), 258 | ('"author" CROSS JOIN "post" ON ("post"."author_id" = "author"."id")', []) 259 | ) 260 | 261 | 262 | class TestFieldProxy(TestCase): 263 | 264 | def test_model(self): 265 | author = T.author.f 266 | post = T.post.f 267 | 268 | self.assertIsInstance(author.first_name, Field) 269 | self.assertEqual( 270 | compile(author), 271 | ('"author"', []) 272 | ) 273 | self.assertEqual( 274 | compile(author.first_name), 275 | ('"author"."first_name"', []) 276 | ) 277 | self.assertEqual( 278 | compile((TableJoin(author) & post).on(post.author_id == author.id)), 279 | ('"author" INNER JOIN "post" ON ("post"."author_id" = "author"."id")', []) 280 | ) 281 | self.assertEqual( 282 | compile(Join(author, post, on=(post.author_id == author.id))), 283 | ('"author" JOIN "post" ON ("post"."author_id" = "author"."id")', []) 284 | ) 285 | self.assertEqual( 286 | compile(InnerJoin(author, post, on=(post.author_id == author.id))), 287 | ('"author" INNER JOIN "post" ON ("post"."author_id" = "author"."id")', []) 288 | ) 289 | self.assertEqual( 290 | compile(LeftJoin(author, post, on=(post.author_id == author.id))), 291 | ('"author" LEFT OUTER JOIN "post" ON ("post"."author_id" = "author"."id")', []) 292 | ) 293 | self.assertEqual( 294 | compile(RightJoin(author, post, on=(post.author_id == author.id))), 295 | ('"author" RIGHT OUTER JOIN "post" ON ("post"."author_id" = "author"."id")', []) 296 | ) 297 | self.assertEqual( 298 | compile(FullJoin(author, post, on=(post.author_id == author.id))), 299 | ('"author" FULL OUTER JOIN "post" ON ("post"."author_id" = "author"."id")', []) 300 | ) 301 | self.assertEqual( 302 | compile(CrossJoin(author, post, on=(post.author_id == author.id))), 303 | ('"author" CROSS JOIN "post" ON ("post"."author_id" = "author"."id")', []) 304 | ) 305 | -------------------------------------------------------------------------------- /sqlbuilder/smartsql/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from sqlbuilder.smartsql.tests.base import TestCase 2 | from sqlbuilder.smartsql.utils import AutoName 3 | 4 | __all__ = ('TestAutoName', ) 5 | 6 | 7 | class TestAutoName(TestCase): 8 | 9 | def test_autoname(self): 10 | auto_name = AutoName() 11 | unique_names = set() 12 | length = 10 13 | for i in range(length): 14 | unique_names.add(next(auto_name)) 15 | self.assertEqual(len(unique_names), length) 16 | -------------------------------------------------------------------------------- /sqlbuilder/smartsql/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import warnings 3 | from functools import wraps 4 | 5 | __all__ = ('Undef', 'UndefType', 'is_list', 'is_allowed_attr', 'opt_checker', 'same', 'warn', ) 6 | 7 | 8 | class UndefType(object): 9 | 10 | def __repr__(self): 11 | return "Undef" 12 | 13 | def __reduce__(self): 14 | return "Undef" 15 | 16 | 17 | Undef = UndefType() 18 | 19 | 20 | class AutoName(object): 21 | def __init__(self, prefix="_auto_"): 22 | self.counter = 0 23 | self.prefix = prefix 24 | 25 | def __iter__(self): 26 | return self 27 | 28 | def __next__(self): 29 | self.counter += 1 30 | return "{0}{1}".format(self.prefix, self.counter) 31 | 32 | next = __next__ 33 | 34 | 35 | auto_name = AutoName() 36 | 37 | 38 | def is_allowed_attr(instance, key): 39 | if key.startswith('__'): 40 | return False 41 | if key in dir(instance.__class__): # type(instance)? 42 | # It's a descriptor, like 'sql' defined in slots 43 | return False 44 | return True 45 | 46 | 47 | def is_list(value): 48 | return isinstance(value, (list, tuple)) 49 | 50 | 51 | def same(name): 52 | def f(self, *a, **kw): 53 | return getattr(self, name)(*a, **kw) 54 | return f 55 | 56 | 57 | def opt_checker(k_list): 58 | def new_deco(f): 59 | @wraps(f) 60 | def new_func(self, *args, **opt): 61 | for k, v in list(opt.items()): 62 | if k not in k_list: 63 | raise TypeError("Not implemented option: {0}".format(k)) 64 | return f(self, *args, **opt) 65 | return new_func 66 | return new_deco 67 | 68 | 69 | def warn(old, new, stacklevel=3): 70 | warnings.warn("{0} is deprecated. Use {1} instead".format(old, new), PendingDeprecationWarning, stacklevel=stacklevel) 71 | --------------------------------------------------------------------------------