mirror of
https://github.com/lfnovo/open-notebook.git
synced 2026-07-23 22:55:45 +00:00
Some checks are pending
Development Build / extract-version (push) Waiting to run
Development Build / build-regular (push) Blocked by required conditions
Development Build / build-single (push) Blocked by required conditions
Development Build / summary (push) Blocked by required conditions
Tests / Backend Tests (push) Waiting to run
Tests / Frontend Tests (push) Waiting to run
* fix(credentials): persist Ollama num_ctx via a flexible config object The `credential` table is SCHEMAFULL, so the model's `num_ctx` field was silently dropped on write and the override never took effect (#875). Rather than add a typed column per provider option, add a single flexible `config` object to the credential table (migration 15). Provider-specific tuning options (currently `num_ctx`) are still exposed as top-level fields on the Credential model and on the API, but are packed into `config` on save and lifted back out on load via a before-validator. Future options only need a Pydantic field + an entry in CONFIG_EXTRAS — no further migrations. - migration 15 (+ down): DEFINE FIELD config ON credential FLEXIBLE TYPE option<object> - Credential: CONFIG_EXTRAS set, _lift_config before-validator, config packing in _prepare_save_data - tests covering pack/unpack round-trip and the empty-config case * fix(credentials): preserve unmapped config keys on save Address cubic review on #903: - `config` is now a real model field mirroring the credential table's FLEXIBLE object and is the on-disk source of truth. `num_ctx` remains a convenience field mirrored from/to config. On save we start from the existing bag and sync the convenience fields in, so a save never clobbers config keys written by a newer version (repo_update uses MERGE, which replaces the whole object). config is only written as None when the merged result is genuinely empty. (The previous approach relied on Pydantic extras, which ObjectModel's default extra="ignore" silently dropped.) - Update database/CLAUDE.md migration totals (15 up/down, incl. migration 15). - Add tests for unmapped-key preservation and clearing num_ctx while keeping other config keys. * fix(credentials): validate config-mirrored extras via Pydantic Address cubic re-review on #903: mirror known config keys (num_ctx) onto their convenience fields in a `before` model validator so they go through normal Pydantic field validation/coercion, instead of an `after`-validator object.__setattr__ that bypassed type checks. Adds a test asserting num_ctx is coerced to int and a non-coercible value is rejected.
244 lines
8.6 KiB
Python
244 lines
8.6 KiB
Python
"""
|
|
Async migration system for SurrealDB using the official Python client.
|
|
Based on patterns from sblpy migration system.
|
|
"""
|
|
|
|
from typing import List
|
|
|
|
from loguru import logger
|
|
|
|
from .repository import db_connection, repo_query
|
|
|
|
|
|
class AsyncMigration:
|
|
"""
|
|
Handles individual migration operations with async support.
|
|
"""
|
|
|
|
def __init__(self, sql: str) -> None:
|
|
"""Initialize migration with SQL content."""
|
|
self.sql = sql
|
|
|
|
@classmethod
|
|
def from_file(cls, file_path: str) -> "AsyncMigration":
|
|
"""Create migration from SQL file."""
|
|
with open(file_path, "r", encoding="utf-8") as file:
|
|
raw_content = file.read()
|
|
# Clean up SQL content
|
|
lines = []
|
|
for line in raw_content.split("\n"):
|
|
line = line.strip()
|
|
if line and not line.startswith("--"):
|
|
lines.append(line)
|
|
sql = " ".join(lines)
|
|
return cls(sql)
|
|
|
|
async def run(self, bump: bool = True) -> None:
|
|
"""Run the migration."""
|
|
try:
|
|
async with db_connection() as connection:
|
|
await connection.query(self.sql)
|
|
|
|
if bump:
|
|
await bump_version()
|
|
else:
|
|
await lower_version()
|
|
|
|
except Exception as e:
|
|
logger.error(f"Migration failed: {str(e)}")
|
|
raise
|
|
|
|
|
|
class AsyncMigrationRunner:
|
|
"""
|
|
Handles running multiple migrations in sequence.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
up_migrations: List[AsyncMigration],
|
|
down_migrations: List[AsyncMigration],
|
|
) -> None:
|
|
"""Initialize runner with migration lists."""
|
|
self.up_migrations = up_migrations
|
|
self.down_migrations = down_migrations
|
|
|
|
async def run_all(self) -> None:
|
|
"""Run all pending up migrations."""
|
|
current_version = await get_latest_version()
|
|
|
|
for i in range(current_version, len(self.up_migrations)):
|
|
logger.info(f"Running migration {i + 1}")
|
|
await self.up_migrations[i].run(bump=True)
|
|
|
|
async def run_one_up(self) -> None:
|
|
"""Run one up migration."""
|
|
current_version = await get_latest_version()
|
|
|
|
if current_version < len(self.up_migrations):
|
|
logger.info(f"Running migration {current_version + 1}")
|
|
await self.up_migrations[current_version].run(bump=True)
|
|
|
|
async def run_one_down(self) -> None:
|
|
"""Run one down migration."""
|
|
current_version = await get_latest_version()
|
|
|
|
if current_version > 0:
|
|
logger.info(f"Rolling back migration {current_version}")
|
|
await self.down_migrations[current_version - 1].run(bump=False)
|
|
|
|
|
|
class AsyncMigrationManager:
|
|
"""
|
|
Main migration manager with async support.
|
|
"""
|
|
|
|
def __init__(self):
|
|
"""Initialize migration manager."""
|
|
self.up_migrations = [
|
|
AsyncMigration.from_file("open_notebook/database/migrations/1.surrealql"),
|
|
AsyncMigration.from_file("open_notebook/database/migrations/2.surrealql"),
|
|
AsyncMigration.from_file("open_notebook/database/migrations/3.surrealql"),
|
|
AsyncMigration.from_file("open_notebook/database/migrations/4.surrealql"),
|
|
AsyncMigration.from_file("open_notebook/database/migrations/5.surrealql"),
|
|
AsyncMigration.from_file("open_notebook/database/migrations/6.surrealql"),
|
|
AsyncMigration.from_file("open_notebook/database/migrations/7.surrealql"),
|
|
AsyncMigration.from_file("open_notebook/database/migrations/8.surrealql"),
|
|
AsyncMigration.from_file("open_notebook/database/migrations/9.surrealql"),
|
|
AsyncMigration.from_file("open_notebook/database/migrations/10.surrealql"),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/11.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/12.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/13.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/14.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/15.surrealql"
|
|
),
|
|
]
|
|
self.down_migrations = [
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/1_down.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/2_down.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/3_down.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/4_down.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/5_down.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/6_down.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/7_down.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/8_down.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/9_down.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/10_down.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/11_down.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/12_down.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/13_down.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/14_down.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/15_down.surrealql"
|
|
),
|
|
]
|
|
self.runner = AsyncMigrationRunner(
|
|
up_migrations=self.up_migrations,
|
|
down_migrations=self.down_migrations,
|
|
)
|
|
|
|
async def get_current_version(self) -> int:
|
|
"""Get current database version."""
|
|
return await get_latest_version()
|
|
|
|
async def needs_migration(self) -> bool:
|
|
"""Check if migration is needed."""
|
|
current_version = await self.get_current_version()
|
|
return current_version < len(self.up_migrations)
|
|
|
|
async def run_migration_up(self):
|
|
"""Run all pending migrations."""
|
|
current_version = await self.get_current_version()
|
|
logger.info(f"Current version before migration: {current_version}")
|
|
|
|
if await self.needs_migration():
|
|
try:
|
|
await self.runner.run_all()
|
|
new_version = await self.get_current_version()
|
|
logger.info(f"Migration successful. New version: {new_version}")
|
|
except Exception as e:
|
|
logger.error(f"Migration failed: {str(e)}")
|
|
raise
|
|
else:
|
|
logger.info("Database is already at the latest version")
|
|
|
|
|
|
# Database version management functions
|
|
async def get_latest_version() -> int:
|
|
"""Get the latest version from the migrations table."""
|
|
try:
|
|
versions = await get_all_versions()
|
|
if not versions:
|
|
return 0
|
|
return max(version["version"] for version in versions)
|
|
except Exception:
|
|
# If migrations table doesn't exist, we're at version 0
|
|
return 0
|
|
|
|
|
|
async def get_all_versions() -> List[dict]:
|
|
"""Get all versions from the migrations table."""
|
|
try:
|
|
result = await repo_query("SELECT * FROM _sbl_migrations ORDER BY version;")
|
|
return result
|
|
except Exception:
|
|
# If table doesn't exist, return empty list
|
|
return []
|
|
|
|
|
|
async def bump_version() -> None:
|
|
"""Bump the version by adding a new entry to migrations table."""
|
|
current_version = await get_latest_version()
|
|
new_version = current_version + 1
|
|
|
|
await repo_query(
|
|
"CREATE type::thing('_sbl_migrations', $version) SET version = $version, applied_at = time::now();",
|
|
{"version": new_version},
|
|
)
|
|
|
|
|
|
async def lower_version() -> None:
|
|
"""Lower the version by removing the latest entry from migrations table."""
|
|
current_version = await get_latest_version()
|
|
if current_version > 0:
|
|
await repo_query(
|
|
"DELETE type::thing('_sbl_migrations', $version);",
|
|
{"version": current_version},
|
|
)
|