Repository: jazzband/django-debug-toolbar

Issue:
Postgresql Raw Query Tuple Parameter 500 error on explain statement
I've got a .raw query with the following filter on a JSONB column named `data`:
`WHERE data ->> 'key' IN %s`

The `%s` is populated by taking the list and passing it in as follows:
`ModelName.objects.raw(query_with_params, [tuple(list_of_values)])`

When viewing the debug pane the SQL generated for the relevant section looks like:
`WHERE data ->> 'key' IN ('value_1', 'value_2', 'value_3')`

When trying to view the explain of the query a 500: Internal Server Error results due to a pscyopg2.errors.syntax error on the following SQL:
`data ->> 'key' IN ARRAY['value_1','value_2','value_3',...`

It looks like there's some issue in what populates the params for [this line](https://github.com/jazzband/django-debug-toolbar/blob/961b166c37244542727222ac255ac121724b34b2/debug_toolbar/panels/sql/views.py#L54) where the passed in tuple is being turned back into a list as lists are turned into ARRAYS not collections.

I was able to get it to work by with the following changes:
`WHERE data ->> 'key' = ANY(%s)`
and
`ModelName.objects.raw(query_with_params, [list_of_values])` where `type(list_of_values)` is <class 'list'>


Discussion:
are you  able to provide a fix?
This can be addressed by modifying [`_quote_params`](https://github.com/jazzband/django-debug-toolbar/blob/961b166c37244542727222ac255ac121724b34b2/debug_toolbar/panels/sql/tracking.py#L112) to return a tuple when `params` is a tuple.

The following code in our test app will trigger a SyntaxError:

```python
list(PostgresJSON.objects.raw("SELECT * FROM tests_postgresjson WHERE field ->> 'key' IN %s", [("a", "b'")]))
```

Resolving patch:
diff --git a/debug_toolbar/panels/sql/tracking.py b/debug_toolbar/panels/sql/tracking.py
--- a/debug_toolbar/panels/sql/tracking.py
+++ b/debug_toolbar/panels/sql/tracking.py
@@ -120,6 +120,8 @@ def _quote_params(self, params):
             return params
         if isinstance(params, dict):
             return {key: self._quote_expr(value) for key, value in params.items()}
+        if isinstance(params, tuple):
+            return tuple(self._quote_expr(p) for p in params)
         return [self._quote_expr(p) for p in params]
 
     def _decode(self, param):


Test patch:
diff --git a/tests/panels/test_sql.py b/tests/panels/test_sql.py
--- a/tests/panels/test_sql.py
+++ b/tests/panels/test_sql.py
@@ -221,6 +221,26 @@ def test_json_param_conversion(self):
             '["{\\"foo\\": \\"bar\\"}"]',
         )
 
+    @unittest.skipUnless(
+        connection.vendor == "postgresql", "Test valid only on PostgreSQL"
+    )
+    def test_tuple_param_conversion(self):
+        self.assertEqual(len(self.panel._queries), 0)
+
+        list(
+            PostgresJSON.objects.raw(
+                "SELECT * FROM tests_postgresjson WHERE field ->> 'key' IN %s",
+                [("a", "b'")],
+            )
+        )
+
+        response = self.panel.process_request(self.request)
+        self.panel.generate_stats(self.request, response)
+
+        # ensure query was logged
+        self.assertEqual(len(self.panel._queries), 1)
+        self.assertEqual(self.panel._queries[0]["params"], '[["a", "b\'"]]')
+
     def test_binary_param_force_text(self):
         self.assertEqual(len(self.panel._queries), 0)