Skip to content

Commit 852e455

Browse files
authored
Merge pull request #1229 from tim-schilling/postgres-json-explain
Support SQL Select and Explain actions for Postgres JSON fields.
2 parents 98308a2 + ab5c53e commit 852e455

File tree

4 files changed

+78
-1
lines changed

4 files changed

+78
-1
lines changed

debug_toolbar/panels/sql/tracking.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
from debug_toolbar import settings as dt_settings
99
from debug_toolbar.utils import get_stack, get_template_info, tidy_stacktrace
1010

11+
try:
12+
from psycopg2._json import Json as PostgresJson
13+
except ImportError:
14+
PostgresJson = None
15+
1116

1217
class SQLQueryTriggered(Exception):
1318
"""Thrown when template panel triggers a query"""
@@ -105,6 +110,8 @@ def _quote_params(self, params):
105110
return [self._quote_expr(p) for p in params]
106111

107112
def _decode(self, param):
113+
if PostgresJson and isinstance(param, PostgresJson):
114+
return param.dumps(param.adapted)
108115
# If a sequence type, decode each element separately
109116
if isinstance(param, (tuple, list)):
110117
return [self._decode(element) for element in param]
@@ -136,7 +143,6 @@ def _record(self, method, sql, params):
136143
_params = json.dumps(self._decode(params))
137144
except TypeError:
138145
pass # object not JSON serializable
139-
140146
template_info = get_template_info()
141147

142148
alias = getattr(self.db, "alias", "default")

tests/models.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,14 @@ def __repr__(self):
88

99
class Binary(models.Model):
1010
field = models.BinaryField()
11+
12+
13+
try:
14+
from django.contrib.postgres.fields import JSONField
15+
16+
class PostgresJSON(models.Model):
17+
field = JSONField()
18+
19+
20+
except ImportError:
21+
pass

tests/panels/test_sql.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@
1111

1212
from ..base import BaseTestCase
1313

14+
try:
15+
from psycopg2._json import Json as PostgresJson
16+
except ImportError:
17+
PostgresJson = None
18+
19+
if connection.vendor == "postgresql":
20+
from ..models import PostgresJSON as PostgresJSONModel
21+
else:
22+
PostgresJSONModel = None
23+
1424

1525
class SQLPanelTestCase(BaseTestCase):
1626
panel_id = "SQLPanel"
@@ -120,6 +130,26 @@ def test_param_conversion(self):
120130
('["Foo", true, false]', "[10, 1]", '["2017-12-22 16:07:01"]'),
121131
)
122132

133+
@unittest.skipUnless(
134+
connection.vendor == "postgresql", "Test valid only on PostgreSQL"
135+
)
136+
def test_json_param_conversion(self):
137+
self.assertEqual(len(self.panel._queries), 0)
138+
139+
list(PostgresJSONModel.objects.filter(field__contains={"foo": "bar"}))
140+
141+
response = self.panel.process_request(self.request)
142+
self.panel.generate_stats(self.request, response)
143+
144+
# ensure query was logged
145+
self.assertEqual(len(self.panel._queries), 1)
146+
self.assertEqual(
147+
self.panel._queries[0][1]["params"], '["{\\"foo\\": \\"bar\\"}"]',
148+
)
149+
self.assertIsInstance(
150+
self.panel._queries[0][1]["raw_params"][0], PostgresJson,
151+
)
152+
123153
def test_binary_param_force_text(self):
124154
self.assertEqual(len(self.panel._queries), 0)
125155

tests/test_integration.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
77
from django.core import signing
88
from django.core.checks import Warning, run_checks
9+
from django.db import connection
910
from django.http import HttpResponse
1011
from django.template.loader import get_template
1112
from django.test import RequestFactory, SimpleTestCase, TestCase
@@ -206,6 +207,35 @@ def test_sql_explain_checks_show_toolbar(self):
206207
)
207208
self.assertEqual(response.status_code, 404)
208209

210+
@unittest.skipUnless(
211+
connection.vendor == "postgresql", "Test valid only on PostgreSQL"
212+
)
213+
def test_sql_explain_postgres_json_field(self):
214+
url = "/__debug__/sql_explain/"
215+
base_query = (
216+
'SELECT * FROM "tests_postgresjson" WHERE "tests_postgresjson"."field" @>'
217+
)
218+
query = base_query + """ '{"foo": "bar"}'"""
219+
data = {
220+
"sql": query,
221+
"raw_sql": base_query + " %s",
222+
"params": '["{\\"foo\\": \\"bar\\"}"]',
223+
"alias": "default",
224+
"duration": "0",
225+
"hash": "2b7172eb2ac8e2a8d6f742f8a28342046e0d00ba",
226+
}
227+
response = self.client.post(url, data)
228+
self.assertEqual(response.status_code, 200)
229+
response = self.client.post(url, data, HTTP_X_REQUESTED_WITH="XMLHttpRequest")
230+
self.assertEqual(response.status_code, 200)
231+
with self.settings(INTERNAL_IPS=[]):
232+
response = self.client.post(url, data)
233+
self.assertEqual(response.status_code, 404)
234+
response = self.client.post(
235+
url, data, HTTP_X_REQUESTED_WITH="XMLHttpRequest"
236+
)
237+
self.assertEqual(response.status_code, 404)
238+
209239
def test_sql_profile_checks_show_toolbar(self):
210240
url = "/__debug__/sql_profile/"
211241
data = {

0 commit comments

Comments
 (0)