Fixed #23426 -- Allowed parameters in migrations.RunSQL
Thanks tchaumeny and Loic for reviews.
This commit is contained in:
parent
d49993fa8f
commit
85f6d89313
|
@ -64,20 +64,32 @@ class RunSQL(Operation):
|
||||||
state_operation.state_forwards(app_label, state)
|
state_operation.state_forwards(app_label, state)
|
||||||
|
|
||||||
def database_forwards(self, app_label, schema_editor, from_state, to_state):
|
def database_forwards(self, app_label, schema_editor, from_state, to_state):
|
||||||
statements = schema_editor.connection.ops.prepare_sql_script(self.sql)
|
self._run_sql(schema_editor, self.sql)
|
||||||
for statement in statements:
|
|
||||||
schema_editor.execute(statement, params=None)
|
|
||||||
|
|
||||||
def database_backwards(self, app_label, schema_editor, from_state, to_state):
|
def database_backwards(self, app_label, schema_editor, from_state, to_state):
|
||||||
if self.reverse_sql is None:
|
if self.reverse_sql is None:
|
||||||
raise NotImplementedError("You cannot reverse this operation")
|
raise NotImplementedError("You cannot reverse this operation")
|
||||||
statements = schema_editor.connection.ops.prepare_sql_script(self.reverse_sql)
|
self._run_sql(schema_editor, self.reverse_sql)
|
||||||
for statement in statements:
|
|
||||||
schema_editor.execute(statement, params=None)
|
|
||||||
|
|
||||||
def describe(self):
|
def describe(self):
|
||||||
return "Raw SQL operation"
|
return "Raw SQL operation"
|
||||||
|
|
||||||
|
def _run_sql(self, schema_editor, sql):
|
||||||
|
if isinstance(sql, (list, tuple)):
|
||||||
|
for sql in sql:
|
||||||
|
params = None
|
||||||
|
if isinstance(sql, (list, tuple)):
|
||||||
|
elements = len(sql)
|
||||||
|
if elements == 2:
|
||||||
|
sql, params = sql
|
||||||
|
else:
|
||||||
|
raise ValueError("Expected a 2-tuple but got %d" % elements)
|
||||||
|
schema_editor.execute(sql, params=params)
|
||||||
|
else:
|
||||||
|
statements = schema_editor.connection.ops.prepare_sql_script(sql)
|
||||||
|
for statement in statements:
|
||||||
|
schema_editor.execute(statement, params=None)
|
||||||
|
|
||||||
|
|
||||||
class RunPython(Operation):
|
class RunPython(Operation):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -188,6 +188,17 @@ the database. On most database backends (all but PostgreSQL), Django will
|
||||||
split the SQL into individual statements prior to executing them. This
|
split the SQL into individual statements prior to executing them. This
|
||||||
requires installing the sqlparse_ Python library.
|
requires installing the sqlparse_ Python library.
|
||||||
|
|
||||||
|
You can also pass a list of strings or 2-tuples. The latter is used for passing
|
||||||
|
queries and parameters in the same way as :ref:`cursor.execute()
|
||||||
|
<executing-custom-sql>`. These three operations are equivalent::
|
||||||
|
|
||||||
|
migrations.RunSQL("INSERT INTO musician (name) VALUES ('Reinhardt');")
|
||||||
|
migrations.RunSQL(["INSERT INTO musician (name) VALUES ('Reinhardt');", None])
|
||||||
|
migrations.RunSQL(["INSERT INTO musician (name) VALUES (%s);", ['Reinhardt']])
|
||||||
|
|
||||||
|
If you want to include literal percent signs in the query, you have to double
|
||||||
|
them if you are passing parameters.
|
||||||
|
|
||||||
The ``state_operations`` argument is so you can supply operations that are
|
The ``state_operations`` argument is so you can supply operations that are
|
||||||
equivalent to the SQL in terms of project state; for example, if you are
|
equivalent to the SQL in terms of project state; for example, if you are
|
||||||
manually creating a column, you should pass in a list containing an ``AddField``
|
manually creating a column, you should pass in a list containing an ``AddField``
|
||||||
|
@ -197,8 +208,13 @@ operation that adds that field and so will try to run it again).
|
||||||
|
|
||||||
.. versionchanged:: 1.7.1
|
.. versionchanged:: 1.7.1
|
||||||
|
|
||||||
If you want to include literal percent signs in the query you don't need to
|
If you want to include literal percent signs in a query without parameters
|
||||||
double them anymore.
|
you don't need to double them anymore.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.8
|
||||||
|
|
||||||
|
The ability to pass parameters to the ``sql`` and ``reverse_sql`` queries
|
||||||
|
was added.
|
||||||
|
|
||||||
.. _sqlparse: https://pypi.python.org/pypi/sqlparse
|
.. _sqlparse: https://pypi.python.org/pypi/sqlparse
|
||||||
|
|
||||||
|
|
|
@ -265,6 +265,12 @@ Management Commands
|
||||||
|
|
||||||
* :djadmin:`makemigrations` can now serialize timezone-aware values.
|
* :djadmin:`makemigrations` can now serialize timezone-aware values.
|
||||||
|
|
||||||
|
Migrations
|
||||||
|
^^^^^^^^^^
|
||||||
|
|
||||||
|
* The :class:`~django.db.migrations.operations.RunSQL` operation can now handle
|
||||||
|
parameters passed to the SQL statements.
|
||||||
|
|
||||||
Models
|
Models
|
||||||
^^^^^^
|
^^^^^^
|
||||||
|
|
||||||
|
|
|
@ -1195,6 +1195,87 @@ class OperationTests(OperationTestBase):
|
||||||
operation.database_backwards("test_runsql", editor, new_state, project_state)
|
operation.database_backwards("test_runsql", editor, new_state, project_state)
|
||||||
self.assertTableNotExists("i_love_ponies")
|
self.assertTableNotExists("i_love_ponies")
|
||||||
|
|
||||||
|
def test_run_sql_params(self):
|
||||||
|
"""
|
||||||
|
#23426 - RunSQL should accept parameters.
|
||||||
|
"""
|
||||||
|
project_state = self.set_up_test_model("test_runsql")
|
||||||
|
# Create the operation
|
||||||
|
operation = migrations.RunSQL(
|
||||||
|
"CREATE TABLE i_love_ponies (id int, special_thing varchar(15));",
|
||||||
|
"DROP TABLE i_love_ponies",
|
||||||
|
)
|
||||||
|
param_operation = migrations.RunSQL(
|
||||||
|
# forwards
|
||||||
|
(
|
||||||
|
"INSERT INTO i_love_ponies (id, special_thing) VALUES (1, 'Django');",
|
||||||
|
["INSERT INTO i_love_ponies (id, special_thing) VALUES (2, %s);", ['Ponies']],
|
||||||
|
("INSERT INTO i_love_ponies (id, special_thing) VALUES (%s, %s);", (3, 'Python',)),
|
||||||
|
),
|
||||||
|
# backwards
|
||||||
|
[
|
||||||
|
"DELETE FROM i_love_ponies WHERE special_thing = 'Django';",
|
||||||
|
["DELETE FROM i_love_ponies WHERE special_thing = 'Ponies';", None],
|
||||||
|
("DELETE FROM i_love_ponies WHERE id = %s OR special_thing = %s;", [3, 'Python']),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make sure there's no table
|
||||||
|
self.assertTableNotExists("i_love_ponies")
|
||||||
|
new_state = project_state.clone()
|
||||||
|
# Test the database alteration
|
||||||
|
with connection.schema_editor() as editor:
|
||||||
|
operation.database_forwards("test_runsql", editor, project_state, new_state)
|
||||||
|
|
||||||
|
# Test parameter passing
|
||||||
|
with connection.schema_editor() as editor:
|
||||||
|
param_operation.database_forwards("test_runsql", editor, project_state, new_state)
|
||||||
|
# Make sure all the SQL was processed
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM i_love_ponies")
|
||||||
|
self.assertEqual(cursor.fetchall()[0][0], 3)
|
||||||
|
|
||||||
|
with connection.schema_editor() as editor:
|
||||||
|
param_operation.database_backwards("test_runsql", editor, new_state, project_state)
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM i_love_ponies")
|
||||||
|
self.assertEqual(cursor.fetchall()[0][0], 0)
|
||||||
|
|
||||||
|
# And test reversal
|
||||||
|
with connection.schema_editor() as editor:
|
||||||
|
operation.database_backwards("test_runsql", editor, new_state, project_state)
|
||||||
|
self.assertTableNotExists("i_love_ponies")
|
||||||
|
|
||||||
|
def test_run_sql_params_invalid(self):
|
||||||
|
"""
|
||||||
|
#23426 - RunSQL should fail when a list of statements with an incorrect
|
||||||
|
number of tuples is given.
|
||||||
|
"""
|
||||||
|
project_state = self.set_up_test_model("test_runsql")
|
||||||
|
new_state = project_state.clone()
|
||||||
|
operation = migrations.RunSQL(
|
||||||
|
# forwards
|
||||||
|
[
|
||||||
|
["INSERT INTO foo (bar) VALUES ('buz');"]
|
||||||
|
],
|
||||||
|
# backwards
|
||||||
|
(
|
||||||
|
("DELETE FROM foo WHERE bar = 'buz';", 'invalid', 'parameter count'),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
with connection.schema_editor() as editor:
|
||||||
|
self.assertRaisesRegexp(ValueError,
|
||||||
|
"Expected a 2-tuple but got 1",
|
||||||
|
operation.database_forwards,
|
||||||
|
"test_runsql", editor, project_state, new_state)
|
||||||
|
|
||||||
|
with connection.schema_editor() as editor:
|
||||||
|
self.assertRaisesRegexp(ValueError,
|
||||||
|
"Expected a 2-tuple but got 3",
|
||||||
|
operation.database_backwards,
|
||||||
|
"test_runsql", editor, new_state, project_state)
|
||||||
|
|
||||||
def test_run_python(self):
|
def test_run_python(self):
|
||||||
"""
|
"""
|
||||||
Tests the RunPython operation
|
Tests the RunPython operation
|
||||||
|
|
Loading…
Reference in New Issue