├── .editorconfig ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── raw_sugar ├── __init__.py ├── managers.py ├── query.py └── sources.py ├── runtests.py ├── setup.py └── tests ├── __init__.py ├── models.py ├── settings.py └── test_querying.py /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | insert_final_newline = true 4 | 5 | # 4 space indentation 6 | [*.py] 7 | indent_style = space 8 | indent_size = 4 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Editors 2 | .vscode/ 3 | .idea/ 4 | 5 | # Vagrant 6 | .vagrant/ 7 | 8 | # Mac/OSX 9 | .DS_Store 10 | 11 | # Windows 12 | Thumbs.db 13 | 14 | # Source for the following rules: https://raw.githubusercontent.com/github/gitignore/master/Python.gitignore 15 | # Byte-compiled / optimized / DLL files 16 | __pycache__/ 17 | *.py[cod] 18 | *$py.class 19 | 20 | # C extensions 21 | *.so 22 | 23 | # Distribution / packaging 24 | .Python 25 | build/ 26 | develop-eggs/ 27 | dist/ 28 | downloads/ 29 | eggs/ 30 | .eggs/ 31 | lib/ 32 | lib64/ 33 | parts/ 34 | sdist/ 35 | var/ 36 | wheels/ 37 | *.egg-info/ 38 | .installed.cfg 39 | *.egg 40 | MANIFEST 41 | 42 | # PyInstaller 43 | # Usually these files are written by a python script from a template 44 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 45 | *.manifest 46 | *.spec 47 | 48 | # Installer logs 49 | pip-log.txt 50 | pip-delete-this-directory.txt 51 | 52 | # Unit test / coverage reports 53 | htmlcov/ 54 | .tox/ 55 | .nox/ 56 | .coverage 57 | .coverage.* 58 | .cache 59 | nosetests.xml 60 | coverage.xml 61 | *.cover 62 | .hypothesis/ 63 | .pytest_cache/ 64 | 65 | # Translations 66 | *.mo 67 | *.pot 68 | 69 | # Django stuff: 70 | *.log 71 | local_settings.py 72 | db.sqlite3 73 | 74 | # Flask stuff: 75 | instance/ 76 | .webassets-cache 77 | 78 | # Scrapy stuff: 79 | .scrapy 80 | 81 | # Sphinx documentation 82 | docs/_build/ 83 | 84 | # PyBuilder 85 | target/ 86 | 87 | # Jupyter Notebook 88 | .ipynb_checkpoints 89 | 90 | # IPython 91 | profile_default/ 92 | ipython_config.py 93 | 94 | # pyenv 95 | .python-version 96 | 97 | # celery beat schedule file 98 | celerybeat-schedule 99 | 100 | # SageMath parsed files 101 | *.sage.py 102 | 103 | # Environments 104 | .env 105 | .venv 106 | env/ 107 | venv/ 108 | ENV/ 109 | env.bak/ 110 | venv.bak/ 111 | 112 | # Spyder project settings 113 | .spyderproject 114 | .spyproject 115 | 116 | # Rope project settings 117 | .ropeproject 118 | 119 | # mkdocs documentation 120 | /site 121 | 122 | # mypy 123 | .mypy_cache/ 124 | .dmypy.json 125 | dmypy.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Manuel Francisco Naranjo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-raw-sugar 2 | 3 | Turns your raw sql into a QuerySet. 4 | 5 | ## Installation 6 | 7 | Install using `pip`... 8 | 9 | pip install django-raw-sugar 10 | 11 | ## How to use 12 | ### Basic usage 13 | Attach `RawManager` instance to your model. Then use it's `.from_raw()` method. 14 | 15 | RawManager.from_raw(raw_query=None, params=None, translations=None, null_fields=None, db_table=None) 16 | 17 | You should provide either `raw_query` or `db_table` (but not both). 18 | 19 | ```python 20 | # models.py 21 | from django.db import models 22 | from raw_sugar import RawManager 23 | 24 | class MySimpleModel(models.Model): 25 | name = models.TextField() 26 | number = models.IntegerField() 27 | source = models.ForeignKey(AnotherSimpleModel, models.DO_NOTHING) 28 | 29 | objects = RawManager() 30 | 31 | # some other file 32 | from .models import MySimpleModel 33 | 34 | queryset = MySimpleModel.objects.from_raw( 35 | 'SELECT Null as id, "my str" as name, 111 as number, Null as source_id') 36 | ``` 37 | 38 | The result of your raw sql must contain all the fields that are present in target model, including primary key and foreign keys. If you know your raw sql lacks some fields, you can provide the `null_fields` argument instead of modifying your query: 39 | 40 | ```python 41 | queryset = MySimpleModel.objects.from_raw( 42 | 'SELECT "my str" as name, 111 as number', null_fields=['id', 'source_id']) 43 | ``` 44 | 45 | The resulting queryset is a regular `models.QuerySet` instance, and can be handled accordingly: 46 | 47 | ```python 48 | queryset = queryset.filter(number__gte=10)\ 49 | .exclude(number__gte=1000)\ 50 | .filter(name__contains='s')\ 51 | .order_by('number')\ 52 | .select_related('source') 53 | print(queryset[0].name) # "my str" 54 | ``` 55 | 56 | ### Passing parameters 57 | If you need to perform parameterized queries, you can use the `params` argument: 58 | ```python 59 | queryset = MySimpleModel.objects.from_raw( 60 | 'SELECT "%s" as name, 111 as number', 61 | params=['my str'], 62 | null_fields=['id', 'source_id']) 63 | ``` 64 | If you want to pass params deferred, you can use the `with_params` method: 65 | ```python 66 | queryset = MySimpleModel.objects.from_raw( 67 | 'SELECT "%s" as name, 111 as number', 68 | null_fields=['id', 'source_id']) 69 | queryset = queryset.with_params('my str') 70 | ``` 71 | 72 | ### Using translations 73 | If the field names of queried table differ from the model field names, you can map fields by using the `translations` argument: 74 | ```python 75 | queryset = MySimpleModel.objects.from_raw( 76 | 'SELECT "%s" as name, 111 as inner_number', 77 | params=['my str'], 78 | translations={'inner_number': 'number'}, 79 | null_fields=['id', 'source_id']) 80 | ``` 81 | 82 | ### Pre defined source raw sql 83 | You can define a model manager that uses your raw sql as query source by default. You can do this by passing a `from_raw` argument to RawManager, or by using the `raw_manager` decorator to method that returns a `FromRaw` instance: 84 | 85 | ```python 86 | from django.db import models 87 | from raw_sugar import raw_manager, RawManager, FromRaw 88 | 89 | class MySimpleModel(models.model): 90 | name = models.TextField() 91 | number = models.IntegerField() 92 | source = models.ForeignKey(AnotherSimpleModel, models.DO_NOTHING) 93 | 94 | my_raw_manager = RawManager(FromRaw('SELECT "my str" as name, 111 as number', 95 | null_fields=['id', 'source_id'])) 96 | 97 | @raw_manager 98 | def my_raw_manager_2(cls): 99 | return FromRaw('SELECT "my str" as name, 111 as number', 100 | null_fields=['id', 'source_id']) 101 | 102 | @raw_manager(is_callable=True) 103 | def my_callable_raw_manager(cls, name=""): 104 | return FromRaw('SELECT %s as name, 111 as number', 105 | null_fields=['id', 'source_id'], 106 | params=[name]) 107 | 108 | # some other file 109 | from .models import MySimpleModel 110 | 111 | queryset = MySimpleModel.my_raw_source.all() 112 | queryset = MySimpleModel.my_raw_source_2.all() 113 | queryset = MySimpleModel.my_callable_raw_source('my str').all() 114 | 115 | print(queryset[0].name) # "my str" 116 | ``` 117 | The `FromRaw` class accepts all the arguments as the `RawManager.from_raw`: 118 | FromRaw(raw_query=None, params=None, translations=None, null_fields=None, db_table=None) 119 | 120 | When you use the `raw_manager` decorator, the parameters you pass to `with_params` method will be passed into the decorated method, not into your raw. If you need this behavour, you can do it manually: 121 | 122 | ```python 123 | @raw_manager(is_callable=True) 124 | def my_callable_raw_manager(cls, *args): 125 | assert len(args) == 2 126 | return FromRaw('SELECT %s as name, %s as number', null_fields=['id', 'source_id'], params=args) 127 | ``` 128 | 129 | ### Querying views / table functions 130 | If you have a sql view or a sql table function in your database and want to query it, instead of passing sql like `SELECT * from my_view` you can use the `db_table` argument: 131 | ```python 132 | queryset = MySimpleModel.objects.from_raw(db_table='my_view') 133 | queryset = MySimpleModel.objects.from_raw(db_table='my_func(%s, %s)', params=['param', 1]) 134 | queryset = MySimpleModel.objects.from_raw(db_table='my_func(%s, %s)').with_params('param', 1) 135 | ``` 136 | 137 | ### Use a QuerySet as a source 138 | You can use a QuerySet instance as a source instead of raw sql by returning a `FromQuerySet` instance from decorated manager method: 139 | 140 | ```python 141 | class MySimpleModel(models.Model): 142 | name = models.TextField() 143 | number = models.IntegerField() 144 | source = models.ForeignKey( 145 | AnotherSimpleModel, models.DO_NOTHING, null=True) 146 | 147 | @raw_manager 148 | def my_qs_manager(cls): 149 | return FromQuerySet( 150 | cls.objects.values('source')\ 151 | .annotate(_number=models.Sum('number')), 152 | translations={'_number': 'number'}) 153 | ``` 154 | 155 | The `FromQuerySet` class accepts only a QuerySet and translations: 156 | 157 | FromQuerySet(queryset, translations=None) 158 | 159 | If the provided QuerySet lacks some fields, the `Null` will be returned. You don't need to specify `null_fields` as you would with the `FromRaw`. 160 | 161 | ## Differences with `Manager.raw()` 162 | Pros: 163 | - The result of executing of your raw sql is a **QuerySet** (!!!), and can filter, order, annotate, union, etc. it. 164 | 165 | Cons: 166 | - The result of your `FromRaw` must contain all fields of target model, including primary and foreign keys. If you omit any, you get an `OperationalError('no such column: ...')` exception. 167 | - If you don't provide some fields in source QuerySet when use the `FromQuerySet`, this fields are filled with `Null`, and can not be loaded on demand. The Django's `RawQuerySet` [allows it](https://docs.djangoproject.com/en/3.1/topics/db/sql/#deferring-model-fields). 168 | -------------------------------------------------------------------------------- /raw_sugar/__init__.py: -------------------------------------------------------------------------------- 1 | from .managers import RawManager, raw_manager 2 | from .sources import FromRaw, FromQuerySet 3 | -------------------------------------------------------------------------------- /raw_sugar/managers.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from .query import RawSugarQuerySet 3 | from .sources import FromRaw, FromQuerySet 4 | 5 | 6 | class RawManagerMixin: 7 | def from_raw(self, raw_query=None, params=[], 8 | translations={}, null_fields=[], 9 | db_table=None): 10 | def source_func(cls, *args): 11 | return FromRaw(raw_query, args, translations, 12 | null_fields, db_table) 13 | return RawSugarQuerySet(self.model, 14 | using=self._db, 15 | _source_func=source_func, 16 | _source_func_args=params) 17 | 18 | def from_queryset(self, queryset, translations={}): 19 | def source_func(cls): 20 | return FromQuerySet(queryset, translations) 21 | return RawSugarQuerySet(self.model, 22 | using=self._db, 23 | _source_func=source_func) 24 | 25 | def with_params(self, *args): 26 | def source_func(cls, *args): 27 | return self._source.with_params(*args) 28 | return RawSugarQuerySet(self.model, 29 | using=self._db, 30 | _source_func=source_func, 31 | _source_func_args=args) 32 | 33 | 34 | class RawManager(models.Manager, RawManagerMixin): 35 | def __init__(self, from_raw=None): 36 | if from_raw is not None: 37 | assert isinstance(from_raw, FromRaw), \ 38 | "Expected a `FromRaw` to be passed "\ 39 | "to 'from_raw', but received a `%s" % type(from_raw) 40 | self._source = from_raw 41 | return super().__init__() 42 | 43 | def get_queryset(self): 44 | if isinstance(self._source, FromRaw): 45 | def source_func(cls, *args): 46 | return self._source.with_params(*args) 47 | return RawSugarQuerySet(self.model, 48 | using=self._db, 49 | _source_func=source_func, 50 | _source_func_args=self._source.params) 51 | else: 52 | return super().get_queryset() 53 | 54 | 55 | class DecoratedRawManager(models.Manager, RawManagerMixin): 56 | def __init__(self, *args, _source_func=None, _source_func_is_callable=False, 57 | _set_source_func_on_next_call=False, **kwargs): 58 | self._source_func = _source_func 59 | if not _source_func_is_callable: 60 | assert callable(self._source_func) 61 | self._source_func_is_callable = _source_func_is_callable 62 | self._set_source_func_on_next_call = _set_source_func_on_next_call 63 | return super().__init__(*args, **kwargs) 64 | 65 | def __call__(self, *args, **kwargs): 66 | if self._set_source_func_on_next_call: 67 | self._source_func = args[0] 68 | assert callable(self._source_func) 69 | self._set_source_func_on_next_call = False 70 | return self 71 | elif self._source_func_is_callable: 72 | return RawSugarQuerySet( 73 | self.model, using=self._db, 74 | _source_func=self._source_func, 75 | _source_func_args=args, 76 | _source_func_kwargs=kwargs) 77 | else: 78 | raise TypeError 79 | 80 | def get_queryset(self): 81 | return RawSugarQuerySet( 82 | self.model, using=self._db, 83 | _source_func=self._source_func) 84 | 85 | 86 | def raw_manager(is_callable=False): 87 | if callable(is_callable): 88 | return DecoratedRawManager(_source_func=is_callable) 89 | if is_callable: 90 | return DecoratedRawManager(_set_source_func_on_next_call=True, 91 | _source_func_is_callable=True) 92 | else: 93 | return DecoratedRawManager(_set_source_func_on_next_call=True) 94 | -------------------------------------------------------------------------------- /raw_sugar/query.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.db.models.sql import Query 3 | 4 | from raw_sugar.sources import SourceRaw, FromQuerySet 5 | 6 | 7 | class RawSugarQuery(Query): 8 | def __init__(self, *args, 9 | _source_func=None, 10 | _source_func_args=None, 11 | _source_func_kwargs=None, 12 | **kwargs): 13 | self._source_func = _source_func 14 | self._source_func_args = _source_func_args 15 | self._source_func_kwargs = _source_func_kwargs 16 | return super().__init__(*args, **kwargs) 17 | 18 | def get_compiler(self, *args, **kwargs): 19 | compiler = super().get_compiler(*args, **kwargs) 20 | get_from_clause_method = compiler.get_from_clause 21 | 22 | def get_from_clause_wrapper(*args, **kwargs): 23 | source = self._source_func( 24 | self.model, 25 | *(self._source_func_args or []), 26 | **(self._source_func_kwargs or {})) 27 | assert isinstance(source, SourceRaw) 28 | if isinstance(source, FromQuerySet): 29 | source._set_target_model(self.model) 30 | 31 | qn = compiler.connection.ops.quote_name 32 | result, params = get_from_clause_method(*args, **kwargs) 33 | assert result[0] == qn(self.model._meta.db_table) 34 | if len(source.translations) > 0 or len(source.null_fields) > 0: 35 | select_fields = [] 36 | for col in self.model._meta.fields: 37 | field_name = col.column 38 | for source_field, target_field in source.translations.items(): 39 | if target_field == field_name: 40 | select_fields.append('{}.{} AS {}'.format( 41 | qn('wrapper_table'), 42 | qn(source_field), 43 | qn(target_field))) 44 | break 45 | else: 46 | if field_name in source.null_fields: 47 | select_fields.append('CAST(NULL AS {}) AS {}'.format( 48 | col.cast_db_type(compiler.connection), 49 | qn(field_name))) 50 | else: 51 | select_fields.append('{}.{}'.format( 52 | qn('wrapper_table'), 53 | qn(field_name))) 54 | wrapper = '(SELECT {} FROM {} AS {})'.format( 55 | ', '.join(select_fields), 56 | source.raw_query, 57 | qn('wrapper_table')) 58 | else: 59 | wrapper = source.raw_query 60 | result[0] = '{} AS {}'.format( 61 | wrapper, qn(self.model._meta.db_table)) 62 | params = tuple(source.params) + tuple(params) 63 | return result, params 64 | 65 | compiler.get_from_clause = get_from_clause_wrapper 66 | return compiler 67 | 68 | 69 | class RawSugarQuerySet(models.QuerySet): 70 | def __init__(self, *args, query=None, _source_func=None, 71 | _source_func_args=None, _source_func_kwargs=None, 72 | **kwargs): 73 | empty_query = query is None 74 | r = super().__init__(*args, query=query, **kwargs) 75 | if empty_query: 76 | self.query = RawSugarQuery( 77 | self.model, 78 | _source_func=_source_func, 79 | _source_func_args=_source_func_args, 80 | _source_func_kwargs=_source_func_kwargs) 81 | return r 82 | 83 | def with_params(self, *args, **kwargs): 84 | clone = self._chain() 85 | clone.query._source_func_args = args 86 | clone.query._source_func_kwargs = kwargs 87 | return clone 88 | -------------------------------------------------------------------------------- /raw_sugar/sources.py: -------------------------------------------------------------------------------- 1 | from copy import copy 2 | from django.db import connection 3 | 4 | 5 | class SourceRaw: 6 | def copy(self): 7 | return copy(self) 8 | 9 | def with_params(self, *args): 10 | clone = self.copy() 11 | clone.params = args 12 | return clone 13 | 14 | 15 | class FromRaw(SourceRaw): 16 | def __init__(self, raw_query=None, params=None, 17 | translations=None, null_fields=None, 18 | db_table=None): 19 | assert raw_query or db_table, \ 20 | "Either `raw_query` or `db_table` must be provided!" 21 | assert not raw_query or not db_table, \ 22 | "Either `raw_query` or `db_table` must be provided, not both!" 23 | self.raw_query = db_table or '({})'.format(raw_query) 24 | self.params = params or [] 25 | self.translations = translations or {} 26 | self.null_fields = null_fields or [] 27 | 28 | 29 | class FromQuerySet(SourceRaw): 30 | def __init__(self, queryset, translations=None): 31 | queryset_fields = list(queryset.query.annotations.keys()) 32 | if translations: 33 | queryset_fields += [translations[i] for i in translations] 34 | if len(queryset.query.values_select) > 0: 35 | for field_name in queryset.query.values_select: 36 | for field in queryset.model._meta.fields: 37 | if field.name == field_name: 38 | queryset_fields.append(field.column) 39 | break 40 | else: 41 | queryset_fields.append(field_name) 42 | else: 43 | queryset_fields += [field.column for field in queryset.model._meta.fields] 44 | self._queryset_fields = queryset_fields 45 | 46 | raw_query, params = queryset.query.as_sql( 47 | connection=connection, compiler=None) 48 | self.raw_query = "({})".format(raw_query) 49 | self.params = params 50 | self.translations = translations or {} 51 | self.null_fields = [] 52 | 53 | def _set_target_model(self, model): 54 | model_fields = [f.column for f in model._meta.fields] 55 | self.null_fields = list( 56 | set(model_fields) - set(self._queryset_fields)) 57 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | from django.core.management import execute_from_command_line 5 | 6 | 7 | def runtests(): 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") 9 | argv = sys.argv[:1] + ['test'] + sys.argv[1:] 10 | execute_from_command_line(argv) 11 | 12 | 13 | if __name__ == '__main__': 14 | runtests() 15 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | # allow setup.py to be run from any path 5 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 6 | 7 | with open("README.md", 'r') as f: 8 | long_description = f.read() 9 | print(long_description) 10 | 11 | setup( 12 | name='django-raw-sugar', 13 | version='0.1.17', 14 | packages=find_packages(), 15 | include_package_data=True, 16 | license='MIT', 17 | description='Turns your raw sql into a QuerySet', 18 | long_description=long_description, 19 | long_description_content_type='text/markdown', 20 | url='https://github.com/zxibizz/django-raw-sugar', 21 | author='Roman Lee', 22 | author_email='romanlee1996@gmail.com', 23 | classifiers=[ 24 | 'Environment :: Web Environment', 25 | 'Framework :: Django', 26 | 'Intended Audience :: Developers', 27 | 'License :: OSI Approved :: MIT License', 28 | 'Operating System :: OS Independent', 29 | 'Programming Language :: Python', 30 | 'Programming Language :: Python :: 2', 31 | 'Programming Language :: Python :: 2.7', 32 | 'Programming Language :: Python :: 3', 33 | 'Programming Language :: Python :: 3.2', 34 | 'Programming Language :: Python :: 3.3', 35 | 'Programming Language :: Python :: 3.4', 36 | 'Topic :: Internet :: WWW/HTTP', 37 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 38 | ], 39 | ) 40 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zxibizz/django-raw-sugar/35fab147ed0f637cc12ae4f28093fc5eca0725e8/tests/__init__.py -------------------------------------------------------------------------------- /tests/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from raw_sugar import raw_manager, RawManager, FromRaw, FromQuerySet 3 | 4 | 5 | class AnotherSimpleModel(models.Model): 6 | pass 7 | 8 | 9 | class MySimpleModel(models.Model): 10 | name = models.TextField() 11 | number = models.IntegerField() 12 | source = models.ForeignKey( 13 | AnotherSimpleModel, models.DO_NOTHING, null=True) 14 | 15 | objects = RawManager() 16 | 17 | my_raw_manager = RawManager(FromRaw('SELECT "my str" as name, 111 as number', 18 | null_fields=['id', 'source_id'])) 19 | 20 | @raw_manager 21 | def my_raw_manager_2(cls): 22 | return FromRaw('SELECT "my str" as name, 111 as number', 23 | null_fields=['id', 'source_id']) 24 | 25 | @raw_manager(is_callable=True) 26 | def my_callable_raw_manager(cls, name): 27 | return FromRaw('SELECT %s as name, 111 as number', 28 | null_fields=['id', 'source_id'], 29 | params=[name]) 30 | 31 | @raw_manager 32 | def my_qs_manager(cls): 33 | return FromQuerySet( 34 | cls.objects.values('source').annotate( 35 | _number=models.Sum('number')), 36 | translations={'_number': 'number'}) 37 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | 2 | DATABASES = { 3 | 'default': { 4 | 'ENGINE': 'django.db.backends.sqlite3', 5 | 'NAME': ':memory:', 6 | }, 7 | } 8 | 9 | INSTALLED_APPS = ( 10 | 'django.contrib.contenttypes', 11 | 'tests', 12 | ) 13 | 14 | SECRET_KEY = 'foobar' 15 | -------------------------------------------------------------------------------- /tests/test_querying.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from .models import MySimpleModel, AnotherSimpleModel 3 | 4 | 5 | class DynamicQueringTest(TestCase): 6 | def setUp(self): 7 | asm1 = AnotherSimpleModel.objects.create() 8 | asm2 = AnotherSimpleModel.objects.create() 9 | MySimpleModel.objects.create( 10 | name='django model 1', number=1, source=asm1) 11 | MySimpleModel.objects.create( 12 | name='django model 2', number=2, source=asm1) 13 | MySimpleModel.objects.create( 14 | name='django model 3', number=3, source=asm1) 15 | MySimpleModel.objects.create( 16 | name='django model 4', number=4, source=asm1) 17 | MySimpleModel.objects.create( 18 | name='django model 5', number=5, source=asm2) 19 | 20 | def test_readme_example_0(self): 21 | queryset = MySimpleModel.objects.all().order_by('id') 22 | 23 | self.assertTrue(queryset.count() == 5) 24 | 25 | res = queryset[0] 26 | 27 | self.assertTrue(res.name == 'django model 1') 28 | self.assertTrue(res.number == 1) 29 | self.assertTrue(res.source_id == 1) 30 | 31 | def test_readme_example_1(self): 32 | queryset = MySimpleModel.objects.from_raw( 33 | 'SELECT Null as id, "my str" as name, 111 as number, Null as source_id') 34 | 35 | queryset = queryset.filter(number__gte=10)\ 36 | .exclude(number__gte=1000)\ 37 | .filter(name__contains='s')\ 38 | .order_by('number')\ 39 | .select_related('source') 40 | 41 | res = queryset[0] 42 | 43 | self.assertTrue(res.name == 'my str') 44 | self.assertTrue(res.number == 111) 45 | 46 | def test_readme_example_2(self): 47 | queryset = MySimpleModel.objects.from_raw( 48 | 'SELECT "my str" as name, 111 as number', null_fields=['id', 'source_id']) 49 | 50 | queryset = queryset.filter(number__gte=10)\ 51 | .exclude(number__gte=1000)\ 52 | .filter(name__contains='s')\ 53 | .order_by('number')\ 54 | .select_related('source') 55 | 56 | res = queryset[0] 57 | 58 | self.assertTrue(res.name == 'my str') 59 | self.assertTrue(res.number == 111) 60 | 61 | def test_readme_example_3(self): 62 | queryset = MySimpleModel.objects.from_raw( 63 | 'SELECT %s as name, 111 as number', 64 | params=['my str'], 65 | null_fields=['id', 'source_id']) 66 | 67 | queryset = queryset.filter(number__gte=10)\ 68 | .exclude(number__gte=1000)\ 69 | .filter(name__contains='s')\ 70 | .order_by('number') 71 | 72 | res = queryset[0] 73 | 74 | self.assertTrue(res.name == 'my str') 75 | self.assertTrue(res.number == 111) 76 | 77 | def test_readme_example_4(self): 78 | queryset = MySimpleModel.objects.from_raw( 79 | 'SELECT %s as name, 111 as inner_number', 80 | params=['my str'], 81 | translations={'inner_number': 'number'}, 82 | null_fields=['id', 'source_id']) 83 | 84 | queryset = queryset.filter(number__gte=10)\ 85 | .exclude(number__gte=1000)\ 86 | .filter(name__contains='s')\ 87 | .order_by('number') 88 | 89 | res = queryset[0] 90 | 91 | self.assertTrue(res.name == 'my str') 92 | self.assertTrue(res.number == 111) 93 | 94 | def test_readme_example_5(self): 95 | queryset = MySimpleModel.my_raw_manager.all() 96 | 97 | queryset = queryset.filter(number__gte=10)\ 98 | .exclude(number__gte=1000)\ 99 | .filter(name__contains='s')\ 100 | .order_by('number') 101 | 102 | res = queryset[0] 103 | 104 | self.assertTrue(res.name == 'my str') 105 | self.assertTrue(res.number == 111) 106 | 107 | def test_readme_example_6(self): 108 | queryset = MySimpleModel.my_raw_manager_2.all() 109 | 110 | queryset = queryset.filter(number__gte=10)\ 111 | .exclude(number__gte=1000)\ 112 | .filter(name__contains='s')\ 113 | .order_by('number') 114 | 115 | res = queryset[0] 116 | 117 | self.assertTrue(res.name == 'my str') 118 | self.assertTrue(res.number == 111) 119 | 120 | def test_readme_example_7(self): 121 | queryset = MySimpleModel.my_callable_raw_manager('my str').all() 122 | 123 | queryset = queryset.filter(number__gte=10)\ 124 | .exclude(number__gte=1000)\ 125 | .filter(name__contains='s')\ 126 | .order_by('number') 127 | 128 | res = queryset[0] 129 | 130 | self.assertTrue(res.name == 'my str') 131 | self.assertTrue(res.number == 111) 132 | 133 | def test_decorated_manager_from_1(self): 134 | queryset = MySimpleModel.my_raw_manager_2.from_raw( 135 | 'SELECT %s as name, 111 as inner_number', 136 | params=['my str'], 137 | translations={'inner_number': 'number'}, 138 | null_fields=['id', 'source_id']) 139 | 140 | queryset = queryset.filter(number__gte=10)\ 141 | .exclude(number__gte=1000)\ 142 | .filter(name__contains='s')\ 143 | .order_by('number') 144 | 145 | res = queryset[0] 146 | 147 | self.assertTrue(res.name == 'my str') 148 | self.assertTrue(res.number == 111) 149 | 150 | def test_decorated_manager_from_2(self): 151 | queryset = MySimpleModel.my_callable_raw_manager.from_raw( 152 | 'SELECT %s as name, 111 as inner_number', 153 | params=['my str'], 154 | translations={'inner_number': 'number'}, 155 | null_fields=['id', 'source_id']) 156 | 157 | queryset = queryset.filter(number__gte=10)\ 158 | .exclude(number__gte=1000)\ 159 | .filter(name__contains='s')\ 160 | .order_by('number') 161 | 162 | res = queryset[0] 163 | 164 | self.assertTrue(res.name == 'my str') 165 | self.assertTrue(res.number == 111) 166 | 167 | def test_call_not_callable(self): 168 | try: 169 | MySimpleModel.my_raw_manager_2().all() 170 | except TypeError: 171 | return 172 | 173 | self.assertTrue(False) 174 | 175 | def test_query_callable_directly(self): 176 | try: 177 | MySimpleModel.my_callable_raw_manager.all()[0] 178 | except TypeError: 179 | return 180 | 181 | self.assertTrue(False) 182 | 183 | def test_deferred_params_1(self): 184 | queryset = MySimpleModel.my_callable_raw_manager.all()\ 185 | .with_params('my str') 186 | 187 | queryset = queryset.filter(number__gte=10)\ 188 | .exclude(number__gte=1000)\ 189 | .filter(name__contains='s')\ 190 | .order_by('number')\ 191 | .select_related('source') 192 | 193 | res = queryset[0] 194 | 195 | self.assertTrue(res.name == 'my str') 196 | self.assertTrue(res.number == 111) 197 | 198 | def test_deferred_params_2(self): 199 | queryset = MySimpleModel.objects.from_raw( 200 | 'SELECT Null as id, %s as name, 111 as number, Null as source_id')\ 201 | .with_params('my str') 202 | 203 | queryset = queryset.filter(number__gte=10)\ 204 | .exclude(number__gte=1000)\ 205 | .filter(name__contains='s')\ 206 | .order_by('number')\ 207 | .select_related('source') 208 | 209 | res = queryset[0] 210 | 211 | self.assertTrue(res.name == 'my str') 212 | self.assertTrue(res.number == 111) 213 | 214 | def test_from_queryset_1(self): 215 | queryset = MySimpleModel.my_qs_manager.all() 216 | 217 | queryset = queryset.filter(number__gte=10)\ 218 | .exclude(number__gte=1000)\ 219 | .order_by('source')\ 220 | .select_related('source') 221 | 222 | res = queryset[0] 223 | 224 | self.assertTrue(res.name is None) 225 | self.assertTrue(res.number == 10) 226 | --------------------------------------------------------------------------------