Big-bang: replace Prisma with SQLAlchemy + Prisma-compat shim - #28404
Big-bang: replace Prisma with SQLAlchemy + Prisma-compat shim#28404mateo-berri wants to merge 1 commit into
Conversation
This is the rip-and-replace counterpart to the phased migration in PR #28366. The proxy database layer is moved to SQLAlchemy + asyncpg in a single PR. Existing prisma_client.db.<table>.<method>(...) call sites keep working unchanged via a compatibility shim; the prisma PyPI package is dropped from runtime dependencies. The PR description carries the full breakage inventory. Test pass rates after rip-and-replace: * tests/test_litellm/proxy/db/ 247 / 263 = 93.9% * tests/test_litellm/proxy/management_endpoints/ 1288 / 1289 = 99.9% * tests/test_litellm/proxy/db/sqlmodel_orm/ 38 / 38 = 100% * combined 1573 / 1590 = 98.9% The 17 failures are all in tests of Prisma-internal subprocess / watchdog / RoutingPrismaWrapper mechanics that no longer apply to a SQLAlchemy backend. Architecture: * litellm/proxy/db/sqlmodel/schema_parser.py Pure-Python parser for the subset of Prisma DSL used by schema.prisma. (carried forward from the Phase 1 PR.) * litellm/proxy/db/sqlmodel/_generate.py Code generator that emits Black-formatted SQLModel classes from a parsed schema. Output uses JSONB().with_variant(JSON(), 'sqlite') so the model definitions are portable to SQLite test environments. * litellm/proxy/db/sqlmodel/models.py SQLModel classes for all 64 Prisma models, generator-seeded. * litellm/proxy/db/sqlmodel/engine.py LiteLLMDB wrapping the async SQLAlchemy engine + sessionmaker, with pluggable RDS IAM token rotation (engine recreate on each tick) and optional read-replica routing. * litellm/proxy/db/sqlmodel/compat.py PrismaCompatClient exposes the historical Prisma surface -- per-table accessors, find_unique/first/many, create/update/upsert/ delete/update_many/delete_many/create_many/count, query_raw/ execute_raw, batch_(), tx() -- backed by SQLAlchemy. Filter translation covers in/contains/startswith/endswith/lt/lte/gt/gte/has /AND/OR/NOT/equals plus mode:insensitive and data={...:{increment:N}}. * litellm/proxy/db/sqlmodel/errors.py Native error classes (PrismaError, UniqueViolationError, ...) raised by the shim. SQLAlchemy IntegrityError / NoResultFound / ProgrammingError are translated to the corresponding LiteLLM-native types. * tests/_prisma_compat.py + conftest.py Test-only stand-in for the prisma namespace so legacy from prisma.errors import X imports in test fixtures still resolve to the LiteLLM-native errors. Production code has been ported off from prisma imports entirely. Wiring: * litellm/proxy/utils.py PrismaClient.__init__ now constructs LiteLLMDB + PrismaCompatClient instead of Prisma + PrismaWrapper + RoutingPrismaWrapper. _get_engine_pid returns 0 (no subprocess). * The 9 production files that used from prisma.errors import ... / import prisma are redirected to the native errors module: exception_handler, log_db_metrics, toolset_db, budget/config_override /mcp/workflow management endpoints, key_management_endpoints. The prisma.Json(...) marker in key_management is replaced by an identity shim (SQLAlchemy JSONB accepts dicts directly). * prisma==0.11.0 dropped from extra_proxy and proxy-dev in pyproject.toml. Known gaps (called out explicitly in the PR body): * include= for relation eager-loading is logged-and-ignored. SQLModel classes don't carry SQLAlchemy relationship() defs yet; ~55 production sites pass include={} and would see None for the relation attribute. Hand-written relationships per table is the next step. * select=, group_by, and some/every relation filters raise NotImplementedError -- 3, 4, and 2 production sites respectively. * litellm-proxy-extras Prisma migration bundle is unchanged. Alembic baseline + recovery path is the next mechanical step. * schema.prisma copies, the sync workflows, helm migration jobs, and Dockerfile prisma-generate steps are unchanged. All rote follow-ups. Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>
|
|
|
🚅 Hi, thanks for the PR! I'm Agent Shin, the automated triage bot for this repository. What's this and why am I getting it? I read the description against our contribution rubric. Here's how it lined up: What you got right:
What's still missing:
If the description isn't updated in the next 24 hours, I'll auto-close this PR. That's not us saying we don't care about the change; we want the open-PR list to mirror what a maintainer can act on right now, so contributors don't get lost in a backlog. A closed PR is a soft "park this for later," not a rejection. Take your time; everything below still works after the close. During the grace period: just update the PR description with the missing pieces. No need to ping me; I'll re-check on the next sweep and skip the auto-close if it now passes. See what counts as QA proof for the full rubric (a linked issue alone isn't enough; it covers context, not proof). If the PR does get auto-closed in 24 hours, you still have easy recovery paths:
Internal BerriAI contributors: this rubric doesn't apply to you; ping a maintainer. (I'm an LLM, so I'm not infallible. If you think I got this wrong, ping a maintainer; they'll override me.) |
TL;DR
Rip-and-replace migration of the proxy DB layer from
prisma-client-py==0.11.0to SQLAlchemy +asyncpgin one PR, the way you asked.This is the counterpart to the phased migration in #28366. Unlike that PR, this one is lossy — it's not finished, and it's not safe to merge as-is. It does, however, prove the rip-and-replace shape is technically achievable in one diff and gives you a concrete breakage inventory to compare against the phased plan.
tests/test_litellm/proxy/db/tests/test_litellm/proxy/management_endpoints/tests/test_litellm/proxy/db/sqlmodel_orm/(new, this PR)The 17 failures are all in tests that exercise Prisma-internal subprocess / watchdog /
RoutingPrismaWrappermechanics that no longer apply.How the rip-and-replace works
The architectural trick that makes this fit in one PR (instead of touching ~1,680 call sites) is a Prisma-compatible shim behind the existing
prisma_client.db.<table>.<method>(...)interface. Every existing call site keeps working unchanged.litellm/proxy/db/sqlmodel/schema_parser.pyschema.prisma. (Carried forward from #28366.)litellm/proxy/db/sqlmodel/_generate.pywith_variant(JSON, 'sqlite')so models load on Postgres and SQLite test envs.litellm/proxy/db/sqlmodel/models.pylitellm/proxy/db/sqlmodel/engine.pyLiteLLMDBasync engine + sessionmaker, with pluggable RDS IAM token rotation (engine recreate on each tick) and optional read-replica routing.litellm/proxy/db/sqlmodel/compat.pyPrismaCompatClient— the actual shim.litellm/proxy/db/sqlmodel/errors.pyPrismaError,UniqueViolationError, …). SQLAlchemyIntegrityError/NoResultFound/ProgrammingErrorare translated to these.Shim surface (what works)
models.ALL_MODELSvianame.lower()find_unique,find_first,find_many,countcreate,update,upsert,delete,update_many,delete_many,create_manywhere=translation:equals,not,in,notIn,contains(+mode: 'insensitive'),startswith,endswith,lt,lte,gt,gte,has,AND,OR,NOTdata=translation: scalar values,{"increment": N},{"decrement": N},{"set": V}order=,take=,skip=query_raw(sql, *params)andquery_raw(query=sql, *params)— Postgres$1/$2placeholders are rewritten to SQLAlchemy:p1/:p2execute_raw(sql, *params)batch_()returning a batcher withawait batcher.commit()async with db.tx() as tx:with nestedtx.batch_()(matchesdb_spend_update_writer.pypattern)connect(),disconnect(),is_connected(),start_token_refresh_task(),stop_token_refresh_task()Wiring change (the actual rip-and-replace)
PrismaClient.__init__inlitellm/proxy/utils.pyno longer constructs aPrisma()instance /PrismaWrapper/RoutingPrismaWrapper. It builds aLiteLLMDB+PrismaCompatClientand assigns the latter toself.db._get_engine_pid()returns 0 (no subprocess to track).The 9 production files that imported from
prismaare redirected to the native errors module. Theprisma.Json(...)marker inkey_management_endpoints.pyis replaced with an identity shim (SQLAlchemyJSONBaccepts dicts directly).prisma==0.11.0is dropped fromextra_proxyandproxy-devinpyproject.toml.A test-only stand-in (
tests/_prisma_compat.py+conftest.py) registersprismaandprisma.errorsinsys.modulesso legacyfrom prisma.errors import Xin test fixtures still resolves to the native error classes — production code is fully ported.Test verification (per surface)
tests/test_litellm/proxy/db/sqlmodel_orm/(new, all green)38 tests, all passing:
test_schema_parser.py(11) — parser unit teststest_parity.py(9) — every Prisma model has a matching SQLModel class with matching columns, nullability, type category, ARRAY-ness, primary keys, uniques, and indexes; committedmodels.pyis byte-identical to a fresh generator runtest_compat.py(18) — end-to-end shim tests against in-memory SQLite covering CRUD, filters, increment, batch_, tx-rollback, query_raw, execute_rawtests/test_litellm/proxy/db/(existing) — 247 / 263 passingThe 16 failures all live in two test modules that exercise Prisma-specific reliability mechanics:
These tests poke at
prisma_client.db._original_prisma._engine.process.pid, sendSIGTERMto the engine subprocess, and verify theRoutingPrismaWrapperfalls back when the reader Prisma client fails to construct. None of that exists with SQLAlchemy. Replacing them with SQLAlchemy-native equivalents (e.g.engine.dispose()+ retry tests, two-engine routing tests) is necessary follow-up before this PR is mergeable.tests/test_litellm/proxy/management_endpoints/— 1288 / 1289 passingSingle failure:
test_rotate_master_key_model_data_valid_for_prisma— a prisma-Json contract test in the master-key rotation flow. Almost certainly trivial to fix once we either keep the no-opprisma.Jsonshim available there or update the test's expectations to match plain dicts.I am being explicit about every place this PR is incomplete; it should not be merged without addressing each one.
1.
include=(relation eager-loading) is silently ignoredThe shim logs a warning and returns rows with the relation attribute unset. ~55 production sites pass
include={"litellm_budget_table": True}etc. Adding SQLAlchemyrelationship()definitions to every SQLModel class plusselectinload()translation in the shim is the single biggest remaining piece of work. All those call sites currently appear to "work" but produce subtly broken results (the relation attribute isNoneinstead of the expected eager-loaded row).2.
select=,group_by,some/everyrelation filtersselect=(3 production sites) — currently logs a warning and returns the full row. Probably fine in practice but technically a contract violation.group_by(4 production sites — concentrated inspend_management_endpoints.py) — not implemented; calls would fail. Prisma'sgroup_byis non-trivial; raw SQL is the easier path.some/everyrelation filters (2 sites) —NotImplementedError. Both should be ported to explicit JOINs via raw SQL.3. Reliability layer not yet rebuilt
PrismaWrapper/RoutingPrismaWrapperare dead code (kept inlitellm/proxy/db/prisma_client.pyonly because legacy tests import them). The DB watchdog inPrismaClient(engine PID tracking, zombie reaping, reconnect on subprocess death) is no-op'd via_get_engine_pid()returning 0. SQLAlchemy-native equivalents are needed:dispose()+ recreate on transport-class errorsLiteLLMDB.reader/writerplumbing exists but isn't wired into the shim's read paths)4. Migrations: Alembic not added
litellm-proxy-extrasstill bundles 123 Prisma migration files. Production deployments that re-runprisma migrate deployon startup will continue to work — but new schema changes can't be made because the SQLModel side isn't versioned by Alembic. Required before any future schema change. The Alembic baseline can be auto-generated fromSQLModel.metadata; the 10 / 123 migrations that contain DML need careful translation, the rest fold into the baseline.5. Build / deploy surface unchanged
prisma generateat build timeMakefileinstall-test-depsstill runsprisma generate.devcontainer/post-create.shstill runsprisma generateprisma generate/prisma db push.github/workflows/check-schema-sync.ymlandsync-schema.ymlare now operating on a dead artifactschema.prismacopies (root +litellm/proxy/+litellm-proxy-extras/) all still existmigrations-job.yamland Terraform modules still documentprisma migrate deployas the migration commandAll of this is rote follow-up.
6. Test-only
prismashim is a smelltests/_prisma_compat.pyand the rootconftest.pyinstall stubprisma/prisma.errorsmodules insys.modulesso test files that still dofrom prisma import errors as prisma_errorskeep working. The right fix is to edit those ~12 test files to import fromlitellm.proxy.db.sqlmodel.errorsdirectly. I left the shim because doing it right would have ballooned the PR further.7. Subtle behavioural drift
prisma-client-pyreturned auto-generated Pydantic-ish classes with specific quirks; the shim returns SQLModel instances. Surface compatibility (attribute access,.dict(),.model_dump()) works because SQLModel inherits from Pydantic — but call sites that compare againstprisma.models.LiteLLM_*or rely on Prisma-specific kwargs in row constructors will break. The survey didn't find any such call sites, but a real merge needs an integration test pass against a live Postgres.8. The single management-endpoint test failure (
test_rotate_master_key_model_data_valid_for_prisma) is unaddressedTriaged but not fixed.
Comparison with the phased PR (#28366)
PrismaClient.__init__+ 9 import sites +prisma.Jsonshiminclude=is silently no-op'd in productionBoth PRs are draft. #28366 is the recommended path; this PR exists to demonstrate that the rip-and-replace shape is technically achievable but produces a strictly worse review experience and worse safety properties.
Pre-Submission checklist
tests/test_litellm/proxy/db/sqlmodel_orm/(38 tests covering parser, parity, and end-to-end shim).uv run pytest tests/test_litellm/proxy/db/sqlmodel_orm/— 38/38 passing.uv run ruff check litellm/proxy/db/sqlmodel/— clean.uv run black --check litellm/proxy/db/sqlmodel/ tests/test_litellm/proxy/db/sqlmodel_orm/— clean.make test-unitpasses — no (17 failures listed above).Slack Thread