From 27a0d9b5fa4aeccdb284688582c5f2bf9a70d2c5 Mon Sep 17 00:00:00 2001 From: leilei3167 Date: Thu, 17 Sep 2026 04:40:12 +0000 Subject: [PATCH] fix(proxy-extras): create SpendLogs call-id index without CONCURRENTLY PostgreSQL rejects CREATE INDEX CONCURRENTLY on partitioned tables (SQLSTATE 0A000), so prisma migrate deploy fails with P3018 when LiteLLM_SpendLogs was partitioned via the spend-logs runbook. Use a plain CREATE INDEX IF NOT EXISTS on the parent instead, which Postgres supports on partitioned tables and propagates to partitions. Fixes #41548 --- .../migration.sql | 25 +++--- ...st_spend_logs_call_id_index_partitioned.py | 81 +++++++++++++++++++ 2 files changed, 95 insertions(+), 11 deletions(-) create mode 100644 tests/proxy_migration_tests/test_spend_logs_call_id_index_partitioned.py diff --git a/litellm-proxy-extras/litellm_proxy_extras/migrations/20260831120001_spend_logs_litellm_call_id_index/migration.sql b/litellm-proxy-extras/litellm_proxy_extras/migrations/20260831120001_spend_logs_litellm_call_id_index/migration.sql index 62ad5c42ba7..7863c2d608f 100644 --- a/litellm-proxy-extras/litellm_proxy_extras/migrations/20260831120001_spend_logs_litellm_call_id_index/migration.sql +++ b/litellm-proxy-extras/litellm_proxy_extras/migrations/20260831120001_spend_logs_litellm_call_id_index/migration.sql @@ -1,12 +1,15 @@ --- CreateIndex (CONCURRENTLY) +-- CreateIndex -- --- Disclaimer: --- - CREATE INDEX CONCURRENTLY cannot run inside a transaction. This migration must stay a --- single statement so Prisma Migrate on PostgreSQL can apply it outside a transaction. --- - Builds are slower and use more I/O than a blocking CREATE INDEX; if the build is --- interrupted, Postgres may leave an INVALID index that must be dropped and recreated. --- - Do not edit this file after it has been applied to any database: Prisma checksums --- migrations; add a new migration instead. --- - Requires PostgreSQL that supports CONCURRENTLY with IF NOT EXISTS (use a new migration --- without IF NOT EXISTS if you must support older versions). -CREATE INDEX CONCURRENTLY IF NOT EXISTS "LiteLLM_SpendLogs_litellm_call_id_idx" ON "LiteLLM_SpendLogs"("litellm_call_id"); +-- PostgreSQL rejects CREATE INDEX CONCURRENTLY on partitioned tables +-- (SQLSTATE 0A000: "cannot create index on partitioned table ... concurrently"). +-- Operators who ran db_scripts/partition_spend_logs.sql therefore fail this +-- migration under `prisma migrate deploy` (Prisma P3018), blocking later ones. +-- +-- A plain CREATE INDEX on the partitioned parent is supported and propagates to +-- every current and future partition (same pattern as partition_spend_logs.sql). +-- Non-partitioned deployments keep the same index name and columns; the build +-- takes a brief stronger lock than CONCURRENTLY would have. +-- +-- Do not edit this file after it has been applied to any database: Prisma +-- checksums migrations; add a new migration instead. +CREATE INDEX IF NOT EXISTS "LiteLLM_SpendLogs_litellm_call_id_idx" ON "LiteLLM_SpendLogs"("litellm_call_id"); diff --git a/tests/proxy_migration_tests/test_spend_logs_call_id_index_partitioned.py b/tests/proxy_migration_tests/test_spend_logs_call_id_index_partitioned.py new file mode 100644 index 00000000000..557c44c78e2 --- /dev/null +++ b/tests/proxy_migration_tests/test_spend_logs_call_id_index_partitioned.py @@ -0,0 +1,81 @@ +"""Regression for BerriAI/litellm#41548. + +PostgreSQL rejects CREATE INDEX CONCURRENTLY on partitioned tables +(SQLSTATE 0A000). Migration 20260831120001 must use a plain CREATE INDEX +so prisma migrate deploy works when LiteLLM_SpendLogs is partitioned. +""" + +from __future__ import annotations + +import os +from pathlib import Path +from typing import Final + +import pytest + +psycopg = pytest.importorskip("psycopg") + +requires_db: Final = pytest.mark.skipif( + "DATABASE_URL" not in os.environ, + reason="requires a postgres database (DATABASE_URL)", +) + +MIGRATION: Final = ( + Path(__file__).resolve().parents[2] + / "litellm-proxy-extras" + / "litellm_proxy_extras" + / "migrations" + / "20260831120001_spend_logs_litellm_call_id_index" + / "migration.sql" +) + +INDEX_SQL: Final = ( + 'CREATE INDEX IF NOT EXISTS "LiteLLM_SpendLogs_litellm_call_id_idx" ' + 'ON "LiteLLM_SpendLogs"("litellm_call_id");' +) + + +def _sql_statements(text: str) -> str: + return "\n".join( + line for line in text.splitlines() if line.strip() and not line.lstrip().startswith("--") + ) + + +def test_spend_logs_call_id_index_migration_avoids_concurrently() -> None: + text = MIGRATION.read_text() + statements = _sql_statements(text) + assert "CONCURRENTLY" not in statements.upper() + assert statements.strip() == INDEX_SQL + + +@requires_db +def test_spend_logs_call_id_index_builds_on_partitioned_table() -> None: + """The migration statement must succeed on a partitioned parent (issue #41548).""" + admin_url = os.environ["DATABASE_URL"].split("?")[0] + with psycopg.connect(admin_url, autocommit=True) as conn: + conn.execute('DROP TABLE IF EXISTS "LiteLLM_SpendLogs" CASCADE') + conn.execute( + """ + CREATE TABLE "LiteLLM_SpendLogs" ( + "request_id" TEXT NOT NULL, + "startTime" TIMESTAMP(3) NOT NULL, + "litellm_call_id" TEXT + ) PARTITION BY RANGE ("startTime") + """ + ) + conn.execute( + """ + CREATE TABLE "LiteLLM_SpendLogs_pdefault" + PARTITION OF "LiteLLM_SpendLogs" DEFAULT + """ + ) + conn.execute(_sql_statements(MIGRATION.read_text())) + rows = conn.execute( + """ + SELECT 1 FROM pg_indexes + WHERE tablename = 'LiteLLM_SpendLogs' + AND indexname = 'LiteLLM_SpendLogs_litellm_call_id_idx' + """ + ).fetchall() + assert rows + conn.execute('DROP TABLE IF EXISTS "LiteLLM_SpendLogs" CASCADE')