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

Co-authored-by: Nick Pope <nick@nickpope.me.uk>
This commit is contained in:
Ryan Cheley 2022-10-30 10:44:33 -07:00 committed by Mariusz Felisiak
parent 7b94847e38
commit 8e6ea1d153
3 changed files with 29 additions and 17 deletions

View File

@ -4,7 +4,8 @@ SQLite backend for the sqlite3 module in the standard library.
import datetime import datetime
import decimal import decimal
import warnings import warnings
from itertools import chain from collections.abc import Mapping
from itertools import chain, tee
from sqlite3 import dbapi2 as Database from sqlite3 import dbapi2 as Database
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
@ -357,20 +358,40 @@ FORMAT_QMARK_REGEX = _lazy_re_compile(r"(?<!%)%s")
class SQLiteCursorWrapper(Database.Cursor): class SQLiteCursorWrapper(Database.Cursor):
""" """
Django uses "format" style placeholders, but sqlite3 uses "qmark" style. Django uses the "format" and "pyformat" styles, but Python's sqlite3 module
This fixes it -- but note that if you want to use a literal "%s" in a query, supports neither of these styles.
you'll need to use "%%s".
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): def execute(self, query, params=None):
if params is None: if params is None:
return Database.Cursor.execute(self, query) 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) return Database.Cursor.execute(self, query, params)
def executemany(self, query, param_list): 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) return Database.Cursor.executemany(self, query, param_list)
def convert_query(self, query): 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("%%", "%") 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}

View File

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

View File

@ -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 SQLite does not support the ``SELECT ... FOR UPDATE`` syntax. Calling it will
have no effect. 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: .. _sqlite-isolation:
Isolation when using ``QuerySet.iterator()`` Isolation when using ``QuerySet.iterator()``