From bad0d0cd4531c861e85f0c9462867dce5f191c79 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Thu, 23 Apr 2026 11:07:58 -0700 Subject: [PATCH] fix(memory): enforce unique key per scope by using NULLS NOT DISTINCT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The unique constraint `(key, user_id, team_id)` on LiteLLM_MemoryTable silently allowed duplicates when user_id or team_id was NULL, because Postgres treats every NULL as distinct by default (ANSI semantics). A caller with no team_id could POST the same key three times and get three rows. Migration: 1. Dedupe existing rows, keeping the most recent per (key, user_id, team_id), using `IS NOT DISTINCT FROM` so NULL == NULL. 2. Drop the old unique index. 3. Recreate it with `NULLS NOT DISTINCT` (Postgres 15+). No code change: POST already returns 409 on unique-violation error messages — it just wasn't firing before because the constraint didn't catch the NULL-team case. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../migration.sql | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 litellm-proxy-extras/litellm_proxy_extras/migrations/20260423090000_memory_unique_nulls_not_distinct/migration.sql diff --git a/litellm-proxy-extras/litellm_proxy_extras/migrations/20260423090000_memory_unique_nulls_not_distinct/migration.sql b/litellm-proxy-extras/litellm_proxy_extras/migrations/20260423090000_memory_unique_nulls_not_distinct/migration.sql new file mode 100644 index 00000000000..3846d3961b6 --- /dev/null +++ b/litellm-proxy-extras/litellm_proxy_extras/migrations/20260423090000_memory_unique_nulls_not_distinct/migration.sql @@ -0,0 +1,34 @@ +-- Fix duplicate-key issue on LiteLLM_MemoryTable. +-- +-- Problem: the unique constraint `(key, user_id, team_id)` does not block +-- duplicates when `user_id` or `team_id` is NULL, because by default +-- Postgres treats each NULL as distinct (ANSI SQL semantics). Callers with +-- no team_id could POST the same key repeatedly and get multiple rows. +-- +-- Fix (Postgres 15+): recreate the unique index with NULLS NOT DISTINCT so +-- NULL values are treated as equal for uniqueness checks. +-- +-- Note: Prisma has no schema syntax for NULLS NOT DISTINCT (as of writing), +-- so this lives as a raw SQL migration. Prisma introspection will still see +-- the index as uniquely covering `(key, user_id, team_id)`, matching the +-- `@@unique` in schema.prisma. + +-- 1. Deduplicate existing rows: keep only the most-recently-updated row per +-- (key, user_id, team_id), using IS NOT DISTINCT FROM so NULL == NULL. +DELETE FROM "LiteLLM_MemoryTable" a +USING "LiteLLM_MemoryTable" b +WHERE a.memory_id <> b.memory_id + AND a.key = b.key + AND a.user_id IS NOT DISTINCT FROM b.user_id + AND a.team_id IS NOT DISTINCT FROM b.team_id + AND ( + a.updated_at < b.updated_at + OR (a.updated_at = b.updated_at AND a.memory_id < b.memory_id) + ); + +-- 2. Drop the old (NULL-distinct) unique index. +DROP INDEX IF EXISTS "LiteLLM_MemoryTable_key_user_id_team_id_key"; + +-- 3. Recreate with NULLS NOT DISTINCT. +CREATE UNIQUE INDEX "LiteLLM_MemoryTable_key_user_id_team_id_key" + ON "LiteLLM_MemoryTable"("key", "user_id", "team_id") NULLS NOT DISTINCT;