Fixed #29251 -- Added bytes to str conversion in LPad/RPad database functions on MySQL.

Thanks Tim Graham for the review.
This commit is contained in:
Mariusz Felisiak 2018-04-03 18:24:04 +02:00 committed by GitHub
parent 4f7467b690
commit 6141c752fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 33 additions and 3 deletions

View File

@ -247,6 +247,9 @@ class BaseDatabaseFeatures:
# Does the backend support keyword parameters for cursor.callproc()? # Does the backend support keyword parameters for cursor.callproc()?
supports_callproc_kwargs = False supports_callproc_kwargs = False
# Convert CharField results from bytes to str in database functions.
db_functions_convert_bytes_to_str = False
def __init__(self, connection): def __init__(self, connection):
self.connection = connection self.connection = connection

View File

@ -48,6 +48,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
SET V_I = P_I; SET V_I = P_I;
END; END;
""" """
db_functions_convert_bytes_to_str = True
@cached_property @cached_property
def _mysql_storage_engine(self): def _mysql_storage_engine(self):

View File

@ -2,6 +2,22 @@ from django.db.models import Func, IntegerField, Transform, Value, fields
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
class BytesToCharFieldConversionMixin:
"""
Convert CharField results from bytes to str.
MySQL returns long data types (bytes) instead of chars when it can't
determine the length of the result string. For example:
LPAD(column1, CHAR_LENGTH(column2), ' ')
returns the LONGTEXT (bytes) instead of VARCHAR.
"""
def convert_value(self, value, expression, connection):
if connection.features.db_functions_convert_bytes_to_str:
if self.output_field.get_internal_type() == 'CharField' and isinstance(value, bytes):
return value.decode()
return super().convert_value(value, expression, connection)
class Chr(Transform): class Chr(Transform):
function = 'CHR' function = 'CHR'
lookup_name = 'chr' lookup_name = 'chr'
@ -110,7 +126,7 @@ class Lower(Transform):
lookup_name = 'lower' lookup_name = 'lower'
class LPad(Func): class LPad(BytesToCharFieldConversionMixin, Func):
function = 'LPAD' function = 'LPAD'
def __init__(self, expression, length, fill_text=Value(' '), **extra): def __init__(self, expression, length, fill_text=Value(' '), **extra):

View File

@ -1,5 +1,5 @@
from django.db.models import Value from django.db.models import CharField, Value
from django.db.models.functions import LPad, RPad from django.db.models.functions import Length, LPad, RPad
from django.test import TestCase from django.test import TestCase
from .models import Author from .models import Author
@ -32,3 +32,13 @@ class PadTests(TestCase):
with self.subTest(function=function): with self.subTest(function=function):
with self.assertRaisesMessage(ValueError, "'length' must be greater or equal to 0."): with self.assertRaisesMessage(ValueError, "'length' must be greater or equal to 0."):
function('name', -1) function('name', -1)
def test_combined_with_length(self):
Author.objects.create(name='Rhonda', alias='john_smith')
Author.objects.create(name='♥♣♠', alias='bytes')
authors = Author.objects.annotate(filled=LPad('name', Length('alias'), output_field=CharField()))
self.assertQuerysetEqual(
authors.order_by('alias'),
[' ♥♣♠', ' Rhonda'],
lambda a: a.filled,
)