Opened 5 years ago

Closed 5 years ago

Last modified 5 years ago

#30371 closed Bug (needsinfo)

sqlmigrate fails with string defaults on mysql

Reported by: Melvyn Sopacua Owned by: nobody
Component: Database layer (models, ORM) Version: 2.1
Severity: Release blocker Keywords: regression
Cc: Triage Stage: Accepted
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

When we have a migration that is adding strings defaults to a field, sqlmigrate will fail:

  File "/home/melvyn/.local/py-env/button3d-7LYQbQGk/lib/python3.6/site-packages/django/db/migrations/migration.py", line 124, in apply
    operation.database_forwards(self.app_label, schema_editor, old_state, project_state)
  File "/home/melvyn/.local/py-env/button3d-7LYQbQGk/lib/python3.6/site-packages/django/db/migrations/operations/fields.py", line 84, in database_forwards
    field,
  File "/home/melvyn/.local/py-env/button3d-7LYQbQGk/lib/python3.6/site-packages/django/db/backends/mysql/schema.py", line 42, in add_field
    super().add_field(model, field)
  File "/home/melvyn/.local/py-env/button3d-7LYQbQGk/lib/python3.6/site-packages/django/db/backends/base/schema.py", line 435, in add_field
    self.execute(sql, params)
  File "/home/melvyn/.local/py-env/button3d-7LYQbQGk/lib/python3.6/site-packages/django/db/backends/base/schema.py", line 128, in execute
    self.collected_sql.append((sql % tuple(map(self.quote_value, params))) + ending)
  File "/home/melvyn/.local/py-env/button3d-7LYQbQGk/lib/python3.6/site-packages/django/db/backends/mysql/schema.py", line 31, in quote_value
    quoted = quoted.decode()
AttributeError: 'str' object has no attribute 'decode'

This is because of the quote_value method calling decode() on a string object (it is a method of a byte object). This doesn't happen when the migration is being applied, presumably, because then we're creating byte objects and the code doesn't trigger, but this is a guess.

Change History (9)

comment:1 by Claude Paroz, 5 years ago

Keywords: regression added
Severity: NormalRelease blocker
Triage Stage: UnreviewedAccepted

comment:2 by Claude Paroz, 5 years ago

Melvyn, could you tell us what's the type of your default value? Please also tell us your MySQL and mysqlclient version.

comment:3 by Claude Paroz, 5 years ago

Resolution: needsinfo
Status: newclosed

I was unable to reproduce until now, we need more information, please.

comment:4 by Melvyn Sopacua, 5 years ago

If you can't reproduce it, you have dead code, cause str.decode() simply isn't a thing.

However, it may be caused by an extension. I will have time on Friday to investigate.

comment:5 by Claude Paroz, 5 years ago

The code counts on the fact that connection.escape() will return a bytestring for a str value. Note the isinstance test is done on value but the decode() is called on quoted.

comment:6 by Melvyn Sopacua, 5 years ago

Thanks!
Note to self: test 2 cases:

  • migrations without any extensions and/or foreign migration operations
  • compare mysql driver versions: pymysql 0.9.3

Also check connection.escape().

in reply to:  6 comment:7 by Claude Paroz, 5 years ago

Replying to Melvyn Sopacua:

  • compare mysql driver versions: pymysql 0.9.3

Ah, ah... that is probably the explanation. It might be that pymysql doesn't return a bytestring from quote() which would explain the issue (to be confirmed). Django doesn't officially support pymysql (read also #30380). It's another issue where we might call force_text instead of decode(). However this section is a bit more performance-sensitive.

comment:8 by Melvyn Sopacua, 5 years ago

I think the proper fix here is to confirm both str on value and 'decode' attribute on quoted.

I've tried to add a test for it, but the code never gets called anymore in the scope of migrations, because Django no longer transmits default values to the backend. So the issue is with our extension that ensures we can do blue/green deployments. Looking at this code confirms your suspicions that pymysql returns strings.

Still - when I read the code the first few times until you highlighted the fact here, I thought the if clause and the modification are using the same value, cause it's such a common pattern you don't read the variable name anymore.
And secondly, it assumes that quoted has the decode() method based on a not related test, so I still suggest to fix it like this:

if isinstance(value, str) and callable(getattr(quoted, 'decode', None)):
    quoted = quoted.decode()

This doesn't require the performance hit of force_text and it even supports exotic quoted values that implement a decode() method.

comment:9 by Mariusz Felisiak, 5 years ago

This should be fixed as a part of a41b09266dcdd01036d59d76fe926fe0386aaade.

Note: See TracTickets for help on using tickets.
Back to Top