Add basic migrations, query expression resolver
This commit is contained in:
parent
afe05fb7dd
commit
0990c2e1b4
15 changed files with 752 additions and 88 deletions
0
redis_developer/orm/migrations/__init__.py
Normal file
0
redis_developer/orm/migrations/__init__.py
Normal file
116
redis_developer/orm/migrations/migrator.py
Normal file
116
redis_developer/orm/migrations/migrator.py
Normal file
|
@ -0,0 +1,116 @@
|
|||
import hashlib
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from redis import ResponseError
|
||||
|
||||
from redis_developer.connections import get_redis_connection
|
||||
from redis_developer.orm.model import model_registry
|
||||
|
||||
redis = get_redis_connection()
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
import importlib
|
||||
import pkgutil
|
||||
|
||||
|
||||
def import_submodules(root_module_name: str):
|
||||
"""Import all submodules of a module, recursively."""
|
||||
# TODO: Call this without specifying a module name, to import everything?
|
||||
root_module = importlib.import_module(root_module_name)
|
||||
for loader, module_name, is_pkg in pkgutil.walk_packages(
|
||||
root_module.__path__, root_module.__name__ + '.'):
|
||||
importlib.import_module(module_name)
|
||||
|
||||
|
||||
def schema_hash_key(index_name):
|
||||
return f"{index_name}:hash"
|
||||
|
||||
|
||||
def create_index(index_name, schema, current_hash):
|
||||
redis.execute_command(f"ft.create {index_name} "
|
||||
f"{schema}")
|
||||
redis.set(schema_hash_key(index_name), current_hash)
|
||||
|
||||
|
||||
class MigrationAction(Enum):
|
||||
CREATE = 2
|
||||
DROP = 1
|
||||
|
||||
|
||||
@dataclass
|
||||
class IndexMigration:
|
||||
model_name: str
|
||||
index_name: str
|
||||
schema: str
|
||||
hash: str
|
||||
action: MigrationAction
|
||||
previous_hash: Optional[str] = None
|
||||
|
||||
def run(self):
|
||||
if self.action is MigrationAction.CREATE:
|
||||
self.create()
|
||||
elif self.action is MigrationAction.DROP:
|
||||
self.drop()
|
||||
|
||||
def create(self):
|
||||
return create_index(self.index_name, self.schema, self.hash)
|
||||
|
||||
def drop(self):
|
||||
redis.execute_command(f"FT.DROPINDEX {self.index_name}")
|
||||
|
||||
|
||||
class Migrator:
|
||||
def __init__(self, module=None):
|
||||
# Try to load any modules found under the given path or module name.
|
||||
if module:
|
||||
import_submodules(module)
|
||||
|
||||
self.migrations = []
|
||||
|
||||
for name, cls in model_registry.items():
|
||||
hash_key = schema_hash_key(cls.Meta.index_name)
|
||||
try:
|
||||
schema = cls.schema()
|
||||
except NotImplementedError:
|
||||
log.info("Skipping migrations for %s", name)
|
||||
continue
|
||||
current_hash = hashlib.sha1(schema.encode("utf-8")).hexdigest()
|
||||
|
||||
try:
|
||||
redis.execute_command("ft.info", cls.Meta.index_name)
|
||||
except ResponseError:
|
||||
self.migrations.append(
|
||||
IndexMigration(name, cls.Meta.index_name, schema, current_hash,
|
||||
MigrationAction.CREATE))
|
||||
|
||||
stored_hash = redis.get(hash_key)
|
||||
schema_out_of_date = current_hash != stored_hash
|
||||
|
||||
if schema_out_of_date:
|
||||
# TODO: Switch out schema with an alias to avoid downtime -- separate migration?
|
||||
self.migrations.append(
|
||||
IndexMigration(name, cls.Meta.index_name, schema, current_hash,
|
||||
MigrationAction.DROP, stored_hash))
|
||||
self.migrations.append(
|
||||
IndexMigration(name, cls.Meta.index_name, schema, current_hash,
|
||||
MigrationAction.CREATE, stored_hash))
|
||||
|
||||
@property
|
||||
def valid_migrations(self):
|
||||
return self.missing_indexes.keys() + self.out_of_date_indexes.keys()
|
||||
|
||||
def validate_migration(self, model_class_name):
|
||||
if model_class_name not in self.valid_migrations:
|
||||
migrations = ", ".join(self.valid_migrations)
|
||||
raise RuntimeError(f"No migration found for {model_class_name}."
|
||||
f"Valid migrations are: {migrations}")
|
||||
|
||||
def run(self):
|
||||
# TODO: Migration history
|
||||
# TODO: Dry run with output
|
||||
for migration in self.migrations:
|
||||
migration.run()
|
Loading…
Add table
Add a link
Reference in a new issue