|
| 1 | +import base64 |
1 | 2 | import contextlib |
2 | 3 | import contextvars |
3 | 4 | import datetime |
4 | | -import json |
5 | 5 | from time import perf_counter |
6 | 6 |
|
7 | 7 | import django.test.testcases |
8 | 8 | from django.apps import apps |
| 9 | +from django.core.exceptions import ImproperlyConfigured |
9 | 10 |
|
10 | 11 | from debug_toolbar import settings as dt_settings |
11 | 12 | from debug_toolbar.sanitize import force_str |
12 | 13 | from debug_toolbar.utils import get_stack_trace, get_template_info |
13 | 14 |
|
| 15 | +Psycopg3Binary = None |
| 16 | + |
14 | 17 | try: |
15 | 18 | import psycopg |
16 | 19 |
|
17 | 20 | PostgresJson = psycopg.types.json.Jsonb |
18 | 21 | STATUS_IN_TRANSACTION = psycopg.pq.TransactionStatus.INTRANS |
| 22 | + Psycopg3Binary = psycopg.Binary |
19 | 23 | except ImportError: |
20 | 24 | try: |
21 | 25 | from psycopg2._json import Json as PostgresJson |
|
24 | 28 | PostgresJson = None |
25 | 29 | STATUS_IN_TRANSACTION = None |
26 | 30 |
|
| 31 | +try: |
| 32 | + from psycopg2.extensions import Binary as Psycopg2Binary |
| 33 | +except ImportError: |
| 34 | + Psycopg2Binary = None |
| 35 | + |
| 36 | +_PostGISAdapter = None |
| 37 | +try: |
| 38 | + from django.contrib.gis.db.backends.postgis.adapter import ( |
| 39 | + PostGISAdapter as _PostGISAdapter, |
| 40 | + ) |
| 41 | +except (ImportError, ImproperlyConfigured): |
| 42 | + pass |
| 43 | + |
27 | 44 | # Prevents SQL queries from being sent to the DB. It's used |
28 | 45 | # by the TemplatePanel to prevent the toolbar from issuing |
29 | 46 | # additional queries. |
@@ -133,6 +150,26 @@ def _decode(self, param): |
133 | 150 | if isinstance(param, dict): |
134 | 151 | return {key: self._decode(value) for key, value in param.items()} |
135 | 152 |
|
| 153 | + # GeoDjango PostGIS geometry parameters: extract EWKB bytes and metadata |
| 154 | + # so the adapter can be reconstructed on the way back out for SELECT/EXPLAIN. |
| 155 | + if _PostGISAdapter is not None and isinstance(param, _PostGISAdapter): |
| 156 | + return { |
| 157 | + "__djdt_postgis__": base64.b64encode(param.ewkb).decode("ascii"), |
| 158 | + "is_geometry": param.is_geometry, |
| 159 | + "geography": param.geography, |
| 160 | + } |
| 161 | + |
| 162 | + # Binary data is handled by DebugToolbarJSONEncoder in store.py. |
| 163 | + # Django's BinaryField calls connection.Database.Binary() which wraps |
| 164 | + # bytes in a driver-specific adapter: psycopg2 uses .adapted, psycopg3 |
| 165 | + # uses .obj. memoryview: psycopg2/sqlite3 binary column values. |
| 166 | + if isinstance(param, (bytes, bytearray, memoryview)): |
| 167 | + return bytes(param) |
| 168 | + if Psycopg2Binary is not None and isinstance(param, Psycopg2Binary): |
| 169 | + return bytes(param.adapted) |
| 170 | + if Psycopg3Binary is not None and isinstance(param, Psycopg3Binary): |
| 171 | + return bytes(param.obj) |
| 172 | + |
136 | 173 | # make sure datetime, date and time are converted to string by force_str |
137 | 174 | CONVERT_TYPES = (datetime.datetime, datetime.date, datetime.time) |
138 | 175 | return force_str(param, strings_only=not isinstance(param, CONVERT_TYPES)) |
@@ -165,10 +202,11 @@ def _record(self, method, sql, params): |
165 | 202 | finally: |
166 | 203 | stop_time = perf_counter() |
167 | 204 | duration = (stop_time - start_time) * 1000 |
168 | | - _params = "" |
| 205 | + _params = None |
169 | 206 | with contextlib.suppress(TypeError): |
170 | | - # object JSON serializable? |
171 | | - _params = json.dumps(self._decode(params)) |
| 207 | + # Decode params - binary data will be handled by DebugToolbarJSONEncoder |
| 208 | + # in store.py when the panel data is serialized |
| 209 | + _params = self._decode(params) |
172 | 210 | template_info = get_template_info() |
173 | 211 |
|
174 | 212 | # Sql might be an object (such as psycopg Composed). |
|
0 commit comments