Skip to content

Phase 1: introduce SQLModel models alongside Prisma, with parity guard - #28366

Draft
mateo-berri wants to merge 1 commit into
litellm_internal_stagingfrom
litellm_prisma-to-sqlmodel-phase1-6e8b
Draft

Phase 1: introduce SQLModel models alongside Prisma, with parity guard#28366
mateo-berri wants to merge 1 commit into
litellm_internal_stagingfrom
litellm_prisma-to-sqlmodel-phase1-6e8b

Conversation

@mateo-berri

Copy link
Copy Markdown
Contributor

Relevant issues

First PR in a multi-phase migration of the proxy persistence layer from Prisma (prisma-client-py==0.11.0) to SQLModel/SQLAlchemy.

Why phased

Scoping the full migration showed:

  • ~1,680 prisma_client.db.<model> call sites across ~147 production files
  • ~177 test files reference Prisma
  • ~288 query_raw / execute_raw calls across ~37 files
  • 64 Prisma models, 3 synced schema.prisma copies, 123 bundled SQL migrations
  • Custom production reliability infra: PrismaWrapper (RDS IAM token rotation), RoutingPrismaWrapper (read-replica split), PrismaDBExceptionHandler (~10 distinct error classifications)
  • 7 Dockerfiles run prisma generate, plus CI workflows, helm charts, and a separately-published litellm-proxy-extras package bundling the migrations

A big-bang cutover would be unreviewable and unsafe. Each phase below lands as its own PR.

What this PR ships (Phase 1)

Foundation only — no runtime behaviour changes. Nothing in the existing proxy imports the new package; it sits alongside Prisma and is guarded against drift.

File Lines Purpose
litellm/proxy/db/sqlmodel/schema_parser.py 473 Pure-Python parser for the subset of Prisma DSL used by schema.prisma.
litellm/proxy/db/sqlmodel/_generate.py 515 Code generator that emits Black-formatted SQLModel classes from a parsed schema.
litellm/proxy/db/sqlmodel/models.py 2,491 SQLModel classes for all 64 Prisma models.
litellm/proxy/db/sqlmodel/README.md 116 Phase plan and explicit out-of-scope items.
tests/test_litellm/proxy/db/sqlmodel_orm/test_schema_parser.py 228 11 parser unit tests.
tests/test_litellm/proxy/db/sqlmodel_orm/test_parity.py 350 9 structural parity tests.
pyproject.toml / uv.lock +18 Adds sqlmodel>=0.0.22,<1.0 to extra_proxy.

Tricky cases the generator handles

  • Composite primary keys (@@id([a, b])) → PrimaryKeyConstraint
  • Composite uniques (@@unique([...])) → UniqueConstraint
  • Indexes with custom names (@@index([...], map: "...")) → Index("custom_name", ...)
  • Indexes without a custom name → emitted with the same default convention Prisma uses (<table>_<col>_..._idx)
  • @@map("foo_table")__tablename__ = "foo_table"
  • @map("created_at")sa_column_kwargs={'name': 'created_at'}
  • String[]ARRAY(Text()) (Postgres-specific, matching Prisma)
  • JsonJSONB with both Python default_factory and server_default=text("'{}'::jsonb") so omitted-column INSERTs match
  • @updatedAtsa_column_kwargs={'onupdate': lambda: datetime.utcnow()}
  • @default(uuid()) / @default(cuid())default_factory=lambda: str(uuid.uuid4())
  • Reserved Python attribute names (metadata, registry clash with SQLAlchemy Declarative) → Python attribute renamed to metadata_ while the on-disk column name remains metadata

Parity test coverage

The 9 parity tests fail CI if models.py and schema.prisma drift in any of:

  1. Set of tables (1:1 mapping for all 64 models)
  2. Set of columns per table
  3. Column nullability
  4. Coarse SQL type category (e.g. BigInt → BigInteger-class, never Integer)
  5. ARRAY-ness (String[]ARRAY(...))
  6. Primary-key column set
  7. Unique-constraint signatures (set-equality, ignoring column order)
  8. Index column tuples (order-preserving — index column order affects which queries it serves)
  9. Byte-for-byte equality between the committed models.py and a fresh generator run (catches any drift the structural checks miss)

Manually verified

  • All 64 Prisma models produce loadable SQLModel classes (from litellm.proxy.db.sqlmodel.models import ALL_MODELS returns 64 classes)
  • Existing Prisma module still imports cleanly alongside the new package
  • Dropping a column from schema.prisma (verified by mutation) is correctly flagged by test_columns_match_for_every_table

What is explicitly not in this PR

These belong to subsequent phases — see litellm/proxy/db/sqlmodel/README.md for the full plan:

  1. Session abstraction that wraps prisma_client today and AsyncSession tomorrow.
  2. CI workflow turning the parity test into a required check.
  3. Raw-SQL hotspot port (~288 calls; lowest-risk batch).
  4. Per-table call site migration (~55 tables; parallelisable by feature area).
  5. Reliability layer rebuild: SQLAlchemy-native PrismaWrapper / RoutingPrismaWrapper / PrismaDBExceptionHandler equivalents.
  6. Alembic baseline + recovery path for existing deployments. The 10/123 migrations that contain DML need careful translation; the rest are pure DDL and fold into the baseline.
  7. Tear out Prisma: drop the pin from pyproject.toml, remove prisma generate from 7 Dockerfiles, retire litellm-proxy-extras, retire the 3 schema.prisma copies and their check-schema-sync/sync-schema workflows.

Linear ticket

n/a — agent-driven scoping work.

Pre-Submission checklist

  • Added tests in tests/test_litellm/proxy/db/sqlmodel_orm/ (20 tests covering both the parser and Prisma↔SQLModel parity).
  • uv run pytest tests/test_litellm/proxy/db/sqlmodel_orm/ — 20/20 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.
  • PR scope is intentionally narrow: foundation only, no behaviour change.
  • Greptile review (will request after a maintainer review).

Slack Thread

Open in Web Open in Cursor 

Foundation for the multi-phase migration of the proxy persistence layer
from Prisma (prisma-client-py==0.11.0) to SQLModel/SQLAlchemy. This change
is non-functional at runtime: nothing imports the new package from the
existing proxy code paths.

Adds:

* litellm/proxy/db/sqlmodel/schema_parser.py
  Pure-Python parser for the subset of Prisma DSL used by schema.prisma.

* litellm/proxy/db/sqlmodel/_generate.py
  Code generator that emits SQLModel class definitions from a parsed
  schema. Produces Black-formatted output. Run manually after schema
  changes:

      uv run python -m litellm.proxy.db.sqlmodel._generate \
          --schema schema.prisma \
          --out litellm/proxy/db/sqlmodel/models.py

* litellm/proxy/db/sqlmodel/models.py
  SQLModel classes for all 64 Prisma models, hand-editable. Composite
  primary keys, @@unique, @@index (incl. map: '...' renames), @@Map,
  String[] arrays, BigInt, Json/JSONB, @updatedat, @default(uuid()/now()),
  and reserved Python attribute names (metadata -> metadata_) are all
  preserved structurally.

* tests/test_litellm/proxy/db/sqlmodel_orm/{test_schema_parser,test_parity}.py
  20 tests covering the parser unit cases, structural parity between
  every Prisma model and its SQLModel class (columns, nullability, type
  category, ARRAY-ness, primary keys, uniques, indexes), and a strict
  guard that the committed models.py is byte-identical to a fresh
  generator run.

* litellm/proxy/db/sqlmodel/README.md
  Phase 1 plan and the explicit out-of-scope items reserved for later
  phases (session abstraction, raw-SQL hotspot port, per-table call site
  migration, replacement of PrismaWrapper / RoutingPrismaWrapper /
  exception classifier, Alembic migrations, Prisma teardown).

Dependencies:

* Adds 'sqlmodel>=0.0.22,<1.0' to the extra_proxy optional group. This
  pulls in SQLAlchemy as a transitive dep. Existing Prisma deps are
  unchanged.

Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@codecov

codecov Bot commented May 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 50.00000% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/proxy/db/sqlmodel/__init__.py 50.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@devin-ai-integration

Copy link
Copy Markdown
Contributor

🚅 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:

  • ✅ Clear problem description
  • ✅ Expected vs. actual behavior

What's still missing:

  • linked GitHub issue or explicit related issue reference
  • end-to-end QA proof with screenshots/video or real commands plus output

The body clearly explains the migration problem and what this phase changes versus what remains out of scope, so context is present. However, the only verification mentioned is unit-test/linters output, which does not count as end-to-end QA proof, and there is no linked issue reference.

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:

  • Comment @agent-shin reconsider after updating the description. I'll re-evaluate and reopen the PR if it now passes.
  • Comment @greptileai to request a fresh Greptile review; that still works even after the PR is closed, and a stronger score is one of the signals that lifts the PR back into the queue. So a low Greptile score isn't a blocker either.

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.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants