summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--django/db/backends/oracle/schema.py2
-rw-r--r--django/db/backends/postgresql/schema.py2
-rw-r--r--docs/releases/2.2.1.txt4
-rw-r--r--tests/migrations/test_operations.py43
4 files changed, 50 insertions, 1 deletions
diff --git a/django/db/backends/oracle/schema.py b/django/db/backends/oracle/schema.py
index 2d9c9699a0..3356788a82 100644
--- a/django/db/backends/oracle/schema.py
+++ b/django/db/backends/oracle/schema.py
@@ -23,7 +23,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
if isinstance(value, (datetime.date, datetime.time, datetime.datetime)):
return "'%s'" % value
elif isinstance(value, str):
- return "'%s'" % value.replace("\'", "\'\'")
+ return "'%s'" % value.replace("\'", "\'\'").replace('%', '%%')
elif isinstance(value, (bytes, bytearray, memoryview)):
return "'%s'" % value.hex()
elif isinstance(value, bool):
diff --git a/django/db/backends/postgresql/schema.py b/django/db/backends/postgresql/schema.py
index 3319ee09a9..0738f009cd 100644
--- a/django/db/backends/postgresql/schema.py
+++ b/django/db/backends/postgresql/schema.py
@@ -24,6 +24,8 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
sql_delete_procedure = 'DROP FUNCTION %(procedure)s(%(param_types)s)'
def quote_value(self, value):
+ if isinstance(value, str):
+ value = value.replace('%', '%%')
# getquoted() returns a quoted bytestring of the adapted value.
return psycopg2.extensions.adapt(value).getquoted().decode()
diff --git a/docs/releases/2.2.1.txt b/docs/releases/2.2.1.txt
index 9f241b83e9..90c9a06e79 100644
--- a/docs/releases/2.2.1.txt
+++ b/docs/releases/2.2.1.txt
@@ -70,3 +70,7 @@ Bugfixes
* Fixed a regression in Django 2.2 where changes were not reliably detected by
auto-reloader when using ``StatReloader`` (:ticket:`30323`).
+
+* Fixed a migration crash on Oracle and PostgreSQL when adding a check
+ constraint with a ``contains``, ``startswith``, or ``endswith`` lookup (or
+ their case-insensitive variant) (:ticket:`30408`).
diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py
index d5c8396596..520a2b2204 100644
--- a/tests/migrations/test_operations.py
+++ b/tests/migrations/test_operations.py
@@ -1856,6 +1856,49 @@ class OperationTests(OperationTestBase):
self.assertEqual(definition[2], {'model_name': "Pony", 'constraint': gt_constraint})
@skipUnlessDBFeature('supports_table_check_constraints')
+ def test_add_constraint_percent_escaping(self):
+ app_label = 'add_constraint_string_quoting'
+ operations = [
+ CreateModel(
+ 'Author',
+ fields=[
+ ('id', models.AutoField(primary_key=True)),
+ ('name', models.CharField(max_length=100)),
+ ('rebate', models.CharField(max_length=100)),
+ ],
+ ),
+ ]
+ from_state = self.apply_operations(app_label, ProjectState(), operations)
+ # "%" generated in startswith lookup should be escaped in a way that is
+ # considered a leading wildcard.
+ check = models.Q(name__startswith='Albert')
+ constraint = models.CheckConstraint(check=check, name='name_constraint')
+ operation = migrations.AddConstraint('Author', constraint)
+ to_state = from_state.clone()
+ operation.state_forwards(app_label, to_state)
+ with connection.schema_editor() as editor:
+ operation.database_forwards(app_label, editor, from_state, to_state)
+ Author = to_state.apps.get_model(app_label, 'Author')
+ with self.assertRaises(IntegrityError), transaction.atomic():
+ Author.objects.create(name='Artur')
+ # Literal "%" should be escaped in a way that is not a considered a
+ # wildcard.
+ check = models.Q(rebate__endswith='%')
+ constraint = models.CheckConstraint(check=check, name='rebate_constraint')
+ operation = migrations.AddConstraint('Author', constraint)
+ from_state = to_state
+ to_state = from_state.clone()
+ operation.state_forwards(app_label, to_state)
+ Author = to_state.apps.get_model(app_label, 'Author')
+ with connection.schema_editor() as editor:
+ operation.database_forwards(app_label, editor, from_state, to_state)
+ Author = to_state.apps.get_model(app_label, 'Author')
+ with self.assertRaises(IntegrityError), transaction.atomic():
+ Author.objects.create(name='Albert', rebate='10$')
+ author = Author.objects.create(name='Albert', rebate='10%')
+ self.assertEqual(Author.objects.get(), author)
+
+ @skipUnlessDBFeature('supports_table_check_constraints')
def test_remove_constraint(self):
project_state = self.set_up_test_model("test_removeconstraint", constraints=[
models.CheckConstraint(check=models.Q(pink__gt=2), name="test_remove_constraint_pony_pink_gt_2"),