From f588147b41c9b9f8be309d362a9ae382f0b0f868 Mon Sep 17 00:00:00 2001 From: 9qeklajc Date: Wed, 1 Jul 2026 17:01:32 +0200 Subject: [PATCH] use slug base --- ...e8f9a0b1_add_slug_to_upstream_providers.py | 43 +++++++++++++++-- routstr/core/provider_slugs.py | 4 +- tests/unit/test_provider_slug_migration.py | 47 +++++++++++++++++++ 3 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 tests/unit/test_provider_slug_migration.py diff --git a/migrations/versions/c6d7e8f9a0b1_add_slug_to_upstream_providers.py b/migrations/versions/c6d7e8f9a0b1_add_slug_to_upstream_providers.py index 198537d8..c140b557 100644 --- a/migrations/versions/c6d7e8f9a0b1_add_slug_to_upstream_providers.py +++ b/migrations/versions/c6d7e8f9a0b1_add_slug_to_upstream_providers.py @@ -10,12 +10,49 @@ from __future__ import annotations import sqlalchemy as sa from alembic import op +from routstr.core.provider_slugs import provider_slug_base, provider_slug_candidate + revision = "c6d7e8f9a0b1" down_revision = "b5e7c9d1f3a2" branch_labels = None depends_on = None +def _allocate_backfill_slug(provider_type: str, reserved_slugs: set[str]) -> str: + base = provider_slug_base(provider_type) + suffix_number = 1 + while True: + candidate = provider_slug_candidate(base, suffix_number) + if candidate not in reserved_slugs: + reserved_slugs.add(candidate) + return candidate + suffix_number += 1 + + +def _backfill_provider_slugs(conn: sa.Connection) -> None: + existing_rows = conn.execute( + sa.text( + "SELECT slug FROM upstream_providers " + "WHERE slug IS NOT NULL AND slug != ''" + ) + ) + reserved_slugs = {str(row.slug).lower() for row in existing_rows} + + rows_to_backfill = conn.execute( + sa.text( + "SELECT id, provider_type FROM upstream_providers " + "WHERE slug IS NULL OR slug = '' " + "ORDER BY id" + ) + ) + for row in rows_to_backfill: + slug = _allocate_backfill_slug(str(row.provider_type), reserved_slugs) + conn.execute( + sa.text("UPDATE upstream_providers SET slug = :slug WHERE id = :id"), + {"slug": slug, "id": row.id}, + ) + + def upgrade() -> None: conn = op.get_bind() inspector = sa.inspect(conn) @@ -27,11 +64,7 @@ def upgrade() -> None: sa.Column("slug", sa.String(), nullable=True), ) - op.execute( - "UPDATE upstream_providers " - "SET slug = LOWER(provider_type) || '-' || CAST(id AS TEXT) " - "WHERE slug IS NULL OR slug = ''" - ) + _backfill_provider_slugs(conn) existing_indexes = {idx["name"] for idx in inspector.get_indexes("upstream_providers")} if "ix_upstream_providers_slug" not in existing_indexes: diff --git a/routstr/core/provider_slugs.py b/routstr/core/provider_slugs.py index cd0d0410..0efb919f 100644 --- a/routstr/core/provider_slugs.py +++ b/routstr/core/provider_slugs.py @@ -28,7 +28,7 @@ def provider_slug_base(provider_type: str) -> str: return base -def _slug_candidate(base: str, suffix_number: int) -> str: +def provider_slug_candidate(base: str, suffix_number: int) -> str: if suffix_number == 1: return base @@ -52,7 +52,7 @@ async def allocate_unique_provider_slug( reserved = {slug.lower() for slug in reserved_slugs} for suffix_number in count(1): - candidate = _slug_candidate(base, suffix_number) + candidate = provider_slug_candidate(base, suffix_number) if candidate in reserved: continue diff --git a/tests/unit/test_provider_slug_migration.py b/tests/unit/test_provider_slug_migration.py new file mode 100644 index 00000000..47adf75f --- /dev/null +++ b/tests/unit/test_provider_slug_migration.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +import importlib + +import sqlalchemy as sa + +migration = importlib.import_module( + "migrations.versions.c6d7e8f9a0b1_add_slug_to_upstream_providers" +) + + +def test_slug_migration_backfill_uses_api_safe_deterministic_slugs() -> None: + engine = sa.create_engine("sqlite:///:memory:") + with engine.begin() as conn: + conn.execute( + sa.text( + "CREATE TABLE upstream_providers (" + "id INTEGER PRIMARY KEY, " + "provider_type VARCHAR NOT NULL, " + "slug VARCHAR NULL" + ")" + ) + ) + conn.execute( + sa.text( + "INSERT INTO upstream_providers (id, provider_type, slug) VALUES " + "(1, 'OpenAI Compatible', NULL), " + "(2, 'OpenAI Compatible', ''), " + "(3, '123', NULL), " + "(4, 'x', NULL), " + "(5, 'anthropic', 'anthropic')" + ) + ) + + migration._backfill_provider_slugs(conn) + + rows = conn.execute( + sa.text("SELECT id, slug FROM upstream_providers ORDER BY id") + ).all() + + assert rows == [ + (1, "openai-compatible"), + (2, "openai-compatible-2"), + (3, "provider-123"), + (4, "x-provider"), + (5, "anthropic"), + ]