Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions migrate_sql/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from django.apps import AppConfig
from django.core.management.commands import makemigrations

from migrate_sql.autodetector import MigrationAutodetectorMixin


def patch_autodetector():
if not issubclass(makemigrations.MigrationAutodetector, MigrationAutodetectorMixin):
makemigrations.MigrationAutodetector = type(
"MigrationAutodetector",
(
MigrationAutodetectorMixin,
makemigrations.MigrationAutodetector,
),
{},
)


class MigrateSQLConfig(AppConfig):
name = "migrate_sql"

def ready(self):
patch_autodetector()
84 changes: 55 additions & 29 deletions migrate_sql/autodetector.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
from django.db.migrations.autodetector import MigrationAutodetector as DjangoMigrationAutodetector
from django.db.migrations.autodetector import OperationDependency
from django.db.migrations.operations import RunSQL
from django.utils.datastructures import OrderedSet

from migrate_sql.operations import (AlterSQL, ReverseAlterSQL, CreateSQL, DeleteSQL, AlterSQLState)
from migrate_sql.graph import SQLStateGraph
from migrate_sql.graph import SQLStateGraph, build_current_graph
from migrate_sql.operations import (
AlterSQL,
AlterSQLState,
CreateSQL,
DeleteSQL,
ReverseAlterSQL,
)


class SQLBlob(object):
class SQLBlob:
pass


# Dummy object used to identify django dependency as the one used by this tool only.
SQL_BLOB = SQLBlob()

Expand Down Expand Up @@ -49,7 +56,7 @@ def is_sql_equal(sqls1, sqls2):
if len(sqls1) != len(sqls2):
return False

for sql1, sql2 in zip(sqls1, sqls2):
for sql1, sql2 in zip(sqls1, sqls2, strict=False):
sql1, params1 = _sql_params(sql1)
sql2, params2 = _sql_params(sql2)
if sql1 != sql2 or params1 != params2:
Expand All @@ -59,34 +66,37 @@ def is_sql_equal(sqls1, sqls2):

def get_ancestors(node):
"""Logic extracted from Django <2.2 as this is dropped in later versions."""
if '_ancestors' not in node.__dict__:
if "_ancestors" not in node.__dict__:
ancestors = []
for parent in sorted(node.parents, reverse=True):
ancestors += get_ancestors(parent)
ancestors.append(node.key)
node.__dict__['_ancestors'] = list(OrderedSet(ancestors))
return node.__dict__['_ancestors']
node.__dict__["_ancestors"] = list(OrderedSet(ancestors))
return node.__dict__["_ancestors"]


def get_descendants(node):
"""Logic extracted from Django <2.2 as this is dropped in later versions."""
if '_descendants' not in node.__dict__:
if "_descendants" not in node.__dict__:
descendants = []
for child in sorted(node.children, reverse=True):
descendants += get_descendants(child)
descendants.append(node.key)
node.__dict__['_descendants'] = list(OrderedSet(descendants))
return node.__dict__['_descendants']
node.__dict__["_descendants"] = list(OrderedSet(descendants))
return node.__dict__["_descendants"]


class MigrationAutodetector(DjangoMigrationAutodetector):
class MigrationAutodetectorMixin:
"""
Substitutes Django's MigrationAutodetector class, injecting SQL migrations logic.
"""
def __init__(self, from_state, to_state, questioner=None, to_sql_graph=None):
super(MigrationAutodetector, self).__init__(from_state, to_state, questioner)
self.to_sql_graph = to_sql_graph
self.from_sql_graph = getattr(self.from_state, 'sql_state', None) or SQLStateGraph()

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.to_sql_graph = build_current_graph()
self.from_sql_graph = (
getattr(self.from_state, "sql_state", None) or SQLStateGraph()
)
self.from_sql_graph.build_graph()
self._sql_operations = []

Expand Down Expand Up @@ -114,7 +124,9 @@ def assemble_changes(self, keys, resolve_keys, sql_state):
sql_item = sql_state.nodes[key]
ancs = get_ancestors(node)[:-1]
ancs.reverse()
pos = next((i for i, k in enumerate(result_keys) if k in ancs), len(result_keys))
pos = next(
(i for i, k in enumerate(result_keys) if k in ancs), len(result_keys)
)
result_keys.insert(pos, key)

if key in resolve_keys and not sql_item.replace:
Expand All @@ -132,8 +144,10 @@ def add_sql_operation(self, app_label, sql_name, operation, dependencies):
Add SQL operation and register it to be used as dependency for further
sequential operations.
"""
deps = [(dp[0], SQL_BLOB, dp[1], self._sql_operations.get(dp)) for dp in dependencies]

deps = [
OperationDependency(dp[0], SQL_BLOB, dp[1], self._sql_operations.get(dp))
for dp in dependencies
]
self.add_operation(app_label, operation, dependencies=deps)
self._sql_operations[(app_label, sql_name)] = operation

Expand All @@ -147,11 +161,17 @@ def _generate_reversed_sql(self, keys, changed_keys):
app_label, sql_name = key
old_item = self.from_sql_graph.nodes[key]
new_item = self.to_sql_graph.nodes[key]
if not old_item.reverse_sql or old_item.reverse_sql == RunSQL.noop or new_item.replace:
if (
not old_item.reverse_sql
or old_item.reverse_sql == RunSQL.noop
or new_item.replace
):
continue

# migrate backwards
operation = ReverseAlterSQL(sql_name, old_item.reverse_sql, reverse_sql=old_item.sql)
operation = ReverseAlterSQL(
sql_name, old_item.reverse_sql, reverse_sql=old_item.sql
)
sql_deps = [n.key for n in self.from_sql_graph.node_map[key].children]
sql_deps.append(key)
self.add_sql_operation(app_label, sql_name, operation, sql_deps)
Expand All @@ -173,14 +193,15 @@ def _generate_sql(self, keys, changed_keys):
# state_reverse_sql, the latter one will be used for building state forward
# instead of reverse_sql.
if new_item.replace:
kwargs['state_reverse_sql'] = reverse_sql
kwargs["state_reverse_sql"] = reverse_sql
reverse_sql = self.from_sql_graph.nodes[key].sql
else:
operation_cls = CreateSQL
kwargs = {'dependencies': list(sql_deps)}
kwargs = {"dependencies": list(sql_deps)}

operation = operation_cls(
sql_name, new_item.sql, reverse_sql=reverse_sql, **kwargs)
sql_name, new_item.sql, reverse_sql=reverse_sql, **kwargs
)
sql_deps.append(key)
self.add_sql_operation(app_label, sql_name, operation, sql_deps)

Expand All @@ -198,8 +219,11 @@ def _generate_altered_sql_dependencies(self, dep_changed_keys):
"""
for key, removed_deps, added_deps in dep_changed_keys:
app_label, sql_name = key
operation = AlterSQLState(sql_name, add_dependencies=tuple(added_deps),
remove_dependencies=tuple(removed_deps))
operation = AlterSQLState(
sql_name,
add_dependencies=tuple(added_deps),
remove_dependencies=tuple(removed_deps),
)
sql_deps = [key]
self.add_sql_operation(app_label, sql_name, operation, sql_deps)

Expand All @@ -210,7 +234,9 @@ def _generate_delete_sql(self, delete_keys):
for key in delete_keys:
app_label, sql_name = key
old_node = self.from_sql_graph.nodes[key]
operation = DeleteSQL(sql_name, old_node.reverse_sql, reverse_sql=old_node.sql)
operation = DeleteSQL(
sql_name, old_node.reverse_sql, reverse_sql=old_node.sql
)
sql_deps = [n.key for n in self.from_sql_graph.node_map[key].children]
sql_deps.append(key)
self.add_sql_operation(app_label, sql_name, operation, sql_deps)
Expand Down Expand Up @@ -263,14 +289,14 @@ def check_dependency(self, operation, dependency):
# NOTE: we follow the sort order created by `assemble_changes` so we build a fixed chain
# of operations. thus we should match exact operation here.
return dependency[3] == operation
return super(MigrationAutodetector, self).check_dependency(operation, dependency)
return super().check_dependency(operation, dependency)

def generate_altered_fields(self):
"""
Injecting point. This is quite awkward, and i'm looking forward Django for having the logic
divided into smaller methods/functions for easier enhancement and substitution.
So far we're doing all the SQL magic in this method.
"""
result = super(MigrationAutodetector, self).generate_altered_fields()
result = super().generate_altered_fields()
self.generate_sql_changes()
return result
Empty file removed migrate_sql/management/__init__.py
Empty file.
Empty file.
136 changes: 0 additions & 136 deletions migrate_sql/management/commands/makemigrations.py

This file was deleted.