Browse Source

Fixed #10070 -- Added support for pyformat style parameters on SQLite.

Co-authored-by: Nick Pope <nick@nickpope.me.uk>
Ryan Cheley 2 years ago
parent
commit
8e6ea1d153

+ 29 - 8
django/db/backends/sqlite3/base.py

@@ -4,7 +4,8 @@ SQLite backend for the sqlite3 module in the standard library.
 import datetime
 import decimal
 import warnings
-from itertools import chain
+from collections.abc import Mapping
+from itertools import chain, tee
 from sqlite3 import dbapi2 as Database
 
 from django.core.exceptions import ImproperlyConfigured
@@ -357,20 +358,40 @@ FORMAT_QMARK_REGEX = _lazy_re_compile(r"(?<!%)%s")
 
 class SQLiteCursorWrapper(Database.Cursor):
     """
-    Django uses "format" style placeholders, but sqlite3 uses "qmark" style.
-    This fixes it -- but note that if you want to use a literal "%s" in a query,
-    you'll need to use "%%s".
+    Django uses the "format" and "pyformat" styles, but Python's sqlite3 module
+    supports neither of these styles.
+
+    This wrapper performs the following conversions:
+
+    - "format" style to "qmark" style
+    - "pyformat" style to "named" style
+
+    In both cases, if you want to use a literal "%s", you'll need to use "%%s".
     """
 
     def execute(self, query, params=None):
         if params is None:
             return Database.Cursor.execute(self, query)
-        query = self.convert_query(query)
+        # Extract names if params is a mapping, i.e. "pyformat" style is used.
+        param_names = list(params) if isinstance(params, Mapping) else None
+        query = self.convert_query(query, param_names=param_names)
         return Database.Cursor.execute(self, query, params)
 
     def executemany(self, query, param_list):
-        query = self.convert_query(query)
+        # Extract names if params is a mapping, i.e. "pyformat" style is used.
+        # Peek carefully as a generator can be passed instead of a list/tuple.
+        peekable, param_list = tee(iter(param_list))
+        if (params := next(peekable, None)) and isinstance(params, Mapping):
+            param_names = list(params)
+        else:
+            param_names = None
+        query = self.convert_query(query, param_names=param_names)
         return Database.Cursor.executemany(self, query, param_list)
 
-    def convert_query(self, query):
-        return FORMAT_QMARK_REGEX.sub("?", query).replace("%%", "%")
+    def convert_query(self, query, *, param_names=None):
+        if param_names is None:
+            # Convert from "format" style to "qmark" style.
+            return FORMAT_QMARK_REGEX.sub("?", query).replace("%%", "%")
+        else:
+            # Convert from "pyformat" style to "named" style.
+            return query % {name: f":{name}" for name in param_names}

+ 0 - 1
django/db/backends/sqlite3/features.py

@@ -18,7 +18,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
     atomic_transactions = False
     can_rollback_ddl = True
     can_create_inline_fk = False
-    supports_paramstyle_pyformat = False
     requires_literal_defaults = True
     can_clone_databases = True
     supports_temporal_subtraction = True

+ 0 - 8
docs/ref/databases.txt

@@ -819,14 +819,6 @@ If you're getting this error, you can solve it by:
 SQLite does not support the ``SELECT ... FOR UPDATE`` syntax. Calling it will
 have no effect.
 
-"pyformat" parameter style in raw queries not supported
--------------------------------------------------------
-
-For most backends, raw queries (``Manager.raw()`` or ``cursor.execute()``)
-can use the "pyformat" parameter style, where placeholders in the query
-are given as ``'%(name)s'`` and the parameters are passed as a dictionary
-rather than a list. SQLite does not support this.
-
 .. _sqlite-isolation:
 
 Isolation when using ``QuerySet.iterator()``