django-sql-compiler 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. django_sql_compiler-0.2.0/CHANGELOG.md +44 -0
  2. django_sql_compiler-0.2.0/LICENSE +21 -0
  3. django_sql_compiler-0.2.0/PKG-INFO +141 -0
  4. {django-sql-compiler-0.1.0 → django_sql_compiler-0.2.0}/README.md +20 -9
  5. django_sql_compiler-0.2.0/pyproject.toml +72 -0
  6. django_sql_compiler-0.2.0/sql_compiler/compilers/__init__.py +24 -0
  7. {django-sql-compiler-0.1.0 → django_sql_compiler-0.2.0}/sql_compiler/compilers/base.py +3 -0
  8. django_sql_compiler-0.2.0/sql_compiler/compilers/mysql.py +40 -0
  9. django_sql_compiler-0.2.0/sql_compiler/compilers/postgresql.py +43 -0
  10. django_sql_compiler-0.2.0/sql_compiler/queryset.py +40 -0
  11. {django-sql-compiler-0.1.0 → django_sql_compiler-0.2.0}/sql_compiler/utils.py +5 -7
  12. django-sql-compiler-0.1.0/PKG-INFO +0 -114
  13. django-sql-compiler-0.1.0/django_sql_compiler.egg-info/PKG-INFO +0 -114
  14. django-sql-compiler-0.1.0/django_sql_compiler.egg-info/SOURCES.txt +0 -16
  15. django-sql-compiler-0.1.0/django_sql_compiler.egg-info/dependency_links.txt +0 -1
  16. django-sql-compiler-0.1.0/django_sql_compiler.egg-info/not-zip-safe +0 -1
  17. django-sql-compiler-0.1.0/django_sql_compiler.egg-info/requires.txt +0 -1
  18. django-sql-compiler-0.1.0/django_sql_compiler.egg-info/top_level.txt +0 -1
  19. django-sql-compiler-0.1.0/setup.cfg +0 -4
  20. django-sql-compiler-0.1.0/setup.py +0 -29
  21. django-sql-compiler-0.1.0/sql_compiler/compilers/__init__.py +0 -13
  22. django-sql-compiler-0.1.0/sql_compiler/compilers/mysql.py +0 -9
  23. django-sql-compiler-0.1.0/sql_compiler/compilers/postgresql.py +0 -16
  24. django-sql-compiler-0.1.0/sql_compiler/queryset.py +0 -30
  25. {django-sql-compiler-0.1.0 → django_sql_compiler-0.2.0}/sql_compiler/__init__.py +0 -0
  26. {django-sql-compiler-0.1.0 → django_sql_compiler-0.2.0}/sql_compiler/manager.py +0 -0
@@ -0,0 +1,44 @@
1
+ # Changelog
2
+
3
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project uses [Semantic Versioning](https://semver.org/).
4
+
5
+ ## [0.2.0] - 2026-10-25
6
+
7
+ The first release since 0.1.1 (June 2021). It ships correctness fixes and first-time MySQL support.
8
+
9
+ ### Added
10
+
11
+ - MySQL support for `django.db.backends.mysql` (via mysqlclient), contributed in [PR 2](https://github.com/JaredStufftGD/django-sql-compiler/pull/2). It is covered by the live-database test suite and CI. In 0.1.1 the MySQL compiler was a stub and was not registered.
12
+
13
+ ### Fixed
14
+
15
+ - Non-ASCII parameters are decoded with the connection's encoding instead of byte by byte, so values like `café` are no longer garbled. 0.1.1 corrupted them.
16
+ - SQL is compiled for the queryset's own database alias (`.using()` or a database router) instead of the default alias.
17
+ - The unsupported-backend error names the ENGINE instead of saying "None backend is not supported".
18
+
19
+ ### Changed
20
+
21
+ - Decoding is strict. Compiler output that can't be decoded with the connection's codec raises `UnicodeDecodeError` (or `LookupError` for an unknown client encoding) instead of returning garbled text. A Postgres connection on `SQL_ASCII`, or another unusual client encoding, with non-ASCII data now raises where 0.1.1 returned text. A compiler that returns non-bytes raises `TypeError`.
22
+ - Semi-public API:
23
+ - `sql_compiler.utils.bytes_to_string` now requires an `encoding` argument.
24
+ - `BaseSQLCompiler` has a new `python_encoding(connection)` method that custom compiler subclasses must implement (the base raises `NotImplementedError`).
25
+ - `MySQLCompiler` is exported from `sql_compiler.compilers`.
26
+ - Install requirements: Python >=3.10 and Django >=4.2. The package declares no database driver (psycopg2 or mysqlclient comes from your Django setup). Effects on existing environments:
27
+ - With an older Django installed but not pinned, upgrading django-sql-compiler can upgrade Django (accepted, because Django <4.2 is end-of-life).
28
+ - With Django <4.2 pinned, or on Python <3.10, pip stays on 0.1.1.
29
+ - An install from git master already reports 0.2.3, so pip won't treat 0.2.0 as an upgrade. Install `django-sql-compiler==0.2.0` explicitly.
30
+
31
+ ### Packaging
32
+
33
+ - The first wheel release (0.1.1 was sdist-only).
34
+ - Built with flit_core instead of setuptools.
35
+ - The license is declared with PEP 639 `License-Expression: MIT`, and LICENSE ships in both artifacts.
36
+ - Metadata-Version 2.5.
37
+ - CHANGELOG.md ships in the sdist.
38
+
39
+ ### Known issues
40
+
41
+ - psycopg 3 is not supported yet. With psycopg 3 installed, Django uses it and `executable_query` raises (planned for 1.0). 0.1.1 had this issue too.
42
+ - `.none()` querysets raise `EmptyResultSet` (planned for 1.0). 0.1.1 had this issue too.
43
+
44
+ [0.2.0]: https://github.com/JaredStufftGD/django-sql-compiler/releases/tag/v0.2.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Grok Data
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.
@@ -0,0 +1,141 @@
1
+ Metadata-Version: 2.5
2
+ Name: django-sql-compiler
3
+ Version: 0.2.0
4
+ Summary: A light-weight module to generate usable SQL from a Django QuerySet.
5
+ Author-email: Grok Data / Jared Stufft <jared@grokdata.tech>
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-Expression: MIT
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Web Environment
11
+ Classifier: Framework :: Django
12
+ Classifier: Framework :: Django :: 4.2
13
+ Classifier: Framework :: Django :: 5.2
14
+ Classifier: Framework :: Django :: 6.0
15
+ Classifier: Framework :: Django :: 6.1
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Internet :: WWW/HTTP
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ License-File: LICENSE
26
+ Requires-Dist: Django>=4.2
27
+ Project-URL: Changelog, https://github.com/JaredStufftGD/django-sql-compiler/blob/master/CHANGELOG.md
28
+ Project-URL: Homepage, https://github.com/JaredStufftGD/django-sql-compiler
29
+ Project-URL: Issues, https://github.com/JaredStufftGD/django-sql-compiler/issues
30
+ Project-URL: Source, https://github.com/JaredStufftGD/django-sql-compiler
31
+ Import-Name: sql_compiler
32
+
33
+ # Django SQL Compiler
34
+
35
+ [![CI](https://github.com/JaredStufftGD/django-sql-compiler/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/JaredStufftGD/django-sql-compiler/actions/workflows/ci.yml)
36
+
37
+ A light-weight module to generate usable SQL from a Django QuerySet.
38
+
39
+ ## Backend Support
40
+ `django-sql-compiler` supports connections made via these Django `ENGINE` values:
41
+
42
+ - `django.db.backends.postgresql`
43
+ - `django.db.backends.postgresql_psycopg2`: a legacy alias for the PostgreSQL backend that Django removed in Django 3.0. The package still accepts the ENGINE string and maps it to the same compiler.
44
+ - `django_tenants.postgresql_backend` (django-tenants)
45
+ - `django.db.backends.mysql` (via mysqlclient)
46
+
47
+ PostgreSQL connections currently require psycopg2; psycopg 3 isn't supported yet. If psycopg 3 is installed, Django uses it and `executable_query` raises.
48
+
49
+ ## About Django SQL Compiler
50
+ The Django ORM is very useful for abstracting away SQL queries from the focus of the developer. This is very useful
51
+ for preventing SQL injection attacks and generating queries programmatically using applied logic in your Django app.
52
+
53
+ However, very complex queries (such as those used in reporting, analytics, or data science projects) can be difficult
54
+ or impossible to create with the Django ORM alone. The ORM provides the `.raw` query method and exposes the raw database
55
+ `connection` objects which can be used to execute arbitrary SQL against the database. In doing so, we lose the benefit
56
+ of the ORM with respect to dynamically adding components to the query (such as filters in a `WHERE` clause) in a way that
57
+ prevents injection attacks.
58
+
59
+ The base Django `QuerySet` object has a `Query` object available at the `.query` property. Casting this `Query` object as a `str`
60
+ prints out what looks like a valid SQL query. However, this version of the query is not properly escaped or quoted, meaning
61
+ it's not **actually** valid SQL unless there are no dynamic components (such as filters from user input) in the `QuerySet`.
62
+
63
+ `django-sql-compiler` aims to provide a way to generate clean, usable SQL from a given `QuerySet`, which can be used in
64
+ tandem with a raw SQL query to give SQL users more flexibility in querying their Django-connected database while still
65
+ retaining the Django ORM for security and dynamic query generation purposes.
66
+
67
+ ## Usage
68
+ Install the package:
69
+ ```
70
+ pip install django-sql-compiler
71
+ ```
72
+
73
+ No database driver is installed with the package and there are no extras: it uses the database connection your Django project already has, so the driver (for example psycopg2 or mysqlclient) comes from your existing Django setup.
74
+
75
+ Add `sql_compiler` to your `INSTALLED_APPS`:
76
+
77
+ ```python
78
+ INSTALLED_APPS = [
79
+ ...,
80
+ 'sql_compiler'
81
+ ]
82
+ ```
83
+
84
+ Add the `SQLCompilerManager` to your models:
85
+
86
+ ```python
87
+ from django.db import models
88
+ from sql_compiler import SQLCompilerManager
89
+
90
+
91
+ class MyModel(models.Model):
92
+
93
+ objects = SQLCompilerManager()
94
+ field_one = models.IntegerField()
95
+ field_two = models.CharField(max_length=200)
96
+ field_three = models.DateTimeField()
97
+
98
+ ```
99
+
100
+ For a given `QuerySet` on the model, you can access the executable query be accessing the `.executable_query` property of the `QuerySet`.
101
+
102
+ ```python
103
+ query_set = MyModel.objects.filter(field_one__lte=10)
104
+ executable_query = query_set.executable_query
105
+ print(executable_query)
106
+ ```
107
+
108
+ This query can then be used in another query as a Common Table Expression or Subquery:
109
+
110
+ ```python
111
+ # Basic usage
112
+ wrapper_query = """select * from ({}) a""".format(executable_query)
113
+ new_queryset = MyModel.objects.raw(wrapper_query)
114
+
115
+ ```
116
+
117
+ While this is a very simple example, you can use this in more complicated queries:
118
+ ```python
119
+
120
+ # inside a view
121
+ filtered_query_set = MyModel.objects.filter(
122
+ field_one__lte=request.data.get('field_one_filter', 0),
123
+ field_two__in=request.data.get('field_two_filter', [])
124
+ )
125
+
126
+
127
+ more_complex_query = """
128
+ select
129
+ RANK() over (partition by a.field_one order by a.field_three desc) as rnk
130
+ ,a.field_one
131
+ ,a.field_two
132
+ from
133
+ ({}) a
134
+ """.format(filtered_query_set.executable_query)
135
+
136
+ more_complex_results = MyModel.objects.raw(more_complex_query)
137
+ ```
138
+
139
+ Now, users who are more familiar with SQL rather than the Django ORM can use the ORM for security and conveniently
140
+ generating SQL queries and use SQL for the rest of their transformations.
141
+
@@ -1,26 +1,35 @@
1
1
  # Django SQL Compiler
2
2
 
3
+ [![CI](https://github.com/JaredStufftGD/django-sql-compiler/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/JaredStufftGD/django-sql-compiler/actions/workflows/ci.yml)
4
+
3
5
  A light-weight module to generate usable SQL from a Django QuerySet.
4
6
 
5
7
  ## Backend Support
6
- Currently, `django-sql-compiler` only supports connections made via the `django.db.backends.postgresql` backend.
8
+ `django-sql-compiler` supports connections made via these Django `ENGINE` values:
9
+
10
+ - `django.db.backends.postgresql`
11
+ - `django.db.backends.postgresql_psycopg2`: a legacy alias for the PostgreSQL backend that Django removed in Django 3.0. The package still accepts the ENGINE string and maps it to the same compiler.
12
+ - `django_tenants.postgresql_backend` (django-tenants)
13
+ - `django.db.backends.mysql` (via mysqlclient)
14
+
15
+ PostgreSQL connections currently require psycopg2; psycopg 3 isn't supported yet. If psycopg 3 is installed, Django uses it and `executable_query` raises.
7
16
 
8
17
  ## About Django SQL Compiler
9
18
  The Django ORM is very useful for abstracting away SQL queries from the focus of the developer. This is very useful
10
19
  for preventing SQL injection attacks and generating queries programmatically using applied logic in your Django app.
11
20
 
12
21
  However, very complex queries (such as those used in reporting, analytics, or data science projects) can be difficult
13
- or impossible to create with the Django ORM alone. The ORM provides the `.raw` query method and exposes the raw database
22
+ or impossible to create with the Django ORM alone. The ORM provides the `.raw` query method and exposes the raw database
14
23
  `connection` objects which can be used to execute arbitrary SQL against the database. In doing so, we lose the benefit
15
- of the ORM with respect to dynamically adding components to the query (such as filters in a `WHERE` clause) in a way that
24
+ of the ORM with respect to dynamically adding components to the query (such as filters in a `WHERE` clause) in a way that
16
25
  prevents injection attacks.
17
26
 
18
- The base Django `QuerySet` object has a `Query` object available at the `.query` property. Casting this `Query` object as a `str`
19
- prints out what looks like a valid SQL query. However, this version of the query is not properly escaped or quoted, meaning
27
+ The base Django `QuerySet` object has a `Query` object available at the `.query` property. Casting this `Query` object as a `str`
28
+ prints out what looks like a valid SQL query. However, this version of the query is not properly escaped or quoted, meaning
20
29
  it's not **actually** valid SQL unless there are no dynamic components (such as filters from user input) in the `QuerySet`.
21
30
 
22
- `django-sql-compiler` aims to provide a way to generate clean, usable SQL from a given `QuerySet`, which can be used in
23
- tandem with a raw SQL query to give SQL users more flexibility in querying their Django-connected database while still
31
+ `django-sql-compiler` aims to provide a way to generate clean, usable SQL from a given `QuerySet`, which can be used in
32
+ tandem with a raw SQL query to give SQL users more flexibility in querying their Django-connected database while still
24
33
  retaining the Django ORM for security and dynamic query generation purposes.
25
34
 
26
35
  ## Usage
@@ -29,6 +38,8 @@ Install the package:
29
38
  pip install django-sql-compiler
30
39
  ```
31
40
 
41
+ No database driver is installed with the package and there are no extras: it uses the database connection your Django project already has, so the driver (for example psycopg2 or mysqlclient) comes from your existing Django setup.
42
+
32
43
  Add `sql_compiler` to your `INSTALLED_APPS`:
33
44
 
34
45
  ```python
@@ -79,7 +90,7 @@ filtered_query_set = MyModel.objects.filter(
79
90
  field_one__lte=request.data.get('field_one_filter', 0),
80
91
  field_two__in=request.data.get('field_two_filter', [])
81
92
  )
82
-
93
+
83
94
 
84
95
  more_complex_query = """
85
96
  select
@@ -94,4 +105,4 @@ more_complex_results = MyModel.objects.raw(more_complex_query)
94
105
  ```
95
106
 
96
107
  Now, users who are more familiar with SQL rather than the Django ORM can use the ORM for security and conveniently
97
- generating SQL queries and use SQL for the rest of their transformations.
108
+ generating SQL queries and use SQL for the rest of their transformations.
@@ -0,0 +1,72 @@
1
+ [project]
2
+ name = "django-sql-compiler"
3
+ version = "0.2.0"
4
+ description = "A light-weight module to generate usable SQL from a Django QuerySet."
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Grok Data / Jared Stufft", email = "jared@grokdata.tech" },
8
+ ]
9
+ license = "MIT"
10
+ license-files = ["LICENSE"]
11
+ requires-python = ">=3.10"
12
+ dependencies = [
13
+ "Django>=4.2",
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Environment :: Web Environment",
18
+ "Framework :: Django",
19
+ "Framework :: Django :: 4.2",
20
+ "Framework :: Django :: 5.2",
21
+ "Framework :: Django :: 6.0",
22
+ "Framework :: Django :: 6.1",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3 :: Only",
25
+ "Programming Language :: Python :: 3.10",
26
+ "Programming Language :: Python :: 3.11",
27
+ "Programming Language :: Python :: 3.12",
28
+ "Programming Language :: Python :: 3.13",
29
+ "Programming Language :: Python :: 3.14",
30
+ "Topic :: Internet :: WWW/HTTP",
31
+ "Topic :: Software Development :: Libraries :: Python Modules",
32
+ ]
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/JaredStufftGD/django-sql-compiler"
36
+ Source = "https://github.com/JaredStufftGD/django-sql-compiler"
37
+ Changelog = "https://github.com/JaredStufftGD/django-sql-compiler/blob/master/CHANGELOG.md"
38
+ Issues = "https://github.com/JaredStufftGD/django-sql-compiler/issues"
39
+
40
+ [dependency-groups]
41
+ dev = [
42
+ "pytest>=8",
43
+ ]
44
+ pg2 = [
45
+ "psycopg2-binary>=2.9.9",
46
+ ]
47
+ pg3 = [
48
+ "psycopg[binary]>=3.2",
49
+ ]
50
+ mysql = [
51
+ "mysqlclient>=2.2.1",
52
+ ]
53
+
54
+ [tool.uv]
55
+ conflicts = [
56
+ [{ group = "pg2" }, { group = "pg3" }],
57
+ ]
58
+
59
+ [build-system]
60
+ requires = ["flit_core>=4,<5"]
61
+ build-backend = "flit_core.buildapi"
62
+
63
+ [tool.flit.module]
64
+ name = "sql_compiler"
65
+
66
+ [tool.flit.sdist]
67
+ include = ["CHANGELOG.md"]
68
+
69
+ [tool.pytest.ini_options]
70
+ testpaths = ["tests"]
71
+ xfail_strict = true
72
+ addopts = "-ra"
@@ -0,0 +1,24 @@
1
+ from .postgresql import PostgresCompiler, PostgresTenantsCompiler, Psycopg2Compiler
2
+ from .mysql import MySQLCompiler
3
+
4
+ __all__ = [
5
+ 'MySQLCompiler',
6
+ 'PostgresCompiler',
7
+ 'PostgresTenantsCompiler',
8
+ 'Psycopg2Compiler',
9
+ 'compiler_map',
10
+ 'SUPPORTED_BACKENDS'
11
+ ]
12
+
13
+ compilers = [
14
+ MySQLCompiler,
15
+ PostgresCompiler,
16
+ PostgresTenantsCompiler,
17
+ Psycopg2Compiler
18
+ ]
19
+
20
+ compiler_map = {
21
+ compiler.backend_name: compiler for compiler in compilers
22
+ }
23
+
24
+ SUPPORTED_BACKENDS = compiler_map.keys()
@@ -6,3 +6,6 @@ class BaseSQLCompiler:
6
6
 
7
7
  def compile_sql(self, connection, query, params):
8
8
  raise NotImplementedError('Subclasses must implement compile_sql')
9
+
10
+ def python_encoding(self, connection):
11
+ raise NotImplementedError('Subclasses must implement python_encoding')
@@ -0,0 +1,40 @@
1
+ import codecs
2
+
3
+ from django.db.utils import ProgrammingError
4
+
5
+ from .base import BaseSQLCompiler
6
+
7
+
8
+ class MySQLCompiler(BaseSQLCompiler):
9
+
10
+ backend_name = 'django.db.backends.mysql'
11
+
12
+ def compile_sql(self, connection, query, params):
13
+ with connection.cursor() as cursor:
14
+ compiled_sql = self.mogrify(cursor, query, params)
15
+ return compiled_sql
16
+
17
+ def python_encoding(self, connection):
18
+ return codecs.lookup(connection.connection.encoding).name
19
+
20
+ def mogrify(self, cursor, query, args=None):
21
+ db = cursor.connection
22
+ if isinstance(query, str):
23
+ query = query.encode(db.encoding)
24
+
25
+ if args is not None:
26
+ if isinstance(args, dict):
27
+ nargs = {}
28
+ for key, item in args.items():
29
+ if isinstance(key, str):
30
+ key = key.encode(db.encoding)
31
+ nargs[key] = db.literal(item)
32
+ args = nargs
33
+ else:
34
+ args = tuple(map(db.literal, args))
35
+ try:
36
+ query = query % args
37
+ except (TypeError, KeyError, ValueError) as m:
38
+ raise ProgrammingError(str(m)) from m
39
+
40
+ return query
@@ -0,0 +1,43 @@
1
+ import codecs
2
+
3
+ from .base import BaseSQLCompiler
4
+
5
+
6
+ class PostgresCompiler(BaseSQLCompiler):
7
+
8
+ """
9
+ Compiles an executable SQL query from a parameterized query and arg list using the postgresql/psycopg2 backend.
10
+ Also reports the raw connection's client encoding as a Python codec name, for psycopg2 and psycopg 3.
11
+ """
12
+
13
+ backend_name = 'django.db.backends.postgresql'
14
+
15
+ def encoding_map(self):
16
+ from psycopg2.extensions import encodings
17
+ return encodings
18
+
19
+ def python_encoding(self, connection):
20
+ raw = connection.connection
21
+ info = getattr(raw, 'info', None)
22
+ if info is not None and hasattr(info, 'encoding'):
23
+ # psycopg 3 already reports a Python codec name
24
+ return codecs.lookup(info.encoding).name
25
+ name = raw.encoding
26
+ try:
27
+ return self.encoding_map()[name]
28
+ except KeyError:
29
+ raise LookupError('unknown PostgreSQL client encoding: {}'.format(name)) from None
30
+
31
+ def compile_sql(self, connection, query, params):
32
+ with connection.cursor() as cursor:
33
+ compiled_sql = cursor.mogrify(query, params)
34
+
35
+ return compiled_sql
36
+
37
+
38
+ class PostgresTenantsCompiler(PostgresCompiler):
39
+ backend_name = 'django_tenants.postgresql_backend'
40
+
41
+
42
+ class Psycopg2Compiler(PostgresCompiler):
43
+ backend_name = 'django.db.backends.postgresql_psycopg2'
@@ -0,0 +1,40 @@
1
+ from django.db import connections
2
+ from django.db.models import QuerySet
3
+ from django.utils.functional import cached_property
4
+ from . import utils
5
+ from . import compilers
6
+
7
+
8
+ class SQLCompilerQuerySet(QuerySet):
9
+
10
+ @cached_property
11
+ def executable_query(self):
12
+
13
+ """
14
+ Takes the parameterized SQL query with the arg list and compiles the query so
15
+ that it can be used anywhere - as a subquery in a Raw SQL query, in a SQL GUI interface, etc.
16
+
17
+ The SQL is built for the queryset's own database alias, so the dialect and the
18
+ escaping come from the same database.
19
+
20
+ The result is decoded with the encoding the driver reports for this connection,
21
+ and undecodable output raises instead of being garbled.
22
+ """
23
+
24
+ alias = self.db
25
+ connection = utils.validate_connection(connections[alias])
26
+
27
+ if not connection:
28
+ raise NotImplementedError('{} backend is not supported.'.format(connections[alias].settings_dict['ENGINE']))
29
+
30
+ query, params = self.query.get_compiler(using=alias).as_sql()
31
+
32
+ backend_name = connection.settings_dict['ENGINE']
33
+ compiler = compilers.compiler_map[backend_name]()
34
+ compiled_query = compiler.compile_sql(connection, query, params)
35
+ if not isinstance(compiled_query, (bytes, bytearray)):
36
+ raise TypeError('compile_sql must return bytes, got {}'.format(type(compiled_query).__name__))
37
+ encoding = compiler.python_encoding(connection)
38
+ compiled_query = utils.bytes_to_string(compiled_query, encoding)
39
+
40
+ return compiled_query
@@ -11,18 +11,16 @@ def validate_connection(connection):
11
11
  """
12
12
 
13
13
  backend_name = connection.settings_dict['ENGINE']
14
-
15
- if backend_name in ('django.db.backends.postgresql_psycopg2', 'django_tenants.postgresql_backend'):
16
- backend_name = 'django.db.backends.postgresql'
17
-
18
14
  if backend_name in SUPPORTED_BACKENDS:
19
15
  return connection
20
16
 
21
17
 
22
- def bytes_to_string(bytes_obj):
18
+ def bytes_to_string(bytes_obj, encoding):
23
19
 
24
20
  """
25
- Converts a bytes object to a string.
21
+ Decodes a bytes object to a string with the Python codec the connection's driver reports.
22
+
23
+ Decoding is strict: LookupError (unknown codec) and UnicodeDecodeError (invalid bytes) propagate.
26
24
  """
27
25
 
28
- return "".join([chr(c) for c in bytes_obj])
26
+ return bytes_obj.decode(encoding)
@@ -1,114 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: django-sql-compiler
3
- Version: 0.1.0
4
- Summary: A light-weight module to generate usable SQL from a Django QuerySet.
5
- Home-page: https://github.com/GrokData/django-sql-compiler
6
- Author: Grok Data / Jared Stufft
7
- Author-email: jared@grokdata.tech
8
- License: MIT
9
- Description: # Django SQL Compiler
10
-
11
- A light-weight module to generate usable SQL from a Django QuerySet.
12
-
13
- ## Backend Support
14
- Currently, `django-sql-compiler` only supports connections made via the `django.db.backends.postgresql` backend.
15
-
16
- ## About Django SQL Compiler
17
- The Django ORM is very useful for abstracting away SQL queries from the focus of the developer. This is very useful
18
- for preventing SQL injection attacks and generating queries programmatically using applied logic in your Django app.
19
-
20
- However, very complex queries (such as those used in reporting, analytics, or data science projects) can be difficult
21
- or impossible to create with the Django ORM alone. The ORM provides the `.raw` query method and exposes the raw database
22
- `connection` objects which can be used to execute arbitrary SQL against the database. In doing so, we lose the benefit
23
- of the ORM with respect to dynamically adding components to the query (such as filters in a `WHERE` clause) in a way that
24
- prevents injection attacks.
25
-
26
- The base Django `QuerySet` object has a `Query` object available at the `.query` property. Casting this `Query` object as a `str`
27
- prints out what looks like a valid SQL query. However, this version of the query is not properly escaped or quoted, meaning
28
- it's not **actually** valid SQL unless there are no dynamic components (such as filters from user input) in the `QuerySet`.
29
-
30
- `django-sql-compiler` aims to provide a way to generate clean, usable SQL from a given `QuerySet`, which can be used in
31
- tandem with a raw SQL query to give SQL users more flexibility in querying their Django-connected database while still
32
- retaining the Django ORM for security and dynamic query generation purposes.
33
-
34
- ## Usage
35
- Install the package:
36
- ```
37
- pip install django-sql-compiler
38
- ```
39
-
40
- Add `sql_compiler` to your `INSTALLED_APPS`:
41
-
42
- ```python
43
- INSTALLED_APPS = [
44
- ...,
45
- 'sql_compiler'
46
- ]
47
- ```
48
-
49
- Add the `SQLCompilerManager` to your models:
50
-
51
- ```python
52
- from django.db import models
53
- from sql_compiler import SQLCompilerManager
54
-
55
-
56
- class MyModel(models.Model):
57
-
58
- objects = SQLCompilerManager()
59
- field_one = models.IntegerField()
60
- field_two = models.CharField(max_length=200)
61
- field_three = models.DateTimeField()
62
-
63
- ```
64
-
65
- For a given `QuerySet` on the model, you can access the executable query be accessing the `.executable_query` property of the `QuerySet`.
66
-
67
- ```python
68
- query_set = MyModel.objects.filter(field_one__lte=10)
69
- executable_query = query_set.executable_query
70
- print(executable_query)
71
- ```
72
-
73
- This query can then be used in another query as a Common Table Expression or Subquery:
74
-
75
- ```python
76
- # Basic usage
77
- wrapper_query = """select * from ({}) a""".format(executable_query)
78
- new_queryset = MyModel.objects.raw(wrapper_query)
79
-
80
- ```
81
-
82
- While this is a very simple example, you can use this in more complicated queries:
83
- ```python
84
-
85
- # inside a view
86
- filtered_query_set = MyModel.objects.filter(
87
- field_one__lte=request.data.get('field_one_filter', 0),
88
- field_two__in=request.data.get('field_two_filter', [])
89
- )
90
-
91
-
92
- more_complex_query = """
93
- select
94
- RANK() over (partition by a.field_one order by a.field_three desc) as rnk
95
- ,a.field_one
96
- ,a.field_two
97
- from
98
- ({}) a
99
- """.format(filtered_query_set.executable_query)
100
-
101
- more_complex_results = MyModel.objects.raw(more_complex_query)
102
- ```
103
-
104
- Now, users who are more familiar with SQL rather than the Django ORM can use the ORM for security and conveniently
105
- generating SQL queries and use SQL for the rest of their transformations.
106
- Platform: UNKNOWN
107
- Classifier: Development Status :: 3 - Alpha
108
- Classifier: Environment :: Web Environment
109
- Classifier: Framework :: Django
110
- Classifier: License :: OSI Approved :: MIT License
111
- Classifier: Programming Language :: Python :: 3
112
- Classifier: Topic :: Internet :: WWW/HTTP
113
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
114
- Description-Content-Type: text/markdown
@@ -1,114 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: django-sql-compiler
3
- Version: 0.1.0
4
- Summary: A light-weight module to generate usable SQL from a Django QuerySet.
5
- Home-page: https://github.com/GrokData/django-sql-compiler
6
- Author: Grok Data / Jared Stufft
7
- Author-email: jared@grokdata.tech
8
- License: MIT
9
- Description: # Django SQL Compiler
10
-
11
- A light-weight module to generate usable SQL from a Django QuerySet.
12
-
13
- ## Backend Support
14
- Currently, `django-sql-compiler` only supports connections made via the `django.db.backends.postgresql` backend.
15
-
16
- ## About Django SQL Compiler
17
- The Django ORM is very useful for abstracting away SQL queries from the focus of the developer. This is very useful
18
- for preventing SQL injection attacks and generating queries programmatically using applied logic in your Django app.
19
-
20
- However, very complex queries (such as those used in reporting, analytics, or data science projects) can be difficult
21
- or impossible to create with the Django ORM alone. The ORM provides the `.raw` query method and exposes the raw database
22
- `connection` objects which can be used to execute arbitrary SQL against the database. In doing so, we lose the benefit
23
- of the ORM with respect to dynamically adding components to the query (such as filters in a `WHERE` clause) in a way that
24
- prevents injection attacks.
25
-
26
- The base Django `QuerySet` object has a `Query` object available at the `.query` property. Casting this `Query` object as a `str`
27
- prints out what looks like a valid SQL query. However, this version of the query is not properly escaped or quoted, meaning
28
- it's not **actually** valid SQL unless there are no dynamic components (such as filters from user input) in the `QuerySet`.
29
-
30
- `django-sql-compiler` aims to provide a way to generate clean, usable SQL from a given `QuerySet`, which can be used in
31
- tandem with a raw SQL query to give SQL users more flexibility in querying their Django-connected database while still
32
- retaining the Django ORM for security and dynamic query generation purposes.
33
-
34
- ## Usage
35
- Install the package:
36
- ```
37
- pip install django-sql-compiler
38
- ```
39
-
40
- Add `sql_compiler` to your `INSTALLED_APPS`:
41
-
42
- ```python
43
- INSTALLED_APPS = [
44
- ...,
45
- 'sql_compiler'
46
- ]
47
- ```
48
-
49
- Add the `SQLCompilerManager` to your models:
50
-
51
- ```python
52
- from django.db import models
53
- from sql_compiler import SQLCompilerManager
54
-
55
-
56
- class MyModel(models.Model):
57
-
58
- objects = SQLCompilerManager()
59
- field_one = models.IntegerField()
60
- field_two = models.CharField(max_length=200)
61
- field_three = models.DateTimeField()
62
-
63
- ```
64
-
65
- For a given `QuerySet` on the model, you can access the executable query be accessing the `.executable_query` property of the `QuerySet`.
66
-
67
- ```python
68
- query_set = MyModel.objects.filter(field_one__lte=10)
69
- executable_query = query_set.executable_query
70
- print(executable_query)
71
- ```
72
-
73
- This query can then be used in another query as a Common Table Expression or Subquery:
74
-
75
- ```python
76
- # Basic usage
77
- wrapper_query = """select * from ({}) a""".format(executable_query)
78
- new_queryset = MyModel.objects.raw(wrapper_query)
79
-
80
- ```
81
-
82
- While this is a very simple example, you can use this in more complicated queries:
83
- ```python
84
-
85
- # inside a view
86
- filtered_query_set = MyModel.objects.filter(
87
- field_one__lte=request.data.get('field_one_filter', 0),
88
- field_two__in=request.data.get('field_two_filter', [])
89
- )
90
-
91
-
92
- more_complex_query = """
93
- select
94
- RANK() over (partition by a.field_one order by a.field_three desc) as rnk
95
- ,a.field_one
96
- ,a.field_two
97
- from
98
- ({}) a
99
- """.format(filtered_query_set.executable_query)
100
-
101
- more_complex_results = MyModel.objects.raw(more_complex_query)
102
- ```
103
-
104
- Now, users who are more familiar with SQL rather than the Django ORM can use the ORM for security and conveniently
105
- generating SQL queries and use SQL for the rest of their transformations.
106
- Platform: UNKNOWN
107
- Classifier: Development Status :: 3 - Alpha
108
- Classifier: Environment :: Web Environment
109
- Classifier: Framework :: Django
110
- Classifier: License :: OSI Approved :: MIT License
111
- Classifier: Programming Language :: Python :: 3
112
- Classifier: Topic :: Internet :: WWW/HTTP
113
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
114
- Description-Content-Type: text/markdown
@@ -1,16 +0,0 @@
1
- README.md
2
- setup.py
3
- django_sql_compiler.egg-info/PKG-INFO
4
- django_sql_compiler.egg-info/SOURCES.txt
5
- django_sql_compiler.egg-info/dependency_links.txt
6
- django_sql_compiler.egg-info/not-zip-safe
7
- django_sql_compiler.egg-info/requires.txt
8
- django_sql_compiler.egg-info/top_level.txt
9
- sql_compiler/__init__.py
10
- sql_compiler/manager.py
11
- sql_compiler/queryset.py
12
- sql_compiler/utils.py
13
- sql_compiler/compilers/__init__.py
14
- sql_compiler/compilers/base.py
15
- sql_compiler/compilers/mysql.py
16
- sql_compiler/compilers/postgresql.py
@@ -1,4 +0,0 @@
1
- [egg_info]
2
- tag_build =
3
- tag_date = 0
4
-
@@ -1,29 +0,0 @@
1
- from setuptools import setup, find_packages
2
-
3
-
4
- setup(
5
- name='django-sql-compiler',
6
- version='0.1.0',
7
- description="A light-weight module to generate usable SQL from a Django QuerySet.",
8
- long_description=open('README.md').read(),
9
- long_description_content_type='text/markdown',
10
- author='Grok Data / Jared Stufft',
11
- author_email='jared@grokdata.tech',
12
- url='https://github.com/GrokData/django-sql-compiler',
13
- license='MIT',
14
- packages=find_packages(),
15
- classifiers=[
16
- "Development Status :: 3 - Alpha",
17
- "Environment :: Web Environment",
18
- "Framework :: Django",
19
- "License :: OSI Approved :: MIT License",
20
- "Programming Language :: Python :: 3",
21
- "Topic :: Internet :: WWW/HTTP",
22
- "Topic :: Software Development :: Libraries :: Python Modules",
23
- ],
24
- install_requires=[
25
- 'django',
26
- ],
27
- include_package_data=True,
28
- zip_safe=False,
29
- )
@@ -1,13 +0,0 @@
1
- from .postgresql import PostgresCompiler
2
-
3
- __all__ = ['PostgresCompiler', 'compiler_map', 'SUPPORTED_BACKENDS']
4
-
5
- compilers = [
6
- PostgresCompiler
7
- ]
8
-
9
- compiler_map = {
10
- compiler.backend_name: compiler for compiler in compilers
11
- }
12
-
13
- SUPPORTED_BACKENDS = compiler_map.keys()
@@ -1,9 +0,0 @@
1
- from .base import BaseSQLCompiler
2
-
3
-
4
- class MySQLCompiler(BaseSQLCompiler):
5
-
6
- backend_name = 'django.db.backends.mysql'
7
-
8
- def compile_sql(self, connection, query, params):
9
- pass
@@ -1,16 +0,0 @@
1
- from .base import BaseSQLCompiler
2
-
3
-
4
- class PostgresCompiler(BaseSQLCompiler):
5
-
6
- """
7
- Compiles an executable SQL query from a parameterized query and arg list using the postgresql/psycopg2 backend.
8
- """
9
-
10
- backend_name = 'django.db.backends.postgresql'
11
-
12
- def compile_sql(self, connection, query, params):
13
- with connection.cursor() as cursor:
14
- compiled_sql = cursor.mogrify(query, params)
15
-
16
- return compiled_sql
@@ -1,30 +0,0 @@
1
- from django.db import connections
2
- from django.db.models import QuerySet
3
- from django.utils.functional import cached_property
4
- from . import utils
5
- from . import compilers
6
-
7
-
8
- class SQLCompilerQuerySet(QuerySet):
9
-
10
- @cached_property
11
- def executable_query(self):
12
-
13
- """
14
- Takes the parameterized SQL query with the arg list and compiles the query so
15
- that it can be used anywhere - as a subquery in a Raw SQL query, in a SQL GUI interface, etc.
16
- """
17
-
18
- connection = utils.validate_connection(connections[self.db])
19
-
20
- if not connection:
21
- raise NotImplementedError('{} backend is not supported.'.format(str(connection)))
22
-
23
- query, params = self.query.sql_with_params()
24
-
25
- backend_name = connection.settings_dict['ENGINE']
26
- compiler = compilers.compiler_map[backend_name]()
27
- compiled_query = compiler.compile_sql(connection, query, params)
28
- compiled_query = utils.bytes_to_string(compiled_query)
29
-
30
- return compiled_query