TradingAgents/docs/agent/decisions/012-portfolio-no-orm.md

3.8 KiB

type status date agent_author tags related_files
decision active 2026-03-20 claude
portfolio
database
supabase
orm
prisma
tradingagents/portfolio/supabase_client.py
tradingagents/portfolio/repository.py
tradingagents/portfolio/migrations/001_initial_schema.sql

Context

When designing the Portfolio Manager data layer (Phase 1), the question arose: should we use an ORM (specifically Prisma) or keep the raw supabase-py client that the scaffolding already plans to use?

The options considered were:

Option Description
Raw supabase-py (chosen) Direct Supabase PostgREST client, builder-pattern API
Prisma Python (prisma-client-py) Code-generated type-safe ORM backed by Node.js
SQLAlchemy Full ORM with Core + ORM layers, Alembic migrations

The Decision

Use raw supabase-py without an ORM for the portfolio data layer.

The data access layer (supabase_client.py) wraps the Supabase client directly. Our own Portfolio, Holding, Trade, and PortfolioSnapshot dataclasses provide the type-safety layer; serialisation is handled by to_dict() / from_dict() on each model.

Why Not Prisma

  1. Node.js runtime dependencyprisma-client-py uses Prisma's Node.js engine at code-generation time. This adds a non-Python runtime requirement to a Python-only project.

  2. Conflicts with Supabase's migration tooling — the project already uses Supabase's SQL migration files (migrations/001_initial_schema.sql) and the Supabase dashboard for schema changes. Prisma's prisma migrate maintains its own shadow database and migration state, creating two competing systems.

  3. Code generation build step — every schema change requires running prisma generate before the Python code works. This complicates CI, local setup, and agent-driven development.

  4. Overkill for 4 tables — the portfolio schema has exactly 4 tables with straightforward CRUD. Prisma's relationship traversal and complex query features offer no benefit here.

Why Not SQLAlchemy

  1. Not using a local database — the database is managed by Supabase (hosted PostgreSQL). SQLAlchemy's connection-pooling and engine management are designed for direct database connections, which bypass Supabase's PostgREST API and Row Level Security.

  2. Extra dependency — SQLAlchemy + Alembic would be significant new dependencies for a non-DB-heavy app.

Why Raw supabase-py Is Sufficient

  • supabase-py provides a clean builder-pattern API: client.table("holdings").select("*").eq("portfolio_id", id).execute()
  • Our dataclasses already provide compile-time type safety and lossless serialisation; the client only handles transport.
  • Migrations are plain SQL files — readable, versionable, Supabase-native.
  • SupabaseClient is a thin singleton wrapper that translates HTTP errors into domain exceptions — this gives us the ORM-like error-handling benefit without the complexity.

Constraints

  • Do not add an ORM dependency (prisma-client-py, sqlalchemy, tortoise-orm) to pyproject.toml without revisiting this decision.
  • Do not bypass SupabaseClient by importing supabase directly in other modules — always go through PortfolioRepository.
  • If the schema grows beyond ~10 tables or requires complex multi-table joins, revisit this decision and consider SQLAlchemy Core (not the ORM layer) with direct asyncpg connections.

Actionable Rules

  • All DB access goes through PortfolioRepositorySupabaseClient.
  • Migrations are .sql files in tradingagents/portfolio/migrations/, run via the Supabase SQL Editor or supabase db push.
  • Type safety comes from dataclass to_dict() / from_dict() — not from a code-generated ORM schema.