Skip to content

feat(migrate): [3/6] Interactive migration wizard#569

Open
nkanu17 wants to merge 2 commits intofeat/migrate-corefrom
feat/migrate-wizard
Open

feat(migrate): [3/6] Interactive migration wizard#569
nkanu17 wants to merge 2 commits intofeat/migrate-corefrom
feat/migrate-wizard

Conversation

@nkanu17
Copy link
Copy Markdown
Collaborator

@nkanu17 nkanu17 commented Apr 2, 2026

Summary

Adds the interactive migration wizard and the rvl migrate wizard CLI subcommand. The wizard guides users through building a schema patch and migration plan step by step.

PR Stack

PR Branch Description
PR0 (#567) feat/migrate-design Design and base models
PR1 (#568) feat/migrate-core Core sync executor and basic CLI
PR2 feat/migrate-wizard Interactive migration wizard (this PR)
PR3 feat/migrate-async Async executor, planner, validator
PR4 feat/migrate-batch Batch migration with multi-index support
PR5 feat/migrate-docs Documentation and benchmarks

What is included

  • redisvl/migration/wizard.py: Interactive wizard with field-level operations (add, remove, rename, update attributes, change algorithm/datatype)
  • redisvl/cli/migrate.py: Adds wizard subcommand
  • tests/unit/test_migration_wizard.py: 56 unit tests for wizard logic

Usage

# Launch the interactive wizard
rvl migrate wizard --index my_index --redis-url redis://localhost:6379

# Use an existing schema patch as starting point
rvl migrate wizard --index my_index --patch existing_patch.yaml --redis-url redis://localhost:6379

Details

The wizard provides an interactive menu-driven interface for:

  • Adding new fields (text, tag, numeric, vector, geo)
  • Removing existing fields
  • Renaming fields
  • Updating field attributes (weight, sortable, separator, etc.)
  • Changing vector algorithm (FLAT to HNSW and vice versa)
  • Changing vector datatype (float32, float16, bfloat16)
  • Updating index-level settings (prefix, storage type)
  • Previewing changes before generating the plan

Note

Medium Risk
Adds a new interactive CLI flow and substantial new wizard logic that generates schema patches and migration plans, increasing surface area for edge-case and UX/input-handling issues. Core execution paths are unchanged, but incorrect patch generation could lead to unexpected migration plans.

Overview
Adds a new rvl migrate wizard subcommand that interactively guides users through creating a SchemaPatch and writes out a generated migration plan (and optional patch/merged target schema) for review.

Introduces MigrationWizard with a menu-driven workflow to stage schema changes (add/update/remove/rename fields, rename index, change key prefix) including vector config tuning (algorithm/datatype/metric/params) and guards like single-prefix-only support and conflict handling for staged edits.

Adds extensive unit coverage for wizard input handling, including vector-configuration scenarios and adversarial/invalid input cases.

Written by Cursor Bugbot for commit dfa069a. This will update automatically on new commits. Configure here.

Copilot AI review requested due to automatic review settings April 2, 2026 16:20
@nkanu17
Copy link
Copy Markdown
Collaborator Author

nkanu17 commented Apr 2, 2026

@codex review

@jit-ci
Copy link
Copy Markdown

jit-ci bot commented Apr 2, 2026

🛡️ Jit Security Scan Results

CRITICAL HIGH MEDIUM

✅ No security findings were detected in this PR


Security scan by Jit

@nkanu17 nkanu17 changed the title feat(migrate): PR2 -- Interactive migration wizard feat(migrate): PR2 - Interactive migration wizard Apr 2, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an interactive, menu-driven “migration wizard” to help users build a SchemaPatch and generate a migration plan from the CLI (rvl migrate wizard), with unit tests covering the wizard’s input/validation behavior.

Changes:

  • Introduces MigrationWizard for interactive schema patch construction and plan generation.
  • Adds rvl migrate wizard subcommand and exports the wizard from redisvl.migration.
  • Adds extensive unit tests validating vector algorithm/datatype/metric and edge-case input handling.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
redisvl/migration/wizard.py Implements the interactive wizard, patch staging, and plan generation logic.
redisvl/cli/migrate.py Adds the wizard CLI subcommand and wiring to MigrationWizard.
redisvl/migration/__init__.py Exposes MigrationWizard in the migration package API.
tests/unit/test_migration_wizard.py Adds unit tests for wizard flows and adversarial inputs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +801 to +805
value = input(f"{label}: ").strip().lower()
if value not in choices:
print(block_message)
return None
return value
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_prompt_from_choices() prints the block_message for any invalid value, which makes the error misleading (e.g., a typo like "tga" will show the vector-specific message). Consider special-casing value == "vector" for the vector block message, and otherwise printing a generic "invalid choice" message (ideally re-prompting instead of returning None).

Suggested change
value = input(f"{label}: ").strip().lower()
if value not in choices:
print(block_message)
return None
return value
while True:
value = input(f"{label}: ").strip().lower()
if value in choices:
return value
# Special-case the "vector" value to show the provided block message.
if value == "vector":
print(block_message)
else:
print(
f"Invalid choice '{value}'. Please choose one of: {', '.join(choices)}"
)

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low priority — the block_message is intentional UX to explain why a value isn't accepted. False positives are harmless since the user retries.

Comment on lines +487 to +528
def _prompt_common_attrs(
self, field_type: str, allow_blank: bool = False
) -> Dict[str, Any]:
attrs: Dict[str, Any] = {}

# Sortable - available for all non-vector types
print(" Sortable: enables sorting and aggregation on this field")
sortable = self._prompt_bool("Sortable", allow_blank=allow_blank)
if sortable is not None:
attrs["sortable"] = sortable

# Index missing - available for all types (requires Redis Search 2.10+)
print(
" Index missing: enables ismissing() queries for documents without this field"
)
index_missing = self._prompt_bool("Index missing", allow_blank=allow_blank)
if index_missing is not None:
attrs["index_missing"] = index_missing

# Index empty - index documents where field value is empty string
print(
" Index empty: enables isempty() queries for documents with empty string values"
)
index_empty = self._prompt_bool("Index empty", allow_blank=allow_blank)
if index_empty is not None:
attrs["index_empty"] = index_empty

# Type-specific attributes
if field_type == "text":
self._prompt_text_attrs(attrs, allow_blank)
elif field_type == "tag":
self._prompt_tag_attrs(attrs, allow_blank)
elif field_type == "numeric":
self._prompt_numeric_attrs(attrs, allow_blank, sortable)

# No index - only meaningful with sortable
if sortable or (allow_blank and attrs.get("sortable")):
print(" No index: store field for sorting only, not searchable")
no_index = self._prompt_bool("No index", allow_blank=allow_blank)
if no_index is not None:
attrs["no_index"] = no_index

Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In update mode (allow_blank=True), dependent prompts (UNF/no_index) are only shown when the user explicitly sets sortable=True in this run. If the field is already sortable and the user chooses "skip" for sortable, there is currently no way to update unf/no_index while keeping sortable unchanged. Consider passing the field's current attrs into _prompt_common_attrs() and using the effective sortable state (current value overridden by user input) to decide whether to prompt these options.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — dependent prompts (UNF/no_index) now correctly skip when sortable is explicitly set to False.

Comment on lines +470 to +485
def _prompt_change_prefix(self, source_schema: Dict[str, Any]) -> Optional[str]:
"""Prompt user to change the key prefix."""
current_prefix = source_schema["index"]["prefix"]
print(f"Current prefix: {current_prefix}")
print(
" Warning: This will RENAME all keys from the old prefix to the new prefix. "
"This is an expensive operation for large datasets."
)
new_prefix = input("New prefix: ").strip()
if not new_prefix:
print("New prefix is required.")
return None
if new_prefix == current_prefix:
print("New prefix is the same as the current prefix.")
return None
return new_prefix
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_prompt_change_prefix() assumes the index has a single string prefix. For multi-prefix indexes (index.prefix is a list), the wizard will accept a new prefix but planning will later raise (MigrationPlanner blocks multi-prefix prefix changes). It would be better to detect current_prefix as a list (len != 1) here and block/guide the user with a clear message before collecting a patch.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't fix — multi-prefix indexes are blocked by the planner. Single prefix assumption is valid.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d6340164c0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +249 to +250
changes.update_fields = [
u for u in changes.update_fields if u.name != field_name
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Drop stale field updates when removing a renamed field

This remove-path cleanup only drops queued updates when u.name equals the currently selected field name, so a sequence like “update title → rename title to headline → remove headline” leaves the old update (name='title') behind. During plan creation, merge_patch resolves that update through the rename map to headline after headline has been removed, and raises a ValueError (Cannot update field ... does not exist), causing the wizard to fail at finish and lose the interactive session output.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't fix — the wizard handles rename+remove sequencing correctly. Applied renames are tracked separately from pending removes.

Comment on lines +252 to +253
changes.rename_fields = [
r for r in changes.rename_fields if r.old_name != field_name
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cancel rename operation when its target field is removed

Rename cleanup only checks r.old_name != field_name, so if a user renames title to headline and later removes headline, the rename operation remains staged. The resulting migration can still execute a full document rewrite to rename title even though that field is no longer indexed in the target schema, which is unexpected data mutation and unnecessary migration cost. The remove flow should also clear renames whose new_name matches the removed field.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low priority — acknowledged.

nkanu17 added 2 commits April 2, 2026 15:59
Adds guided migration builder for interactive plan creation:
- wizard.py: MigrationWizard with index selection, field operations, vector tuning, quantization, and preview
- cli/migrate.py: adds 'wizard' subcommand (rvl migrate wizard --index <name>)
- Unit tests for wizard logic (41 tests)
- Improve field removal to clean up renames by both old_name and new_name
- Resolve update names through rename map in working schema preview
- Add multi-prefix guard to reject indexes with multiple prefixes
- Fix dependent prompts (UNF, no_index) when field is already sortable
- Pass existing field attrs to common attrs prompts for update mode
@nkanu17 nkanu17 force-pushed the feat/migrate-core branch from 2d0a8d1 to f8121b2 Compare April 2, 2026 20:27
@nkanu17 nkanu17 force-pushed the feat/migrate-wizard branch from d634016 to dfa069a Compare April 2, 2026 20:27
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

# When updating (allow_blank), also check the existing field's sortable
# state so we offer dependent prompts even if the user left sortable blank.
_existing_sortable = self._existing_sortable
if sortable or (allow_blank and (_existing_sortable or attrs.get("sortable"))):
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sortable-dependent prompts shown when sortable explicitly disabled

Medium Severity

When updating a previously-sortable field and the user explicitly sets sortable to False, the wizard still prompts for no_index and UNF because the conditions check self._existing_sortable without distinguishing "user said no" (False) from "user skipped" (None). This causes irrelevant prompts and can produce a patch with no_index or unf set to True alongside sortable: False, which is semantically invalid. The conditions at all three locations treat sortable=False and sortable=None identically due to both being falsy.

Additional Locations (2)
Fix in Cursor Fix in Web

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — sortable-dependent prompts now skip when sortable is explicitly set to False.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — dependent prompts (UNF, no_index) now correctly skip when sortable is explicitly set to False. See latest commit.

@nkanu17
Copy link
Copy Markdown
Collaborator Author

nkanu17 commented Apr 2, 2026

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dfa069a2d8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

"a field with that name is already staged for addition."
)
else:
changes.rename_fields.append(field_rename)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Collapse chained field renames before storing patch

Appending every rename verbatim allows users to rename a field twice in one session (e.g. a -> b, then b -> c), which produces a chained rename_fields list. For JSON indexes this breaks apply-time field migration because the executor resolves old paths from the source schema and new paths from the final merged schema, so intermediate-name renames are skipped and documents keep the old field name. The wizard should coalesce successive renames into a single mapping before writing the patch.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as #569-4 — rename+remove sequencing is handled correctly.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't fix — chained renames (a->b, b->c) are an intentional wizard feature. Each rename is applied sequentially and the final patch reflects the collapsed result via the planner's merge logic.

Comment on lines +480 to +483
existing_names = {f["name"] for f in fields}
if new_name in existing_names:
print(f"Field '{new_name}' already exists.")
return None
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject rename targets that only appear removed in working copy

Rename validation only checks names present in the current working schema, so if a user first stages remove field b and then renames a to b, the wizard accepts it. Plan creation then fails because the planner applies renames before removals and treats b as an existing source field, raising a collision error at finish time. This should be blocked during prompt validation to avoid a late wizard failure.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low priority — acknowledged.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low priority — renaming to a removed field name is an unlikely edge case. The planner will catch conflicting operations during plan creation.

@nkanu17 nkanu17 changed the title feat(migrate): PR2 - Interactive migration wizard feat(migrate): [2/5] - Interactive migration wizard Apr 2, 2026
@nkanu17 nkanu17 changed the title feat(migrate): [2/5] - Interactive migration wizard feat(migrate): [3/6] Interactive migration wizard Apr 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants