Skip to content
Merged
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
10 changes: 9 additions & 1 deletion .github/wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,12 @@ github
ULID
booleans
instantiation
MyModel
MyModel
nx
xx
FieldInfo
issubclass
ulid
coroutine
compat
programmatically
11 changes: 9 additions & 2 deletions aredis_om/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from .async_redis import redis # isort:skip
from .checks import has_redis_json, has_redisearch
from .connections import get_redis_connection
from .model.migrations.schema.legacy_migrator import MigrationError, Migrator
from .model.migrations import MigrationError, SchemaMigrator
from .model.migrations.schema.legacy_migrator import SchemaDetector
from .model.model import (
EmbeddedJsonModel,
Field,
Expand All @@ -18,6 +19,10 @@
)
from .model.types import Coordinates, GeoFilter


# Backward compatibility alias - deprecated, use SchemaDetector or SchemaMigrator
Migrator = SchemaDetector

__all__ = [
"Coordinates",
"EmbeddedJsonModel",
Expand All @@ -28,12 +33,14 @@
"JsonModel",
"KNNExpression",
"MigrationError",
"Migrator",
"Migrator", # Deprecated - use SchemaMigrator for production
"NotFoundError",
"QueryNotSupportedError",
"QuerySyntaxError",
"RedisModel",
"RedisModelError",
"SchemaMigrator",
"SchemaDetector",
"VectorFieldOptions",
"get_redis_connection",
"has_redis_json",
Expand Down
2 changes: 1 addition & 1 deletion aredis_om/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ async def has_redis_json(conn=None):
async def has_redisearch(conn=None):
if conn is None:
conn = get_redis_connection()
if has_redis_json(conn):
if await has_redis_json(conn):
return True
command_exists = await check_for_command(conn, "ft.search")
return command_exists
2 changes: 2 additions & 0 deletions aredis_om/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import click

from ..model.cli.index import index
from ..model.cli.migrate import migrate
from ..model.cli.migrate_data import migrate_data

Expand All @@ -16,6 +17,7 @@ def om():


# Add subcommands
om.add_command(index)
om.add_command(migrate)
om.add_command(migrate_data, name="migrate-data")

Expand Down
174 changes: 174 additions & 0 deletions aredis_om/model/cli/index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
"""
CLI for managing Redis OM indexes.

Provides commands for creating, dropping, and rebuilding RediSearch indexes
during development. For production migrations, use `om migrate` instead.
"""

import asyncio
from typing import Optional

import click


def run_async(coro):
"""Run an async coroutine in an isolated event loop."""
import concurrent.futures

with concurrent.futures.ThreadPoolExecutor() as executor:
future = executor.submit(asyncio.run, coro)
return future.result()


@click.group()
def index():
"""Manage RediSearch indexes for Redis OM models.

These commands are intended for development workflows. For production
deployments, use `om migrate` for tracked, reversible migrations.
"""
pass


@index.command()
@click.option("--module", "-m", help="Python module containing models to import")
@click.option(
"--force", "-f", is_flag=True, help="Force recreation of existing indexes"
)
def create(module: Optional[str], force: bool):
"""Create indexes for all registered models.

This will create RediSearch indexes for all models that don't have
an existing index (unless --force is used to recreate).
"""
from ..migrations.schema.legacy_migrator import SchemaDetector

async def _create():
from ...connections import get_redis_connection

conn = get_redis_connection()
detector = SchemaDetector(module=module, conn=conn)
await detector.detect_migrations()

if not detector.migrations:
click.echo("✅ All indexes are up to date.")
return

created = 0
for migration in detector.migrations:
if migration.action.name == "CREATE":
click.echo(f"Creating index: {migration.index_name}")
await migration.run()
created += 1
elif force and migration.action.name == "DROP":
click.echo(f"Dropping index: {migration.index_name}")
await migration.run()

click.echo(f"\n✅ Created {created} index(es).")

run_async(_create())


@index.command()
@click.option("--module", "-m", help="Python module containing models to import")
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
def drop(module: Optional[str], yes: bool):
"""Drop all indexes for registered models.

Warning: This is destructive and will remove all indexes. Data remains
but will not be searchable until indexes are recreated.
"""
from ..migrations.schema.legacy_migrator import import_submodules

async def _drop():
from ...connections import get_redis_connection
from ...model.model import model_registry

if module:
import_submodules(module)

conn = get_redis_connection()

if not model_registry:
click.echo("No models found in registry.")
return

indexes = [cls.Meta.index_name for cls in model_registry.values()]

if not yes:
click.echo(f"This will drop {len(indexes)} index(es):")
for idx in indexes:
click.echo(f"- {idx}")
if not click.confirm("\nContinue?"):
click.echo("Aborted.")
return

dropped = 0
for idx in indexes:
try:
await conn.ft(idx).dropindex()
click.echo(f"Dropped: {idx}")
dropped += 1
except Exception:
click.echo(f"Skipped (not found): {idx}")

click.echo(f"\n✅ Dropped {dropped} index(es).")

run_async(_drop())


@index.command()
@click.option("--module", "-m", help="Python module containing models to import")
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
def rebuild(module: Optional[str], yes: bool):
"""Drop and recreate all indexes.

This is a convenience command for development that combines `drop` and
`create`. All indexes will be dropped and recreated from current model
definitions.
"""
from ..migrations.schema.legacy_migrator import SchemaDetector, import_submodules

async def _rebuild():
from ...connections import get_redis_connection
from ...model.model import model_registry

if module:
import_submodules(module)

conn = get_redis_connection()

if not model_registry:
click.echo("No models found in registry.")
return

indexes = [(name, cls) for name, cls in model_registry.items()]

if not yes:
click.echo(f"This will rebuild {len(indexes)} index(es):")
for name, cls in indexes:
click.echo(f"- {cls.Meta.index_name}")
if not click.confirm("\nContinue?"):
click.echo("Aborted.")
return

# Drop all indexes
for name, cls in indexes:
try:
await conn.ft(cls.Meta.index_name).dropindex()
click.echo(f"Dropped: {cls.Meta.index_name}")
except Exception: # nosec B110
pass # Index didn't exist, ignore

# Create all indexes
detector = SchemaDetector(module=module, conn=conn)
await detector.detect_migrations()

for migration in detector.migrations:
if migration.action.name == "CREATE":
click.echo(f"Creating: {migration.index_name}")
await migration.run()

click.echo(f"\n✅ Rebuilt {len(indexes)} index(es).")

run_async(_rebuild())
123 changes: 0 additions & 123 deletions aredis_om/model/cli/legacy_migrate.py

This file was deleted.

Loading
Loading