crochet-migration 0.1.0__py3-none-any.whl → 0.1.1__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.
@@ -153,6 +153,7 @@ class MigrationEngine:
153
153
  # Compute schema hash and diff
154
154
  schema_hash = ""
155
155
  diff_summary = ""
156
+ diff_obj = None
156
157
  if current_snapshot is not None:
157
158
  current_snapshot = hash_snapshot(current_snapshot)
158
159
  schema_hash = current_snapshot.schema_hash
@@ -166,9 +167,9 @@ class MigrationEngine:
166
167
  prev_json = self._ledger.get_snapshot(prev_hash)
167
168
  if prev_json:
168
169
  prev_snapshot = SchemaSnapshot.from_json(prev_json)
169
- diff = diff_snapshots(prev_snapshot, current_snapshot)
170
- if diff.has_changes:
171
- diff_summary = diff.summary()
170
+ diff_obj = diff_snapshots(prev_snapshot, current_snapshot)
171
+ if diff_obj.has_changes:
172
+ diff_summary = diff_obj.summary()
172
173
 
173
174
  content = render_migration(
174
175
  revision_id=revision_id,
@@ -177,6 +178,7 @@ class MigrationEngine:
177
178
  schema_hash=schema_hash,
178
179
  rollback_safe=rollback_safe,
179
180
  diff_summary=diff_summary,
181
+ diff=diff_obj,
180
182
  )
181
183
 
182
184
  return write_migration_file(
@@ -5,6 +5,10 @@ from __future__ import annotations
5
5
  import re
6
6
  from datetime import datetime, timezone
7
7
  from pathlib import Path
8
+ from typing import TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ from crochet.ir.diff import SchemaDiff
8
12
 
9
13
  _MIGRATION_TEMPLATE = '''\
10
14
  """
@@ -52,6 +56,189 @@ def generate_revision_id(seq: int, description: str) -> str:
52
56
  return f"{seq:04d}_{slug}"
53
57
 
54
58
 
59
+ def generate_operations_from_diff(diff: "SchemaDiff") -> tuple[str, str]:
60
+ """Generate upgrade and downgrade operation code from a SchemaDiff.
61
+
62
+ Returns a tuple of (upgrade_code, downgrade_code) as strings.
63
+ """
64
+ upgrade_lines: list[str] = []
65
+ downgrade_lines: list[str] = []
66
+
67
+ # Process node changes
68
+ for nc in diff.node_changes:
69
+ if nc.kind == "added":
70
+ # Node added - no automatic operations (user should handle data)
71
+ upgrade_lines.append(f"# TODO: Handle new node '{nc.new.label}' (kgid={nc.kgid})")
72
+ downgrade_lines.append(f"# TODO: Clean up node '{nc.new.label}' (kgid={nc.kgid})")
73
+ elif nc.kind == "removed":
74
+ # Node removed - no automatic operations (user should handle data)
75
+ upgrade_lines.append(f"# TODO: Handle removed node '{nc.old.label}' (kgid={nc.kgid})")
76
+ downgrade_lines.append(f"# TODO: Restore node '{nc.old.label}' (kgid={nc.kgid})")
77
+ elif nc.kind == "modified":
78
+ label = nc.new.label if nc.new else nc.old.label
79
+
80
+ # Handle label rename
81
+ if nc.label_renamed and nc.old and nc.new:
82
+ upgrade_lines.append(
83
+ f'ctx.rename_label("{nc.old.label}", "{nc.new.label}")'
84
+ )
85
+ downgrade_lines.append(
86
+ f'ctx.rename_label("{nc.new.label}", "{nc.old.label}")'
87
+ )
88
+
89
+ # Handle property changes
90
+ for pc in nc.property_changes:
91
+ if pc.kind == "added":
92
+ # Property added
93
+ upgrade_lines.append(
94
+ f'ctx.add_node_property("{label}", "{pc.property_name}")'
95
+ )
96
+ downgrade_lines.append(
97
+ f'ctx.remove_node_property("{label}", "{pc.property_name}")'
98
+ )
99
+
100
+ # Handle constraints/indexes for new property
101
+ if pc.new:
102
+ if pc.new.unique_index:
103
+ upgrade_lines.append(
104
+ f'ctx.add_unique_constraint("{label}", "{pc.property_name}")'
105
+ )
106
+ downgrade_lines.append(
107
+ f'ctx.drop_unique_constraint("{label}", "{pc.property_name}")'
108
+ )
109
+ elif pc.new.index:
110
+ upgrade_lines.append(
111
+ f'ctx.add_index("{label}", "{pc.property_name}")'
112
+ )
113
+ downgrade_lines.append(
114
+ f'ctx.drop_index("{label}", "{pc.property_name}")'
115
+ )
116
+ if pc.new.required:
117
+ upgrade_lines.append(
118
+ f'ctx.add_node_property_existence_constraint("{label}", "{pc.property_name}")'
119
+ )
120
+ downgrade_lines.append(
121
+ f'ctx.drop_node_property_existence_constraint("{label}", "{pc.property_name}")'
122
+ )
123
+
124
+ elif pc.kind == "removed":
125
+ # Property removed
126
+ upgrade_lines.append(
127
+ f'ctx.remove_node_property("{label}", "{pc.property_name}")'
128
+ )
129
+ downgrade_lines.append(
130
+ f'ctx.add_node_property("{label}", "{pc.property_name}")'
131
+ )
132
+
133
+ # Handle constraints/indexes for removed property
134
+ if pc.old:
135
+ if pc.old.unique_index:
136
+ upgrade_lines.append(
137
+ f'ctx.drop_unique_constraint("{label}", "{pc.property_name}")'
138
+ )
139
+ downgrade_lines.append(
140
+ f'ctx.add_unique_constraint("{label}", "{pc.property_name}")'
141
+ )
142
+ elif pc.old.index:
143
+ upgrade_lines.append(
144
+ f'ctx.drop_index("{label}", "{pc.property_name}")'
145
+ )
146
+ downgrade_lines.append(
147
+ f'ctx.add_index("{label}", "{pc.property_name}")'
148
+ )
149
+ if pc.old.required:
150
+ upgrade_lines.append(
151
+ f'ctx.drop_node_property_existence_constraint("{label}", "{pc.property_name}")'
152
+ )
153
+ downgrade_lines.append(
154
+ f'ctx.add_node_property_existence_constraint("{label}", "{pc.property_name}")'
155
+ )
156
+
157
+ elif pc.kind == "modified" and pc.old and pc.new:
158
+ # Property modified - handle constraint/index changes
159
+ if pc.old.unique_index != pc.new.unique_index:
160
+ if pc.new.unique_index:
161
+ upgrade_lines.append(
162
+ f'ctx.add_unique_constraint("{label}", "{pc.property_name}")'
163
+ )
164
+ downgrade_lines.append(
165
+ f'ctx.drop_unique_constraint("{label}", "{pc.property_name}")'
166
+ )
167
+ else:
168
+ upgrade_lines.append(
169
+ f'ctx.drop_unique_constraint("{label}", "{pc.property_name}")'
170
+ )
171
+ downgrade_lines.append(
172
+ f'ctx.add_unique_constraint("{label}", "{pc.property_name}")'
173
+ )
174
+
175
+ if pc.old.index != pc.new.index:
176
+ if pc.new.index:
177
+ upgrade_lines.append(
178
+ f'ctx.add_index("{label}", "{pc.property_name}")'
179
+ )
180
+ downgrade_lines.append(
181
+ f'ctx.drop_index("{label}", "{pc.property_name}")'
182
+ )
183
+ else:
184
+ upgrade_lines.append(
185
+ f'ctx.drop_index("{label}", "{pc.property_name}")'
186
+ )
187
+ downgrade_lines.append(
188
+ f'ctx.add_index("{label}", "{pc.property_name}")'
189
+ )
190
+
191
+ if pc.old.required != pc.new.required:
192
+ if pc.new.required:
193
+ upgrade_lines.append(
194
+ f'ctx.add_node_property_existence_constraint("{label}", "{pc.property_name}")'
195
+ )
196
+ downgrade_lines.append(
197
+ f'ctx.drop_node_property_existence_constraint("{label}", "{pc.property_name}")'
198
+ )
199
+ else:
200
+ upgrade_lines.append(
201
+ f'ctx.drop_node_property_existence_constraint("{label}", "{pc.property_name}")'
202
+ )
203
+ downgrade_lines.append(
204
+ f'ctx.add_node_property_existence_constraint("{label}", "{pc.property_name}")'
205
+ )
206
+
207
+ # Process relationship changes
208
+ for rc in diff.relationship_changes:
209
+ if rc.kind == "added":
210
+ upgrade_lines.append(f"# TODO: Handle new relationship '{rc.new.rel_type}' (kgid={rc.kgid})")
211
+ downgrade_lines.append(f"# TODO: Clean up relationship '{rc.new.rel_type}' (kgid={rc.kgid})")
212
+ elif rc.kind == "removed":
213
+ upgrade_lines.append(f"# TODO: Handle removed relationship '{rc.old.rel_type}' (kgid={rc.kgid})")
214
+ downgrade_lines.append(f"# TODO: Restore relationship '{rc.old.rel_type}' (kgid={rc.kgid})")
215
+ elif rc.kind == "modified":
216
+ # Handle relationship property changes similarly to nodes
217
+ rel_type = rc.new.rel_type if rc.new else rc.old.rel_type
218
+ for pc in rc.property_changes:
219
+ if pc.kind == "added":
220
+ upgrade_lines.append(
221
+ f'# TODO: Add property "{pc.property_name}" to relationship {rel_type}'
222
+ )
223
+ elif pc.kind == "removed":
224
+ upgrade_lines.append(
225
+ f'# TODO: Remove property "{pc.property_name}" from relationship {rel_type}'
226
+ )
227
+
228
+ # Format the code
229
+ if upgrade_lines:
230
+ upgrade_code = " " + "\n ".join(upgrade_lines)
231
+ else:
232
+ upgrade_code = " pass"
233
+
234
+ if downgrade_lines:
235
+ downgrade_code = " " + "\n ".join(downgrade_lines)
236
+ else:
237
+ downgrade_code = " pass"
238
+
239
+ return upgrade_code, downgrade_code
240
+
241
+
55
242
  def render_migration(
56
243
  revision_id: str,
57
244
  parent_id: str | None,
@@ -59,11 +246,23 @@ def render_migration(
59
246
  schema_hash: str,
60
247
  rollback_safe: bool = True,
61
248
  diff_summary: str = "",
249
+ diff: "SchemaDiff | None" = None,
62
250
  ) -> str:
63
251
  """Render a migration file from template."""
64
252
  now = datetime.now(timezone.utc).isoformat()
65
253
 
66
- if diff_summary:
254
+ # Generate operations from diff if available
255
+ if diff is not None and diff.has_changes:
256
+ upgrade_lines, downgrade_lines = generate_operations_from_diff(diff)
257
+ # Prepend comment header showing what was detected
258
+ if diff_summary:
259
+ comment_header = _DIFF_COMMENT_HEADER
260
+ for line in diff_summary.splitlines():
261
+ comment_header += f" # {line}\n"
262
+ upgrade_lines = comment_header + "\n" + upgrade_lines
263
+ downgrade_lines = comment_header + "\n" + downgrade_lines
264
+ elif diff_summary:
265
+ # Fallback to old behavior if only summary is provided
67
266
  upgrade_lines = _DIFF_COMMENT_HEADER
68
267
  for line in diff_summary.splitlines():
69
268
  upgrade_lines += f" # {line}\n"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crochet-migration
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Versioned schema & data migrations for neomodel Neo4j graphs
5
5
  Project-URL: Homepage, https://github.com/keshavd/crochet
6
6
  Project-URL: Repository, https://github.com/keshavd/crochet
@@ -52,7 +52,7 @@ graph.
52
52
  ## Installation
53
53
 
54
54
  ```bash
55
- pip install crochet
55
+ pip install crochet-migration
56
56
  ```
57
57
 
58
58
  For development:
@@ -13,14 +13,14 @@ crochet/ir/schema.py,sha256=0MTOWGAmtWDVGCSfUEAdHXNM-lriyeT3gCUbsF3OsSc,6160
13
13
  crochet/ledger/__init__.py,sha256=xXaClHCWAbo36NfYMo41VftPo3O7kPpIyW-wj4Lwaqw,133
14
14
  crochet/ledger/sqlite.py,sha256=yGy4dDh5KPC5-N3HVaK1FpD7GYixoM2M5_IDRaEROvU,9421
15
15
  crochet/migrations/__init__.py,sha256=-112mqAx8xTQzu33lA3mmiZAP6v_H0_tPlOpYRazVeE,204
16
- crochet/migrations/engine.py,sha256=CyqtjWJbFPuIRfgMZ30ng1D7RUJGR0-i8Z1lxq5Z6sc,9563
16
+ crochet/migrations/engine.py,sha256=YmZ1OuaYmTWof5cp3HZbLKQVhUHARO1OScvMgePEt5w,9626
17
17
  crochet/migrations/operations.py,sha256=iv2aFF5w-X5K2y5fqB-w8CuSNN15eEswDHyOJIiamrY,10360
18
- crochet/migrations/template.py,sha256=1vca5mDkQKt6B4M5mLn3NZErSZAannfkUuf1LPjdU3U,2673
18
+ crochet/migrations/template.py,sha256=A_cZ_ECD5vybg_GaoB3uYydfail-Ygm8hzG915EBsGA,12637
19
19
  crochet/scaffold/__init__.py,sha256=ZPyyQ3_s2uwFaw8kdpsYT7q4Br5TdU6KVCJbc0hhslo,236
20
20
  crochet/scaffold/node.py,sha256=vXIqyYUj1Ot4qPUIwcwXsFKJsdcIbEh8Y71ASgWH4nY,1266
21
21
  crochet/scaffold/relationship.py,sha256=eHKAt0olYcGXAA5TdnIl2DOZpN31GK9y-hJFutF2Tqg,1373
22
- crochet_migration-0.1.0.dist-info/METADATA,sha256=tGQyC-qLHV0VbIW-u77FU10-EB6_ZXc3hPKa-hW2Tvc,8285
23
- crochet_migration-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
24
- crochet_migration-0.1.0.dist-info/entry_points.txt,sha256=wKTVp7ky8zuaFIMskw4FEVSppYDoKG-6CV1joyCKSC8,45
25
- crochet_migration-0.1.0.dist-info/licenses/LICENSE,sha256=plFEmT-Ix7lZ5QZvnBsTTETSVDcBhM9sY8lWCxU6llg,1068
26
- crochet_migration-0.1.0.dist-info/RECORD,,
22
+ crochet_migration-0.1.1.dist-info/METADATA,sha256=OzS8_dvoN65VJAfMiiZn3B8-q9zb7Gy3O4_MRO_i4HU,8295
23
+ crochet_migration-0.1.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
24
+ crochet_migration-0.1.1.dist-info/entry_points.txt,sha256=wKTVp7ky8zuaFIMskw4FEVSppYDoKG-6CV1joyCKSC8,45
25
+ crochet_migration-0.1.1.dist-info/licenses/LICENSE,sha256=plFEmT-Ix7lZ5QZvnBsTTETSVDcBhM9sY8lWCxU6llg,1068
26
+ crochet_migration-0.1.1.dist-info/RECORD,,