#32595 closed Bug (fixed)
mysql DatabaseSchemaEditor can't `quote_value` on byte strings
| Reported by: | Ryan Siemens | Owned by: | Mariusz Felisiak | 
|---|---|---|---|
| Component: | Database layer (models, ORM) | Version: | 3.1 | 
| Severity: | Normal | Keywords: | |
| Cc: | Ryan Siemens, Claude Paroz | Triage Stage: | Ready for checkin | 
| Has patch: | yes | Needs documentation: | no | 
| Needs tests: | no | Patch needs improvement: | no | 
| Easy pickings: | no | UI/UX: | no | 
Description
Using the the django.db.backends.mysql backend with mysqlclient fails to render the schema migration with sqlmigrate when using a BinaryField with a binary string default value.
You can recreate this with a simple app using the following model or see the attached sample project (MD5 = 2be52a23a4001e90728d3b0f5276e087):
class TestModel(models.Model):
    ok = models.BinaryField(default=b'some-blob')
    # add this after the initial migration
    # not_ok = models.BinaryField(default=b'some-other-blob')
Then perform the following steps:
- run initial 
manage.py makemigrations - uncomment the 
not_ok = models.BinaryField(default=b'some-other-blob') - run 
manage.py makemigrationsagain to generate the alter table migration - run 
manage.py sqlmigrate <app> 0002 - assert the following 
TypeErroris raised 
Traceback (most recent call last):
  File "manage.py", line 22, in <module>
    main()
  File "manage.py", line 18, in main
    execute_from_command_line(sys.argv)
  File "/Users/rsiemens/sandbox/quote_value_bug/.venv/lib/python3.8/site-packages/django/core/management/__init__.py", line 401, in execute_from_command_line
    utility.execute()
  File "/Users/rsiemens/sandbox/quote_value_bug/.venv/lib/python3.8/site-packages/django/core/management/__init__.py", line 395, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/Users/rsiemens/sandbox/quote_value_bug/.venv/lib/python3.8/site-packages/django/core/management/base.py", line 330, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/Users/rsiemens/sandbox/quote_value_bug/.venv/lib/python3.8/site-packages/django/core/management/commands/sqlmigrate.py", line 29, in execute
    return super().execute(*args, **options)
  File "/Users/rsiemens/sandbox/quote_value_bug/.venv/lib/python3.8/site-packages/django/core/management/base.py", line 371, in execute
    output = self.handle(*args, **options)
  File "/Users/rsiemens/sandbox/quote_value_bug/.venv/lib/python3.8/site-packages/django/core/management/commands/sqlmigrate.py", line 65, in handle
    sql_statements = loader.collect_sql(plan)
  File "/Users/rsiemens/sandbox/quote_value_bug/.venv/lib/python3.8/site-packages/django/db/migrations/loader.py", line 345, in collect_sql
    state = migration.apply(state, schema_editor, collect_sql=True)
  File "/Users/rsiemens/sandbox/quote_value_bug/.venv/lib/python3.8/site-packages/django/db/migrations/migration.py", line 124, in apply
    operation.database_forwards(self.app_label, schema_editor, old_state, project_state)
  File "/Users/rsiemens/sandbox/quote_value_bug/.venv/lib/python3.8/site-packages/django/db/migrations/operations/fields.py", line 104, in database_forwards
    schema_editor.add_field(
  File "/Users/rsiemens/sandbox/quote_value_bug/.venv/lib/python3.8/site-packages/django/db/backends/mysql/schema.py", line 89, in add_field
    super().add_field(model, field)
  File "/Users/rsiemens/sandbox/quote_value_bug/.venv/lib/python3.8/site-packages/django/db/backends/base/schema.py", line 487, in add_field
    self.execute(sql, params)
  File "/Users/rsiemens/sandbox/quote_value_bug/.venv/lib/python3.8/site-packages/django/db/backends/base/schema.py", line 137, in execute
    self.collected_sql.append((sql % tuple(map(self.quote_value, params))) + ending)
  File "/Users/rsiemens/sandbox/quote_value_bug/.venv/lib/python3.8/site-packages/django/db/backends/mysql/schema.py", line 55, in quote_value
    quoted = self.connection.connection.escape(value, self.connection.connection.encoders)
TypeError: bytes() argument 2 must be str, not dict
The error seems to be raised from the mysqlclient connection.escape, but interestingly this works if you alter the self.connection.connection.encoders right before escaping (https://github.com/django/django/blob/main/django/db/backends/mysql/schema.py#L57) by doing self.connection.connection.encoders.pop(bytes). Looking at the default converters mysqlclient sets up (https://github.com/PyMySQL/mysqlclient/blob/v2.0.1/MySQLdb/converters.py#L106-L139) I don't see a bytes one being added, so I'm guessing django adds it somewhere along the way.
Using:
Python 3.8.5
MySQL 8.0.23
Django==3.1.7
mysqlclient==2.0.3
Attachments (1)
Change History (12)
by , 5 years ago
| Attachment: | example.tar.gz added | 
|---|
comment:1 by , 5 years ago
| Cc: | added | 
|---|
comment:2 by , 5 years ago
| Resolution: | → invalid | 
|---|---|
| Status: | new → closed | 
Thanks for this ticket, however that's an issue in mysqlclient (see also mysqlclient#306). I'm not sure but maybe they should use _bytes_literal(). Please create a new issue in mysqlclient and feel-free to ping me on it.
comment:3 by , 5 years ago
Thanks, Mariusz. I went ahead and opened https://github.com/PyMySQL/mysqlclient/issues/489
comment:4 by , 5 years ago
| Cc: | added | 
|---|---|
| Resolution: | invalid | 
| Status: | closed → new | 
| Triage Stage: | Unreviewed → Accepted | 
There seems to be no easy way to fix this in mysqlclient at least for now, see [discussion](https://github.com/PyMySQL/mysqlclient/issues/489). I'm going to add a workaround in the MySQL backend.
comment:5 by , 5 years ago
| Owner: | changed from to | 
|---|---|
| Status: | new → assigned | 
comment:7 by , 5 years ago
| Triage Stage: | Accepted → Ready for checkin | 
|---|
example app to recreate bug