|
2 | 2 |
|
3 | 3 | from copy import deepcopy |
4 | 4 | from pathlib import Path |
5 | | -from typing import Any, Dict, List, Optional |
| 5 | +from typing import Any, Dict, List, Optional, Tuple |
6 | 6 |
|
7 | 7 | import yaml |
8 | 8 |
|
9 | 9 | from redisvl.index import SearchIndex |
10 | 10 | from redisvl.migration.models import ( |
11 | 11 | DiffClassification, |
| 12 | + FieldRename, |
12 | 13 | KeyspaceSnapshot, |
13 | 14 | MigrationPlan, |
| 15 | + RenameOperations, |
14 | 16 | SchemaPatch, |
15 | 17 | SourceSnapshot, |
16 | 18 | ) |
@@ -94,16 +96,28 @@ def create_plan_from_patch( |
94 | 96 | ) |
95 | 97 | source_schema = IndexSchema.from_dict(snapshot.schema_snapshot) |
96 | 98 | merged_target_schema = self.merge_patch(source_schema, schema_patch) |
| 99 | + |
| 100 | + # Extract rename operations first |
| 101 | + rename_operations, rename_warnings = self._extract_rename_operations( |
| 102 | + source_schema, schema_patch |
| 103 | + ) |
| 104 | + |
| 105 | + # Classify diff with awareness of rename operations |
97 | 106 | diff_classification = self.classify_diff( |
98 | | - source_schema, schema_patch, merged_target_schema |
| 107 | + source_schema, schema_patch, merged_target_schema, rename_operations |
99 | 108 | ) |
100 | 109 |
|
| 110 | + # Build warnings list |
| 111 | + warnings = ["Index downtime is required"] |
| 112 | + warnings.extend(rename_warnings) |
| 113 | + |
101 | 114 | return MigrationPlan( |
102 | 115 | source=snapshot, |
103 | 116 | requested_changes=schema_patch.model_dump(exclude_none=True), |
104 | 117 | merged_target_schema=merged_target_schema.to_dict(), |
105 | 118 | diff_classification=diff_classification, |
106 | | - warnings=["Index downtime is required"], |
| 119 | + rename_operations=rename_operations, |
| 120 | + warnings=warnings, |
107 | 121 | ) |
108 | 122 |
|
109 | 123 | def snapshot_source( |
@@ -223,29 +237,100 @@ def merge_patch( |
223 | 237 | schema_dict["index"].update(changes.index) |
224 | 238 | return IndexSchema.from_dict(schema_dict) |
225 | 239 |
|
| 240 | + def _extract_rename_operations( |
| 241 | + self, |
| 242 | + source_schema: IndexSchema, |
| 243 | + schema_patch: SchemaPatch, |
| 244 | + ) -> Tuple[RenameOperations, List[str]]: |
| 245 | + """Extract rename operations from the patch and generate warnings. |
| 246 | +
|
| 247 | + Returns: |
| 248 | + Tuple of (RenameOperations, warnings list) |
| 249 | + """ |
| 250 | + source_dict = source_schema.to_dict() |
| 251 | + changes = schema_patch.changes |
| 252 | + warnings: List[str] = [] |
| 253 | + |
| 254 | + # Index rename |
| 255 | + rename_index: Optional[str] = None |
| 256 | + if "name" in changes.index: |
| 257 | + new_name = changes.index["name"] |
| 258 | + old_name = source_dict["index"].get("name") |
| 259 | + if new_name != old_name: |
| 260 | + rename_index = new_name |
| 261 | + warnings.append( |
| 262 | + f"Index rename: '{old_name}' -> '{new_name}' (index-only change, no document migration needed)" |
| 263 | + ) |
| 264 | + |
| 265 | + # Prefix change |
| 266 | + change_prefix: Optional[str] = None |
| 267 | + if "prefix" in changes.index: |
| 268 | + new_prefix = changes.index["prefix"] |
| 269 | + old_prefix = source_dict["index"].get("prefix") |
| 270 | + if new_prefix != old_prefix: |
| 271 | + change_prefix = new_prefix |
| 272 | + warnings.append( |
| 273 | + f"Prefix change: '{old_prefix}' -> '{new_prefix}' " |
| 274 | + "(requires RENAME for all keys, may be slow for large datasets)" |
| 275 | + ) |
| 276 | + |
| 277 | + # Field renames from explicit rename_fields |
| 278 | + rename_fields: List[FieldRename] = list(changes.rename_fields) |
| 279 | + for field_rename in rename_fields: |
| 280 | + warnings.append( |
| 281 | + f"Field rename: '{field_rename.old_name}' -> '{field_rename.new_name}' " |
| 282 | + "(requires read/write for all documents, may be slow for large datasets)" |
| 283 | + ) |
| 284 | + |
| 285 | + return ( |
| 286 | + RenameOperations( |
| 287 | + rename_index=rename_index, |
| 288 | + change_prefix=change_prefix, |
| 289 | + rename_fields=rename_fields, |
| 290 | + ), |
| 291 | + warnings, |
| 292 | + ) |
| 293 | + |
226 | 294 | def classify_diff( |
227 | 295 | self, |
228 | 296 | source_schema: IndexSchema, |
229 | 297 | schema_patch: SchemaPatch, |
230 | 298 | merged_target_schema: IndexSchema, |
| 299 | + rename_operations: Optional[RenameOperations] = None, |
231 | 300 | ) -> DiffClassification: |
232 | 301 | blocked_reasons: List[str] = [] |
233 | 302 | changes = schema_patch.changes |
234 | 303 | source_dict = source_schema.to_dict() |
235 | 304 | target_dict = merged_target_schema.to_dict() |
236 | 305 |
|
| 306 | + # Check which rename operations are being handled |
| 307 | + has_index_rename = rename_operations and rename_operations.rename_index |
| 308 | + has_prefix_change = rename_operations and rename_operations.change_prefix |
| 309 | + has_field_renames = ( |
| 310 | + rename_operations and len(rename_operations.rename_fields) > 0 |
| 311 | + ) |
| 312 | + renamed_field_names = set() |
| 313 | + if has_field_renames and rename_operations: |
| 314 | + renamed_field_names = { |
| 315 | + fr.old_name for fr in rename_operations.rename_fields |
| 316 | + } |
| 317 | + |
237 | 318 | for index_key, target_value in changes.index.items(): |
238 | 319 | source_value = source_dict["index"].get(index_key) |
239 | 320 | if source_value == target_value: |
240 | 321 | continue |
241 | 322 | if index_key == "name": |
242 | | - blocked_reasons.append( |
243 | | - "Changing the index name requires document migration (not yet supported)." |
244 | | - ) |
| 323 | + # Index rename is now supported - skip blocking if we have rename_operations |
| 324 | + if not has_index_rename: |
| 325 | + blocked_reasons.append( |
| 326 | + "Changing the index name requires document migration (not yet supported)." |
| 327 | + ) |
245 | 328 | elif index_key == "prefix": |
246 | | - blocked_reasons.append( |
247 | | - "Changing index prefixes requires document migration (not yet supported)." |
248 | | - ) |
| 329 | + # Prefix change is now supported |
| 330 | + if not has_prefix_change: |
| 331 | + blocked_reasons.append( |
| 332 | + "Changing index prefixes requires document migration (not yet supported)." |
| 333 | + ) |
249 | 334 | elif index_key == "key_separator": |
250 | 335 | blocked_reasons.append( |
251 | 336 | "Changing the key separator requires document migration (not yet supported)." |
@@ -291,9 +376,11 @@ def classify_diff( |
291 | 376 | ) |
292 | 377 | blocked_reasons.extend(vector_blocked) |
293 | 378 |
|
294 | | - blocked_reasons.extend( |
295 | | - self._detect_possible_field_renames(source_fields, target_fields) |
296 | | - ) |
| 379 | + # Detect possible field renames only if not explicitly provided |
| 380 | + if not has_field_renames: |
| 381 | + blocked_reasons.extend( |
| 382 | + self._detect_possible_field_renames(source_fields, target_fields) |
| 383 | + ) |
297 | 384 |
|
298 | 385 | return DiffClassification( |
299 | 386 | supported=len(blocked_reasons) == 0, |
|
0 commit comments