django-migrate-sql-deux 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ __version__ = '1.0.0'
@@ -0,0 +1,326 @@
1
+ import django
2
+ from django.conf import settings
3
+ from django.db.migrations.autodetector import MigrationAutodetector as DjangoMigrationAutodetector
4
+ from django.db.migrations.operations import RunSQL
5
+ from django.utils.datastructures import OrderedSet
6
+ if django.VERSION >= (5, 1):
7
+ from django.db.migrations.autodetector import OperationDependency
8
+
9
+ from .operations import (AlterSQL, ReverseAlterSQL, CreateSQL, DeleteSQL, AlterSQLState)
10
+ from .graph import SQLStateGraph
11
+
12
+
13
+ class SQLBlob(object):
14
+ pass
15
+
16
+ # Dummy object used to identify django dependency as the one used by this tool only.
17
+ SQL_BLOB = SQLBlob()
18
+
19
+
20
+ def _sql_params(sql):
21
+ """
22
+ Identify `sql` as either SQL string or 2-tuple of SQL and params.
23
+ Same format as supported by Django's RunSQL operation for sql/reverse_sql.
24
+ """
25
+ params = None
26
+ if isinstance(sql, (list, tuple)):
27
+ elements = len(sql)
28
+ if elements == 2:
29
+ sql, params = sql
30
+ else:
31
+ raise ValueError("Expected a 2-tuple but got %d" % elements)
32
+ return sql, params
33
+
34
+
35
+ def is_sql_equal(sqls1, sqls2):
36
+ """
37
+ Find out equality of two SQL items.
38
+
39
+ See https://docs.djangoproject.com/en/1.8/ref/migration-operations/#runsql.
40
+ Args:
41
+ sqls1, sqls2: SQL items, have the same format as supported by Django's RunSQL operation.
42
+ Returns:
43
+ (bool) `True` if equal, otherwise `False`.
44
+ """
45
+ is_seq1 = isinstance(sqls1, (list, tuple))
46
+ is_seq2 = isinstance(sqls2, (list, tuple))
47
+
48
+ if not is_seq1:
49
+ sqls1 = (sqls1,)
50
+ if not is_seq2:
51
+ sqls2 = (sqls2,)
52
+
53
+ if len(sqls1) != len(sqls2):
54
+ return False
55
+
56
+ for sql1, sql2 in zip(sqls1, sqls2):
57
+ sql1, params1 = _sql_params(sql1)
58
+ sql2, params2 = _sql_params(sql2)
59
+ if sql1 != sql2 or params1 != params2:
60
+ return False
61
+ return True
62
+
63
+
64
+ def get_ancestors(node):
65
+ """Logic extracted from Django <2.2 as this is dropped in later versions."""
66
+ if '_ancestors' not in node.__dict__:
67
+ ancestors = []
68
+ for parent in sorted(node.parents, reverse=True):
69
+ ancestors += get_ancestors(parent)
70
+ ancestors.append(node.key)
71
+ node.__dict__['_ancestors'] = list(OrderedSet(ancestors))
72
+ return node.__dict__['_ancestors']
73
+
74
+
75
+ def get_descendants(node):
76
+ """Logic extracted from Django <2.2 as this is dropped in later versions."""
77
+ if '_descendants' not in node.__dict__:
78
+ descendants = []
79
+ for child in sorted(node.children, reverse=True):
80
+ descendants += get_descendants(child)
81
+ descendants.append(node.key)
82
+ node.__dict__['_descendants'] = list(OrderedSet(descendants))
83
+ return node.__dict__['_descendants']
84
+
85
+
86
+ class MigrationAutodetector(DjangoMigrationAutodetector):
87
+ """
88
+ Substitutes Django's MigrationAutodetector class, injecting SQL migrations logic.
89
+ """
90
+ def __init__(self, from_state, to_state, questioner=None, to_sql_graph=None):
91
+ super(MigrationAutodetector, self).__init__(from_state, to_state, questioner)
92
+ self.to_sql_graph = to_sql_graph
93
+ self.from_sql_graph = getattr(self.from_state, 'sql_state', None) or SQLStateGraph()
94
+ self.from_sql_graph.build_graph()
95
+ self._sql_operations = []
96
+
97
+ def assemble_changes(self, keys, resolve_keys, sql_state):
98
+ """
99
+ Accepts keys of SQL items available, sorts them and adds additional dependencies.
100
+ Uses graph of `sql_state` nodes to build `keys` and `resolve_keys` into sequence that
101
+ starts with leaves (items that have not dependents) and ends with roots.
102
+
103
+ Changes `resolve_keys` argument as dependencies are added to the result.
104
+
105
+ Args:
106
+ keys (list): List of migration keys, that are one of create/delete operations, and
107
+ dont require respective reverse operations.
108
+ resolve_keys (list): List of migration keys, that are changing existing items,
109
+ and may require respective reverse operations.
110
+ sql_sate (graph.SQLStateGraph): State of SQL items.
111
+ Returns:
112
+ (list) Sorted sequence of migration keys, enriched with dependencies.
113
+ """
114
+ result_keys = []
115
+ all_keys = keys | resolve_keys
116
+ for key in all_keys:
117
+ node = sql_state.node_map[key]
118
+ sql_item = sql_state.nodes[key]
119
+ ancs = get_ancestors(node)[:-1]
120
+ ancs.reverse()
121
+ pos = next((i for i, k in enumerate(result_keys) if k in ancs), len(result_keys))
122
+ result_keys.insert(pos, key)
123
+
124
+ if key in resolve_keys and not sql_item.replace:
125
+ # ancestors() and descendants() include key itself, need to cut it out.
126
+ descs = reversed(get_descendants(node)[:-1])
127
+ for desc in descs:
128
+ if desc not in all_keys and desc not in result_keys:
129
+ result_keys.insert(pos, desc)
130
+ # these items added may also need reverse operations.
131
+ resolve_keys.add(desc)
132
+ return result_keys
133
+
134
+ def add_sql_operation(self, app_label, sql_name, operation, dependencies):
135
+ """
136
+ Add SQL operation and register it to be used as dependency for further
137
+ sequential operations.
138
+ """
139
+ if django.VERSION >= (5, 1):
140
+ deps = [
141
+ OperationDependency(
142
+ app_label=dp[0],
143
+ model_name=SQL_BLOB,
144
+ field_name=dp[1],
145
+ type=self._sql_operations.get(dp),
146
+ )
147
+ for dp in dependencies
148
+ ]
149
+ else:
150
+ deps = [(dp[0], SQL_BLOB, dp[1], self._sql_operations.get(dp)) for dp in dependencies]
151
+
152
+ self.add_operation(app_label, operation, dependencies=deps)
153
+ self._sql_operations[(app_label, sql_name)] = operation
154
+
155
+ def _generate_reversed_sql(self, keys, changed_keys):
156
+ """
157
+ Generate reversed operations for changes, that require full rollback and creation.
158
+ """
159
+ for key in keys:
160
+ if key not in changed_keys:
161
+ continue
162
+ app_label, sql_name = key
163
+ old_item = self.from_sql_graph.nodes[key]
164
+ new_item = self.to_sql_graph.nodes[key]
165
+ if not old_item.reverse_sql or old_item.reverse_sql == RunSQL.noop or new_item.replace:
166
+ continue
167
+
168
+ # migrate backwards
169
+ operation = ReverseAlterSQL(sql_name, old_item.reverse_sql, reverse_sql=old_item.sql)
170
+ sql_deps = [n.key for n in self.from_sql_graph.node_map[key].children]
171
+ sql_deps.append(key)
172
+ self.add_sql_operation(app_label, sql_name, operation, sql_deps)
173
+
174
+ def _generate_sql(self, keys, changed_keys):
175
+ """
176
+ Generate forward operations for changing/creating SQL items.
177
+ """
178
+ for key in reversed(keys):
179
+ app_label, sql_name = key
180
+ new_item = self.to_sql_graph.nodes[key]
181
+ sql_deps = [n.key for n in self.to_sql_graph.node_map[key].parents]
182
+ reverse_sql = new_item.reverse_sql
183
+
184
+ if key in changed_keys:
185
+ operation_cls = AlterSQL
186
+ kwargs = {}
187
+ # in case of replace mode, AlterSQL will hold sql, reverse_sql and
188
+ # state_reverse_sql, the latter one will be used for building state forward
189
+ # instead of reverse_sql.
190
+ if new_item.replace:
191
+ kwargs['state_reverse_sql'] = reverse_sql
192
+ reverse_sql = self.from_sql_graph.nodes[key].sql
193
+ else:
194
+ operation_cls = CreateSQL
195
+ kwargs = {'dependencies': list(sql_deps)}
196
+
197
+ operation = operation_cls(
198
+ sql_name, new_item.sql, reverse_sql=reverse_sql, **kwargs)
199
+ sql_deps.append(key)
200
+ self.add_sql_operation(app_label, sql_name, operation, sql_deps)
201
+
202
+ def _generate_altered_sql_dependencies(self, dep_changed_keys):
203
+ """
204
+ Generate forward operations for changing/creating SQL item dependencies.
205
+
206
+ Dependencies are only in-memory and should be reflecting database dependencies, so
207
+ changing them in SQL config does not alter database. Such actions are persisted in separate
208
+ type operation - `AlterSQLState`.
209
+
210
+ Args:
211
+ dep_changed_keys (list): Data about keys, that have their dependencies changed.
212
+ List of tuples (key, removed depndencies, added_dependencies).
213
+ """
214
+ for key, removed_deps, added_deps in dep_changed_keys:
215
+ app_label, sql_name = key
216
+ operation = AlterSQLState(sql_name, add_dependencies=tuple(added_deps),
217
+ remove_dependencies=tuple(removed_deps))
218
+ sql_deps = [key]
219
+ self.add_sql_operation(app_label, sql_name, operation, sql_deps)
220
+
221
+ def _generate_delete_sql(self, delete_keys):
222
+ """
223
+ Generate forward delete operations for SQL items.
224
+ """
225
+ for key in delete_keys:
226
+ app_label, sql_name = key
227
+ old_node = self.from_sql_graph.nodes[key]
228
+ operation = DeleteSQL(sql_name, old_node.reverse_sql, reverse_sql=old_node.sql)
229
+ sql_deps = [n.key for n in self.from_sql_graph.node_map[key].children]
230
+ sql_deps.append(key)
231
+ self.add_sql_operation(app_label, sql_name, operation, sql_deps)
232
+
233
+ def generate_sql_changes(self):
234
+ """
235
+ Starting point of this tool, which identifies changes and generates respective
236
+ operations.
237
+ """
238
+ from_keys = set(self.from_sql_graph.nodes.keys())
239
+ to_keys = set(self.to_sql_graph.nodes.keys())
240
+ new_keys = to_keys - from_keys
241
+ delete_keys = from_keys - to_keys
242
+ changed_keys = set()
243
+ dep_changed_keys = []
244
+
245
+ for key in from_keys & to_keys:
246
+ old_node = self.from_sql_graph.nodes[key]
247
+ new_node = self.to_sql_graph.nodes[key]
248
+
249
+ # identify SQL changes -- these will alter database.
250
+ if not is_sql_equal(old_node.sql, new_node.sql):
251
+ changed_keys.add(key)
252
+
253
+ # identify dependencies change
254
+ old_deps = self.from_sql_graph.dependencies[key]
255
+ new_deps = self.to_sql_graph.dependencies[key]
256
+ removed_deps = old_deps - new_deps
257
+ added_deps = new_deps - old_deps
258
+ if removed_deps or added_deps:
259
+ dep_changed_keys.append((key, removed_deps, added_deps))
260
+
261
+ # we do basic sort here and inject dependency keys here.
262
+ # operations built using these keys will properly set operation dependencies which will
263
+ # enforce django to build/keep a correct order of operations (stable_topological_sort).
264
+ keys = self.assemble_changes(new_keys, changed_keys, self.to_sql_graph)
265
+ delete_keys = self.assemble_changes(delete_keys, set(), self.from_sql_graph)
266
+
267
+ self._sql_operations = {}
268
+ self._generate_reversed_sql(keys, changed_keys)
269
+ self._generate_sql(keys, changed_keys)
270
+ self._generate_delete_sql(delete_keys)
271
+ self._generate_altered_sql_dependencies(dep_changed_keys)
272
+
273
+ def check_dependency(self, operation, dependency):
274
+ """
275
+ Enhances default behavior of method by checking dependency for matching operation.
276
+ """
277
+ if django.VERSION >= (5, 1):
278
+ # dependency is a named tuple
279
+ if isinstance(dependency.model_name, SQLBlob):
280
+ # NOTE: we follow the sort order created by `assemble_changes` so we build a fixed chain
281
+ # of operations. thus we should match exact operation here.
282
+ return dependency.type == operation
283
+ else:
284
+ # dependency is a tuple
285
+ if isinstance(dependency[1], SQLBlob):
286
+ # NOTE: we follow the sort order created by `assemble_changes` so we build a fixed chain
287
+ # of operations. thus we should match exact operation here.
288
+ return dependency[3] == operation
289
+ return super(MigrationAutodetector, self).check_dependency(operation, dependency)
290
+
291
+ def generate_altered_fields(self):
292
+ """
293
+ Injecting point. This is quite awkward, and i'm looking forward Django for having the logic
294
+ divided into smaller methods/functions for easier enhancement and substitution.
295
+ So far we're doing all the SQL magic in this method.
296
+ """
297
+ result = super(MigrationAutodetector, self).generate_altered_fields()
298
+ self.generate_sql_changes()
299
+ return result
300
+
301
+ @staticmethod
302
+ def _resolve_dependency(dependency):
303
+ """
304
+ Return the resolved dependency and a boolean denoting whether or not
305
+ it was swappable.
306
+ """
307
+ if dependency[0] != "__setting__":
308
+ return dependency, False
309
+ resolved_app_label, resolved_object_name = getattr(
310
+ settings, dependency[1]
311
+ ).split(".")
312
+ if django.VERSION >= (5, 1):
313
+ return (
314
+ OperationDependency(
315
+ resolved_app_label,
316
+ resolved_object_name.lower(),
317
+ dependency[2],
318
+ dependency[3],
319
+ ),
320
+ True,
321
+ )
322
+ else:
323
+ return (
324
+ (resolved_app_label, resolved_object_name.lower()) + dependency[2:],
325
+ True
326
+ )
@@ -0,0 +1,25 @@
1
+ class SQLItem(object):
2
+ """
3
+ Represents any SQL entity (unit), for example function, type, index or trigger.
4
+ """
5
+ def __init__(self, name, sql, reverse_sql=None, dependencies=None, replace=False):
6
+ """
7
+ Args:
8
+ name (str): Name of the SQL item. Should be unique among other items in the current
9
+ application. It is the name that other items can refer to.
10
+ sql (str/tuple): Forward SQL that creates entity.
11
+ drop_sql (str/tuple, optional): Backward SQL that destroyes entity. (DROPs).
12
+ dependencies (list, optional): Collection of item keys, that the current one depends on.
13
+ Each element is a tuple of two: (app, item_name). Order does not matter.
14
+ replace (bool, optional): If `True`, further migrations will not drop previous version
15
+ of item before creating, assuming that a forward SQL replaces. For example Postgre's
16
+ `create or replace function` which does not require dropping it previously.
17
+ If `False` then each changed item will get two operations: dropping previous version
18
+ and creating new one.
19
+ Default = `False`.
20
+ """
21
+ self.name = name
22
+ self.sql = sql
23
+ self.reverse_sql = reverse_sql
24
+ self.dependencies = dependencies or []
25
+ self.replace = replace
@@ -0,0 +1,119 @@
1
+ from collections import defaultdict
2
+ from importlib import import_module
3
+
4
+ from django.db.migrations.graph import Node, NodeNotFoundError, CircularDependencyError
5
+ from django.conf import settings
6
+ from django.apps import apps
7
+
8
+ SQL_CONFIG_MODULE = settings.__dict__.get('SQL_CONFIG_MODULE', 'sql_config')
9
+
10
+
11
+ class SQLStateGraph(object):
12
+ """
13
+ Represents graph assembled by SQL items as nodes and parent-child relations as arcs.
14
+ """
15
+ def __init__(self):
16
+ self.nodes = {}
17
+ self.node_map = {}
18
+ self.dependencies = defaultdict(set)
19
+
20
+ def remove_node(self, key):
21
+ # XXX: Workaround for Issue #2
22
+ # Silences state aggregation problem in `migrate` command.
23
+ if key in self.nodes and key in self.node_map:
24
+ del self.nodes[key]
25
+ del self.node_map[key]
26
+
27
+ def add_node(self, key, sql_item):
28
+ node = Node(key)
29
+ self.node_map[key] = node
30
+ self.nodes[key] = sql_item
31
+
32
+ def add_lazy_dependency(self, child, parent):
33
+ """
34
+ Add dependency to be resolved and applied later.
35
+ """
36
+ self.dependencies[child].add(parent)
37
+
38
+ def remove_lazy_dependency(self, child, parent):
39
+ """
40
+ Add dependency to be resolved and applied later.
41
+ """
42
+ self.dependencies[child].remove(parent)
43
+
44
+ def remove_lazy_for_child(self, child):
45
+ """
46
+ Remove dependency to be resolved and applied later.
47
+ """
48
+ if child in self.dependencies:
49
+ del self.dependencies[child]
50
+
51
+ def build_graph(self):
52
+ """
53
+ Read lazy dependency list and build graph.
54
+ """
55
+ for child, parents in self.dependencies.items():
56
+ if child not in self.nodes:
57
+ raise NodeNotFoundError(
58
+ "App %s SQL item dependencies reference nonexistent child node %r" % (
59
+ child[0], child),
60
+ child
61
+ )
62
+ for parent in parents:
63
+ if parent not in self.nodes:
64
+ raise NodeNotFoundError(
65
+ "App %s SQL item dependencies reference nonexistent parent node %r" % (
66
+ child[0], parent),
67
+ parent
68
+ )
69
+ self.node_map[child].add_parent(self.node_map[parent])
70
+ self.node_map[parent].add_child(self.node_map[child])
71
+
72
+ for node in self.nodes:
73
+ self.ensure_not_cyclic(node,
74
+ lambda x: (parent.key for parent in self.node_map[x].parents))
75
+
76
+ def ensure_not_cyclic(self, start, get_children):
77
+ # Algo from GvR:
78
+ # http://neopythonic.blogspot.co.uk/2009/01/detecting-cycles-in-directed-graph.html
79
+ todo = set(self.nodes)
80
+ while todo:
81
+ node = todo.pop()
82
+ stack = [node]
83
+ while stack:
84
+ top = stack[-1]
85
+ for node in get_children(top):
86
+ if node in stack:
87
+ cycle = stack[stack.index(node):]
88
+ raise CircularDependencyError(", ".join("%s.%s" % n for n in cycle))
89
+ if node in todo:
90
+ stack.append(node)
91
+ todo.remove(node)
92
+ break
93
+ else:
94
+ node = stack.pop()
95
+
96
+
97
+ def build_current_graph():
98
+ """
99
+ Read current state of SQL items from the current project state.
100
+
101
+ Returns:
102
+ (SQLStateGraph) Current project state graph.
103
+ """
104
+ graph = SQLStateGraph()
105
+ for app_name, config in apps.app_configs.items():
106
+ try:
107
+ module = import_module(
108
+ '.'.join((config.module.__name__, SQL_CONFIG_MODULE)))
109
+ sql_items = module.sql_items
110
+ except (ImportError, AttributeError):
111
+ continue
112
+ for sql_item in sql_items:
113
+ graph.add_node((app_name, sql_item.name), sql_item)
114
+
115
+ for dep in sql_item.dependencies:
116
+ graph.add_lazy_dependency((app_name, sql_item.name), dep)
117
+
118
+ graph.build_graph()
119
+ return graph
File without changes
File without changes
@@ -0,0 +1,143 @@
1
+ """
2
+ Replaces built-in Django command and forces it generate SQL item modification operations
3
+ into regular Django migrations.
4
+ """
5
+
6
+ import sys
7
+
8
+ import django
9
+ from django.core.management.commands.makemigrations import Command as MakeMigrationsCommand
10
+ from django.db.migrations.loader import MigrationLoader
11
+ from django.db.migrations import Migration
12
+ from django.core.management.base import CommandError, no_translations
13
+ from django.db.migrations.questioner import InteractiveMigrationQuestioner
14
+ from django.apps import apps
15
+ from django.db.migrations.state import ProjectState
16
+
17
+ from ...autodetector import MigrationAutodetector
18
+ from ...graph import build_current_graph
19
+
20
+
21
+ class Command(MakeMigrationsCommand):
22
+
23
+ @no_translations
24
+ def handle(self, *app_labels, **options):
25
+ if django.VERSION >= (4, 1):
26
+ self.written_files = []
27
+ self.verbosity = options.get('verbosity')
28
+ self.interactive = options.get('interactive')
29
+ self.dry_run = options.get('dry_run', False)
30
+ self.merge = options.get('merge', False)
31
+ self.empty = options.get('empty', False)
32
+ self.migration_name = options.get('name', None)
33
+ self.exit_code = options.get('exit_code', False)
34
+ self.include_header = options.get('include_header', True)
35
+ check_changes = options.get('check_changes', False)
36
+ if django.VERSION >= (4, 1):
37
+ self.scriptable = options["scriptable"]
38
+ # If logs and prompts are diverted to stderr, remove the ERROR style.
39
+ if self.scriptable:
40
+ self.stderr.style_func = None
41
+
42
+ # Make sure the app they asked for exists
43
+ app_labels = set(app_labels)
44
+ bad_app_labels = set()
45
+ for app_label in app_labels:
46
+ try:
47
+ apps.get_app_config(app_label)
48
+ except LookupError:
49
+ bad_app_labels.add(app_label)
50
+ if bad_app_labels:
51
+ for app_label in bad_app_labels:
52
+ self.stderr.write("App '%s' could not be found. Is it in INSTALLED_APPS?" % app_label)
53
+ sys.exit(2)
54
+
55
+ # Load the current graph state. Pass in None for the connection so
56
+ # the loader doesn't try to resolve replaced migrations from DB.
57
+ loader = MigrationLoader(None, ignore_no_migrations=True)
58
+
59
+ # Before anything else, see if there's conflicting apps and drop out
60
+ # hard if there are any and they don't want to merge
61
+ conflicts = loader.detect_conflicts()
62
+
63
+ # If app_labels is specified, filter out conflicting migrations for unspecified apps
64
+ if app_labels:
65
+ conflicts = {
66
+ app_label: conflict for app_label, conflict in conflicts.items()
67
+ if app_label in app_labels
68
+ }
69
+
70
+ if conflicts and not self.merge:
71
+ name_str = "; ".join(
72
+ "%s in %s" % (", ".join(names), app)
73
+ for app, names in conflicts.items()
74
+ )
75
+ raise CommandError(
76
+ "Conflicting migrations detected (%s).\nTo fix them run "
77
+ "'python manage.py makemigrations --merge'" % name_str
78
+ )
79
+
80
+ # If they want to merge and there's nothing to merge, then politely exit
81
+ if self.merge and not conflicts:
82
+ self.stdout.write("No conflicts detected to merge.")
83
+ return
84
+
85
+ # If they want to merge and there is something to merge, then
86
+ # divert into the merge code
87
+ if self.merge and conflicts:
88
+ return self.handle_merge(loader, conflicts)
89
+
90
+ state = loader.project_state()
91
+
92
+ # NOTE: customization. Passing graph to autodetector.
93
+ sql_graph = build_current_graph()
94
+
95
+ # Set up autodetector
96
+ autodetector = MigrationAutodetector(
97
+ state,
98
+ ProjectState.from_apps(apps),
99
+ InteractiveMigrationQuestioner(specified_apps=app_labels, dry_run=self.dry_run),
100
+ sql_graph,
101
+ )
102
+
103
+ # If they want to make an empty migration, make one for each app
104
+ if self.empty:
105
+ if not app_labels:
106
+ raise CommandError("You must supply at least one app label when using --empty.")
107
+ # Make a fake changes() result we can pass to arrange_for_graph
108
+ changes = {
109
+ app: [Migration("custom", app)]
110
+ for app in app_labels
111
+ }
112
+ changes = autodetector.arrange_for_graph(
113
+ changes=changes,
114
+ graph=loader.graph,
115
+ migration_name=self.migration_name,
116
+ )
117
+ self.write_migration_files(changes)
118
+ return
119
+
120
+ # Detect changes
121
+ changes = autodetector.changes(
122
+ graph=loader.graph,
123
+ trim_to_apps=app_labels or None,
124
+ convert_apps=app_labels or None,
125
+ migration_name=self.migration_name,
126
+ )
127
+
128
+ if not changes:
129
+ # No changes? Tell them.
130
+ if self.verbosity >= 1:
131
+ if len(app_labels) == 1:
132
+ self.stdout.write("No changes detected in app '%s'" % app_labels.pop())
133
+ elif len(app_labels) > 1:
134
+ self.stdout.write("No changes detected in apps '%s'" % ("', '".join(app_labels)))
135
+ else:
136
+ self.stdout.write("No changes detected")
137
+
138
+ if self.exit_code:
139
+ sys.exit(1)
140
+ else:
141
+ self.write_migration_files(changes)
142
+ if check_changes:
143
+ sys.exit(1)
@@ -0,0 +1,221 @@
1
+ import django
2
+ from django.db.migrations.operations import RunSQL
3
+ from django.db.migrations.operations.base import Operation
4
+ if django.VERSION >= (5, 1):
5
+ from django.db.migrations.operations.base import OperationCategory
6
+
7
+ from .graph import SQLStateGraph
8
+ from .config import SQLItem
9
+
10
+
11
+ class MigrateSQLMixin(object):
12
+ def get_sql_state(self, state):
13
+ """
14
+ Get SQLStateGraph from state.
15
+ """
16
+ if not hasattr(state, 'sql_state'):
17
+ setattr(state, 'sql_state', SQLStateGraph())
18
+ return state.sql_state
19
+
20
+
21
+ class AlterSQLState(MigrateSQLMixin, Operation):
22
+ """
23
+ Alters in-memory state of SQL item.
24
+ This operation is generated separately from others since it does not affect database.
25
+ """
26
+
27
+ if django.VERSION >= (5, 1):
28
+ category = OperationCategory.PYTHON
29
+
30
+ def describe(self):
31
+ return 'Alter SQL state "{name}"'.format(name=self.name)
32
+
33
+ def deconstruct(self):
34
+ kwargs = {
35
+ 'name': self.name,
36
+ }
37
+ if self.add_dependencies:
38
+ kwargs['add_dependencies'] = self.add_dependencies
39
+ if self.remove_dependencies:
40
+ kwargs['remove_dependencies'] = self.remove_dependencies
41
+ return (self.__class__.__name__, [], kwargs)
42
+
43
+ def state_forwards(self, app_label, state):
44
+ sql_state = self.get_sql_state(state)
45
+ key = (app_label, self.name)
46
+
47
+ if key not in sql_state.nodes:
48
+ # XXX: dummy for `migrate` command, that does not preserve state object.
49
+ # Should fail with error when fixed.
50
+ return
51
+
52
+ sql_item = sql_state.nodes[key]
53
+
54
+ for dep in self.add_dependencies:
55
+ # we are also adding relations to aggregated SQLItem, but only to restore
56
+ # original items. Still using graph for advanced node/arc manipulations.
57
+
58
+ # XXX: dummy `if` for `migrate` command, that does not preserve state object.
59
+ # Fail with error when fixed
60
+ if dep in sql_item.dependencies:
61
+ sql_item.dependencies.remove(dep)
62
+ sql_state.add_lazy_dependency(key, dep)
63
+
64
+ for dep in self.remove_dependencies:
65
+ sql_item.dependencies.append(dep)
66
+ sql_state.remove_lazy_dependency(key, dep)
67
+
68
+ def database_forwards(self, app_label, schema_editor, from_state, to_state):
69
+ pass
70
+
71
+ def database_backwards(self, app_label, schema_editor, from_state, to_state):
72
+ pass
73
+
74
+ @property
75
+ def reversible(self):
76
+ return True
77
+
78
+ def __init__(self, name, add_dependencies=None, remove_dependencies=None):
79
+ """
80
+ Args:
81
+ name (str): Name of SQL item in current application to alter state for.
82
+ add_dependencies (list):
83
+ Unordered list of dependencies to add to state.
84
+ remove_dependencies (list):
85
+ Unordered list of dependencies to remove from state.
86
+ """
87
+ self.name = name
88
+ self.add_dependencies = add_dependencies or ()
89
+ self.remove_dependencies = remove_dependencies or ()
90
+
91
+
92
+ class BaseAlterSQL(MigrateSQLMixin, RunSQL):
93
+ """
94
+ Base class for operations that alter database.
95
+ """
96
+
97
+ if django.VERSION >= (5, 1):
98
+ category = OperationCategory.SQL
99
+
100
+ def __init__(self, name, sql, reverse_sql=None, state_operations=None, hints=None):
101
+ super(BaseAlterSQL, self).__init__(sql, reverse_sql=reverse_sql,
102
+ state_operations=state_operations, hints=hints)
103
+ self.name = name
104
+
105
+ def deconstruct(self):
106
+ name, args, kwargs = super(BaseAlterSQL, self).deconstruct()
107
+ kwargs['name'] = self.name
108
+ return (name, args, kwargs)
109
+
110
+
111
+ class ReverseAlterSQL(BaseAlterSQL):
112
+
113
+ if django.VERSION >= (5, 1):
114
+ category = OperationCategory.ALTERATION
115
+
116
+ def describe(self):
117
+ return 'Reverse alter SQL "{name}"'.format(name=self.name)
118
+
119
+
120
+ class AlterSQL(BaseAlterSQL):
121
+ """
122
+ Updates SQL item with a new version.
123
+ """
124
+
125
+ if django.VERSION >= (5, 1):
126
+ category = OperationCategory.ALTERATION
127
+
128
+ def __init__(self, name, sql, reverse_sql=None, state_operations=None, hints=None,
129
+ state_reverse_sql=None):
130
+ """
131
+ Args:
132
+ name (str): Name of SQL item in current application to alter state for.
133
+ sql (str/list): Forward SQL for item creation.
134
+ reverse_sql (str/list): Backward SQL for reversing create operation.
135
+ state_reverse_sql (str/list): Backward SQL used to alter state of backward SQL
136
+ *instead* of `reverse_sql`. Used for operations generated for items with
137
+ `replace` = `True`.
138
+ """
139
+ super(AlterSQL, self).__init__(name, sql, reverse_sql=reverse_sql,
140
+ state_operations=state_operations, hints=hints)
141
+ self.state_reverse_sql = state_reverse_sql
142
+
143
+ def deconstruct(self):
144
+ name, args, kwargs = super(AlterSQL, self).deconstruct()
145
+ kwargs['name'] = self.name
146
+ if self.state_reverse_sql:
147
+ kwargs['state_reverse_sql'] = self.state_reverse_sql
148
+ return (name, args, kwargs)
149
+
150
+ def describe(self):
151
+ return 'Alter SQL "{name}"'.format(name=self.name)
152
+
153
+ def state_forwards(self, app_label, state):
154
+ super(AlterSQL, self).state_forwards(app_label, state)
155
+ sql_state = self.get_sql_state(state)
156
+ key = (app_label, self.name)
157
+
158
+ if key not in sql_state.nodes:
159
+ # XXX: dummy for `migrate` command, that does not preserve state object.
160
+ # Fail with error when fixed
161
+ return
162
+
163
+ sql_item = sql_state.nodes[key]
164
+ sql_item.sql = self.sql
165
+ sql_item.reverse_sql = self.state_reverse_sql or self.reverse_sql
166
+
167
+
168
+ class CreateSQL(BaseAlterSQL):
169
+ """
170
+ Creates new SQL item in database.
171
+ """
172
+
173
+ if django.VERSION >= (5, 1):
174
+ category = OperationCategory.ADDITION
175
+
176
+ def describe(self):
177
+ return 'Create SQL "{name}"'.format(name=self.name)
178
+
179
+ def deconstruct(self):
180
+ name, args, kwargs = super(CreateSQL, self).deconstruct()
181
+ kwargs['name'] = self.name
182
+ if self.dependencies:
183
+ kwargs['dependencies'] = self.dependencies
184
+ return (name, args, kwargs)
185
+
186
+ def __init__(self, name, sql, reverse_sql=None, state_operations=None, hints=None,
187
+ dependencies=None):
188
+ super(CreateSQL, self).__init__(name, sql, reverse_sql=reverse_sql,
189
+ state_operations=state_operations, hints=hints)
190
+ self.dependencies = dependencies or ()
191
+
192
+ def state_forwards(self, app_label, state):
193
+ super(CreateSQL, self).state_forwards(app_label, state)
194
+ sql_state = self.get_sql_state(state)
195
+
196
+ sql_state.add_node(
197
+ (app_label, self.name),
198
+ SQLItem(self.name, self.sql, self.reverse_sql, list(self.dependencies)),
199
+ )
200
+
201
+ for dep in self.dependencies:
202
+ sql_state.add_lazy_dependency((app_label, self.name), dep)
203
+
204
+
205
+ class DeleteSQL(BaseAlterSQL):
206
+ """
207
+ Deltes SQL item from database.
208
+ """
209
+
210
+ if django.VERSION >= (5, 1):
211
+ category = OperationCategory.REMOVAL
212
+
213
+ def describe(self):
214
+ return 'Delete SQL "{name}"'.format(name=self.name)
215
+
216
+ def state_forwards(self, app_label, state):
217
+ super(DeleteSQL, self).state_forwards(app_label, state)
218
+ sql_state = self.get_sql_state(state)
219
+
220
+ sql_state.remove_node((app_label, self.name))
221
+ sql_state.remove_lazy_for_child((app_label, self.name))
@@ -0,0 +1,306 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-migrate-sql-deux
3
+ Version: 1.0.0
4
+ Summary: Django Migrations for raw SQL
5
+ Author-email: Bogdan Klichuk <klichukb@gmail.com>, Bruno Alla <oss@browniebroke.com>
6
+ License: BSD
7
+ Project-URL: Bug Tracker, https://github.com/browniebroke/django-migrate-sql-deux/issues
8
+ Project-URL: Changelog, https://github.com/browniebroke/django-migrate-sql-deux/blob/main/CHANGELOG.md
9
+ Project-URL: repository, https://github.com/browniebroke/django-migrate-sql-deux
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Natural Language :: English
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Framework :: Django
15
+ Classifier: Framework :: Django :: 4.2
16
+ Classifier: Framework :: Django :: 5.0
17
+ Classifier: Framework :: Django :: 5.1
18
+ Classifier: Framework :: Django :: 5.2
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Software Development :: Libraries
25
+ Requires-Python: >=3.9
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: django>=4.2
29
+ Dynamic: license-file
30
+
31
+ # django-migrate-sql-deux
32
+
33
+ <p align="center">
34
+ <a href="https://github.com/browniebroke/django-migrate-sql-deux/actions/workflows/ci.yml?query=branch%3Amain">
35
+ <img src="https://img.shields.io/github/actions/workflow/status/browniebroke/django-migrate-sql-deux/ci.yml?branch=main&label=CI&logo=github&style=flat-square" alt="CI Status" >
36
+ </a>
37
+ <a href="https://codecov.io/gh/browniebroke/django-migrate-sql-deux">
38
+ <img src="https://img.shields.io/codecov/c/github/browniebroke/django-migrate-sql-deux.svg?logo=codecov&logoColor=fff&style=flat-square" alt="Test coverage percentage">
39
+ </a>
40
+ </p>
41
+ <p align="center">
42
+ <a href="https://github.com/astral-sh/uv">
43
+ <img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json" alt="uv">
44
+ </a>
45
+ <a href="https://github.com/astral-sh/ruff">
46
+ <img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff">
47
+ </a>
48
+ <a href="https://github.com/pre-commit/pre-commit">
49
+ <img src="https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white&style=flat-square" alt="pre-commit">
50
+ </a>
51
+ </p>
52
+ <p align="center">
53
+ <a href="https://pypi.org/project/django-migrate-sql-deux/">
54
+ <img src="https://img.shields.io/pypi/v/django-migrate-sql-deux.svg?logo=python&logoColor=fff&style=flat-square" alt="PyPI Version">
55
+ </a>
56
+ <img src="https://img.shields.io/pypi/pyversions/django-migrate-sql-deux.svg?style=flat-square&logo=python&amp;logoColor=fff" alt="Supported Python versions">
57
+ <img src="https://img.shields.io/pypi/l/django-migrate-sql-deux.svg?style=flat-square" alt="License">
58
+ </p>
59
+
60
+ ---
61
+
62
+ **Source Code**: <a href="https://github.com/browniebroke/django-migrate-sql-deux" target="_blank">https://github.com/browniebroke/django-migrate-sql-deux </a>
63
+
64
+ ---
65
+
66
+ Django Migrations support for raw SQL.
67
+
68
+ > [!NOTE]
69
+ > This package is a fork of the `django-migrate-sql` package, originally published by Bogdan Klichuk. This package appears unmaintained, so we decided to start a fork as we depended on it. Most of the code is from the original author.
70
+
71
+ ## About
72
+
73
+ This tool implements mechanism for managing changes to custom SQL entities (functions, types, indices, triggers) using built-in migration mechanism. Technically creates a sophistication layer on top of the
74
+ `RunSQL` Django operation.
75
+
76
+ ## What it does
77
+
78
+ - Makes maintaining your SQL functions, custom composite types, indices and triggers easier.
79
+ - Structures SQL into configuration of **SQL items**, that are
80
+ identified by names and divided among apps, just like models.
81
+ - Automatically gathers and persists changes of your custom SQL into
82
+ migrations using `makemigrations`.
83
+ - Properly executes backwards/forwards keeping integrity of database.
84
+ - Create -> Drop -> Recreate approach for changes to items that do not
85
+ support altering and require dropping and recreating.
86
+ - Dependencies system for SQL items, which solves the problem of
87
+ updating items, that rely on others (for example custom
88
+ types/functions that use other custom types), and require dropping all
89
+ dependency tree previously with further recreation.
90
+
91
+ ## What it does not
92
+
93
+ - Does not parse SQL nor validate queries during `makemigrations` or
94
+ `migrate` because is database-agnostic. For this same reason setting
95
+ up proper dependencies is user's responsibility.
96
+ - Does not create `ALTER` queries for items that support this, for
97
+ example `ALTER TYPE` in PostgreSQL, because is database-agnostic. In
98
+ case your tools allow rolling all the changes through `ALTER` queries,
99
+ you can consider not using this app **or** restructure migrations
100
+ manually after creation by nesting generated operations into
101
+ `` `state_operations `` of [`RunSQL`](https://docs.djangoproject.com/en/1.8/ref/migration-operations/#runsql)
102
+ that does `ALTER`.
103
+ - (**TODO**)During `migrate` does not restore full state of items for
104
+ analysis, thus does not notify about existing changes to schema that
105
+ are not migrated **nor** does not recognize circular dependencies
106
+ during migration execution.
107
+
108
+ ## Installation
109
+
110
+ Install from PyPi:
111
+
112
+ ```shell
113
+ pip install django-migrate-sql-deux
114
+ ```
115
+
116
+ Add `django_migrate_sql` to `INSTALLED_APPS`:
117
+
118
+ ```python
119
+ INSTALLED_APPS = [
120
+ # ...
121
+ 'django_migrate_sql',
122
+ ]
123
+ ```
124
+
125
+ App defines a custom `makemigrations` command, that inherits from
126
+ Django's core one, so in order `django_migrate_sql` app to kick in put it
127
+ after any other apps that redefine `makemigrations` command too.
128
+
129
+ ## Usage
130
+
131
+ 1) Create `sql_config.py` module to root of a target app you want to
132
+ manage custom SQL for.
133
+ 2) Define SQL items in it (`sql_items`), for example:
134
+
135
+ ```python
136
+ # PostgreSQL example.
137
+ # Let's define a simple function and let `django_migrate_sql` manage its changes.
138
+
139
+ from django_migrate_sql.config import SQLItem
140
+
141
+ sql_items = [
142
+ SQLItem(
143
+ 'make_sum', # name of the item
144
+ 'create or replace function make_sum(a int, b int) returns int as $$ '
145
+ 'begin return a + b; end; '
146
+ '$$ language plpgsql;', # forward sql
147
+ reverse_sql='drop function make_sum(int, int);', # sql for removal
148
+ ),
149
+ ]
150
+ ```
151
+
152
+ 3) Create migration `python manage.py makemigrations`:
153
+
154
+ Migrations for 'app_name':
155
+ 0002_auto_xxxx.py:
156
+ - Create SQL "make_sum"
157
+
158
+ You can take a look at content this generated:
159
+
160
+ ```python
161
+ from django.db import migrations, models
162
+ import django_migrate_sql.operations
163
+
164
+
165
+ class Migration(migrations.Migration):
166
+ dependencies = [
167
+ ('app_name', '0001_initial'),
168
+ ]
169
+ operations = [
170
+ django_migrate_sql.operations.CreateSQL(
171
+ name='make_sum',
172
+ sql='create or replace function make_sum(a int, b int) returns int as $$ begin return a + b; end; $$ language plpgsql;',
173
+ reverse_sql='drop function make_sum(int, int);',
174
+ ),
175
+ ]
176
+ ```
177
+
178
+ 4) Execute migration `python manage.py migrate`:
179
+
180
+ Operations to perform:
181
+ Apply all migrations: app_name
182
+ Running migrations:
183
+ Rendering model states... DONE
184
+ Applying app_name.0002_xxxx... OK
185
+
186
+ Check result in `python manage.py dbshell`:
187
+
188
+ db_name=# select make_sum(12, 15);
189
+ make_sum
190
+ ----------
191
+ 27
192
+ (1 row)
193
+
194
+ Now, say, you want to change the function implementation so that it
195
+ takes a custom type as argument:
196
+
197
+ 5) Edit your `sql_config.py`:
198
+
199
+ ```python
200
+ # PostgreSQL example #2.
201
+ # Function and custom type.
202
+
203
+ from django_migrate_sql.config import SQLItem
204
+
205
+ sql_items = [
206
+ SQLItem(
207
+ 'make_sum', # name of the item
208
+ 'create or replace function make_sum(a mynum, b mynum) returns mynum as $$ '
209
+ 'begin return (a.num + b.num, 'result')::mynum; end; '
210
+ '$$ language plpgsql;', # forward sql
211
+ reverse_sql='drop function make_sum(mynum, mynum);', # sql for removal
212
+ # depends on `mynum` since takes it as argument. we won't be able to drop function
213
+ # without dropping `mynum` first.
214
+ dependencies=[('app_name', 'mynum')],
215
+ ),
216
+ SQLItem(
217
+ 'mynum' # name of the item
218
+ 'create type mynum as (num int, name varchar(20));', # forward sql
219
+ reverse_sql='drop type mynum;', # sql for removal
220
+ ),
221
+ ]
222
+ ```
223
+
224
+ 6) Generate migration `python manage.py makemigrations`:
225
+
226
+ ```
227
+ Migrations for 'app_name':
228
+ 0003_xxxx:
229
+ - Reverse alter SQL "make_sum"
230
+ - Create SQL "mynum"
231
+ - Alter SQL "make_sum"
232
+ - Alter SQL state "make_sum"
233
+ ```
234
+
235
+ You can take a look at the content this generated:
236
+
237
+ ```python
238
+ from django.db import migrations, models
239
+ import django_migrate_sql.operations
240
+
241
+
242
+ class Migration(migrations.Migration):
243
+ dependencies = [
244
+ ('app_name', '0002_xxxx'),
245
+ ]
246
+ operations = [
247
+ django_migrate_sql.operations.ReverseAlterSQL(
248
+ name='make_sum',
249
+ sql='drop function make_sum(int, int);',
250
+ reverse_sql='create or replace function make_sum(a int, b int) returns int as $$ begin return a + b; end; $$ language plpgsql;',
251
+ ),
252
+ django_migrate_sql.operations.CreateSQL(
253
+ name='mynum',
254
+ sql='create type mynum as (num int, name varchar(20));',
255
+ reverse_sql='drop type mynum;',
256
+ ),
257
+ django_migrate_sql.operations.AlterSQL(
258
+ name='make_sum',
259
+ sql='create or replace function make_sum(a mynum, b mynum) returns mynum as $$ begin return (a.num + b.num, \'result\')::mynum; end; $$ language plpgsql;',
260
+ reverse_sql='drop function make_sum(mynum, mynum);',
261
+ ),
262
+ django_migrate_sql.operations.AlterSQLState(
263
+ name='make_sum',
264
+ add_dependencies=[('app_name', 'mynum')],
265
+ ),
266
+ ]
267
+ ```
268
+
269
+ **NOTE:** Previous function is completely dropped before creation
270
+ because definition of it changed. `CREATE OR REPLACE` would create
271
+ another version of it, so `DROP` makes it clean.
272
+
273
+ **If you put `replace=True` as kwarg to an `SQLItem`
274
+ definition, it will NOT drop + create it, but just rerun forward SQL,
275
+ which is `CREATE OR REPLACE` in this example.**
276
+
277
+ 7) Execute migration `python manage.py migrate`:
278
+
279
+
280
+ ```
281
+ Operations to perform:
282
+ Apply all migrations: app_name
283
+ Running migrations:
284
+ Rendering model states... DONE
285
+ Applying brands.0003_xxxx... OK
286
+ ```
287
+
288
+ Check results:
289
+
290
+ ```
291
+ db_name=# select make_sum((5, 'a')::mynum, (3, 'b')::mynum);
292
+ make_sum
293
+ ------------
294
+ (8,result)
295
+ (1 row)
296
+
297
+ db_name=# select make_sum(12, 15);
298
+ ERROR: function make_sum(integer, integer) does not exist
299
+ LINE 1: select make_sum(12, 15);
300
+ ^
301
+ HINT: No function matches the given name and argument types. You might need to add explicit type casts.
302
+ ```
303
+
304
+ For more examples see `tests`.
305
+
306
+ Feel free to [open new issues](https://github.com/browniebroke/django-migrate-sql-deux/issues).
@@ -0,0 +1,13 @@
1
+ django_migrate_sql/__init__.py,sha256=RsZjRjMprNcDm97wqRRSk6rTLgTX8N0GyicZyZ8OsBQ,22
2
+ django_migrate_sql/autodetector.py,sha256=e_y_xFtc1SKk34dJuuQ8a0NfaGjhsgdmuD9xrflbUUQ,13501
3
+ django_migrate_sql/config.py,sha256=uYms22MPVPt3obvb9-a7TvzFgZrSK1gR4KDMGZuQlfw,1391
4
+ django_migrate_sql/graph.py,sha256=MWKiZhzuWF4sCMV3elQ8kqV2yljL4PSHHykNV-YZHOo,4111
5
+ django_migrate_sql/operations.py,sha256=8j3FO4xO9i2KXQp3wag82crTVEl7W4Wh_gQMqOsrXKE,7577
6
+ django_migrate_sql/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ django_migrate_sql/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ django_migrate_sql/management/commands/makemigrations.py,sha256=EY3m265PDgx4MKz_sx8gczqd_24Q9MobgjjoD5l_ngI,5535
9
+ django_migrate_sql_deux-1.0.0.dist-info/licenses/LICENSE,sha256=AMfBy7--KzNJGEN6xLEy_-kdwvQP70Ck5y7jLGYmPrs,754
10
+ django_migrate_sql_deux-1.0.0.dist-info/METADATA,sha256=F84DGVO25FRWnRBzOVWtIVr1cd7b5HRDCuuiWVVFWTY,11064
11
+ django_migrate_sql_deux-1.0.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
12
+ django_migrate_sql_deux-1.0.0.dist-info/top_level.txt,sha256=At0os64aXXqAwg_WvAa6d9BIoiYXjhHeC84IaUVre3Q,19
13
+ django_migrate_sql_deux-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (78.1.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2016, Bogdan Klichuk <klichukb@gmail.com>
2
+
3
+ Permission to use, copy, modify, and/or distribute this software for any
4
+ purpose with or without fee is hereby granted, provided that the above
5
+ copyright notice and this permission notice appear in all copies.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
@@ -0,0 +1 @@
1
+ django_migrate_sql