Database Management Skill
Database Architecture
PostgreSQL 16 with Prisma 7 (using @prisma/adapter-pg driver adapter with raw pg.Pool).
- 12 models, 10 enums, 13 migrations
- All IDs: native PostgreSQL UUID (
@db.Uuid)
- All tables:
snake_case via @@map()
- All columns:
snake_case via @map()
Complete Table Reference
| Table |
Purpose |
Key Relations |
users |
User accounts (auth, profile, role) |
→ Role, ← RefreshTokens, ← OtpCodes |
roles |
Role definitions (RBAC) |
← Users |
refresh_tokens |
JWT refresh tokens (hashed) |
→ User |
otp_codes |
OTP codes (email verify, password reset) |
→ User |
teams |
Football teams |
→ Stadium, ← TeamPlayers, ← SeasonTeams, ← Matches, ← Standings |
players |
Football players |
← TeamPlayers, ← MatchEvents |
stadiums |
Stadiums |
← Teams, ← Matches |
seasons |
League seasons |
← Matches, ← SeasonTeams, ← Regulations, ← Standings |
team_players |
Team-player roster (join table) |
→ Team, → Player |
season_teams |
Season-team registration (join table) |
→ Season, → Team |
matches |
Match records |
→ Season, → HomeTeam, → AwayTeam, → Stadium, ← MatchEvents |
match_events |
Match events (goals, cards, subs) |
→ Match, → Player, → RelatedPlayer, → Team |
regulations |
Season-scoped key-value configs |
→ Season |
standings |
League standings (computed/cached) |
→ Season, → Team |
Entity Relationship Diagram
erDiagram
users ||--o{ refresh_tokens : has
users ||--o{ otp_codes : has
users }o--|| roles : belongs_to
teams }o--|| stadiums : home_stadium
teams ||--o{ team_players : roster
players ||--o{ team_players : roster
seasons ||--o{ matches : contains
seasons ||--o{ season_teams : registrations
seasons ||--o{ regulations : config
seasons ||--o{ standings : table
teams ||--o{ season_teams : registered
teams ||--o{ standings : ranked
matches ||--o{ match_events : events
matches }o--|| teams : home_team
matches }o--|| teams : away_team
matches }o--|| stadiums : venue
match_events }o--|| players : player
match_events }o--|| players : related_player
match_events }o--|| teams : team
Schema Development Workflow
1. Modify Schema
Edit apps/api/prisma/schema.prisma
2. Create Migration
cd apps/api
pnpm dlx prisma migrate dev --name descriptive_name
3. Migration Naming Convention
YYYYMMDDHHMMSS_descriptive_name
# Examples:
20260128113243_init_registration
20260129180318_add_auth_models
20260201000000_add_indexes
4. Generate Client
pnpm dlx prisma generate
NOTE: postinstall script auto-runs prisma generate after pnpm install.
Current Migrations (13)
| Migration |
Purpose |
init_registration |
Teams, players, team_players tables |
init_matches |
Matches, match_events tables |
add_auth_models |
Users, refresh_tokens |
add_team_manager_referee_roles |
Role enum expansion |
add_roles_table |
Separate roles table |
add_otp_and_email_verification |
OTP codes, email verification |
add_session_profile_oauth |
Session tracking, OAuth fields, profile |
add_season_stadium_roster_models |
Seasons, stadiums, roster |
add_indexes |
Performance indexes |
add_regulations_season_teams_standings |
Regulations, season_teams, standings |
convert_text_to_uuid |
Migrate text IDs to native UUID |
add_role_id_to_users |
Add roleId FK to users |
add_penalty_event_types |
Add PENALTY, PENALTY_MISS to EventType enum |
Database Seeding
Master Seeder (prisma/seed.ts)
Chains all seed scripts in order:
cd apps/api && pnpm run db:seed
Seed Scripts (7)
| Script |
Purpose |
seed.ts |
Master orchestrator |
verify-demo-users.ts |
5 demo accounts (admin, manager, referee, supervisor, user) |
seed-teams.ts |
10 V-League teams with stadium mappings |
seed-players.ts |
100+ Vietnamese + foreign players, randomized positions/stats |
seed-stadiums.ts |
15 Vietnamese stadiums |
register-teams-to-season.ts |
Auto-register teams to active season |
cleanup-stale-matches.ts |
Remove orphaned match records |
Common Database Operations
# Reset database (drop all, re-migrate, re-seed)
pnpm dlx prisma migrate reset
# Open Prisma Studio GUI
pnpm dlx prisma studio
# Generate client after schema change
pnpm dlx prisma generate
# Check migration status
pnpm dlx prisma migrate status
# Apply migrations in production
pnpm dlx prisma migrate deploy
# Format schema file
pnpm dlx prisma format
Schema Patterns
Soft Delete (Roster)
model TeamPlayer {
joinedAt DateTime @default(now()) @map("joined_at")
leftAt DateTime? @map("left_at") // null = active, set = removed
}
Enum with Default
model Team {
status TeamStatus @default(ACTIVE)
}
Many-to-Many via Join Table
model Team {
roster TeamPlayer[] // Relation name: "roster"
}
model Player {
roster TeamPlayer[] // Same relation name
}
model TeamPlayer {
team Team @relation(fields: [teamId], references: [id])
player Player @relation(fields: [playerId], references: [id])
}
CRITICAL: Prisma relation name is roster, not teamPlayers. Use include: { roster: true }.
Indexes
model Match {
@@index([seasonId])
@@index([homeTeamId])
@@index([awayTeamId])
@@index([status])
}
Query Optimization Tips
- Select only needed fields: Use
select instead of returning entire records
- Pagination: Always use
skip/take with count() for large tables
- Avoid N+1: Use
include for relations instead of separate queries
- Parallel queries: Use
Promise.all([findMany(), count()]) for paginated results
- Indexes: Key fields (seasonId, teamId, status) are already indexed
- Driver adapter: Prisma 7's
@prisma/adapter-pg uses raw pg.Pool for better performance
Environment Configuration
DATABASE_URL="postgresql://vleague:password@localhost:5432/vleague_db"
Docker Database
# Start PostgreSQL only
docker compose -f infra/docker-compose.db.yml up -d
# Connection: postgresql://vleague:vleague@localhost:5432/vleague
Backup & Restore
# Backup
pg_dump -U vleague -h localhost -d vleague_db > backup.sql
# Restore
psql -U vleague -h localhost -d vleague_db < backup.sql
Troubleshooting
| Problem |
Solution |
| "Prisma client not generated" |
Run pnpm dlx prisma generate |
| Migration conflict |
Run pnpm dlx prisma migrate reset (dev only) |
| Connection refused |
Check PostgreSQL is running + DATABASE_URL is correct |
| Schema drift |
Run pnpm dlx prisma migrate dev to reconcile |
| UUID type mismatch |
Ensure all ID fields use @db.Uuid |
| Relation not found |
Check Prisma relation names (e.g., roster not teamPlayers) |
Converted and distributed by TomeVault — claim your Tome and manage your conversions.
1---2name: database-management-23description: Prisma schema, migrations, seeding, and database operations for SE104_VLEAGUE Use when this capability is needed.4---56# Database Management Skill78## Database Architecture910**PostgreSQL 16** with **Prisma 7** (using `@prisma/adapter-pg` driver adapter with raw `pg.Pool`).1112- **12 models**, **10 enums**, **13 migrations**13- All IDs: native PostgreSQL UUID (`@db.Uuid`)14- All tables: `snake_case` via `@@map()`15- All columns: `snake_case` via `@map()`1617---1819## Complete Table Reference2021| Table | Purpose | Key Relations |22| ---------------- | ---------------------------------------- | --------------------------------------------------------------- |23| `users` | User accounts (auth, profile, role) | → Role, ← RefreshTokens, ← OtpCodes |24| `roles` | Role definitions (RBAC) | ← Users |25| `refresh_tokens` | JWT refresh tokens (hashed) | → User |26| `otp_codes` | OTP codes (email verify, password reset) | → User |27| `teams` | Football teams | → Stadium, ← TeamPlayers, ← SeasonTeams, ← Matches, ← Standings |28| `players` | Football players | ← TeamPlayers, ← MatchEvents |29| `stadiums` | Stadiums | ← Teams, ← Matches |30| `seasons` | League seasons | ← Matches, ← SeasonTeams, ← Regulations, ← Standings |31| `team_players` | Team-player roster (join table) | → Team, → Player |32| `season_teams` | Season-team registration (join table) | → Season, → Team |33| `matches` | Match records | → Season, → HomeTeam, → AwayTeam, → Stadium, ← MatchEvents |34| `match_events` | Match events (goals, cards, subs) | → Match, → Player, → RelatedPlayer, → Team |35| `regulations` | Season-scoped key-value configs | → Season |36| `standings` | League standings (computed/cached) | → Season, → Team |3738---3940## Entity Relationship Diagram4142```mermaid43erDiagram44 users ||--o{ refresh_tokens : has45 users ||--o{ otp_codes : has46 users }o--|| roles : belongs_to47 teams }o--|| stadiums : home_stadium48 teams ||--o{ team_players : roster49 players ||--o{ team_players : roster50 seasons ||--o{ matches : contains51 seasons ||--o{ season_teams : registrations52 seasons ||--o{ regulations : config53 seasons ||--o{ standings : table54 teams ||--o{ season_teams : registered55 teams ||--o{ standings : ranked56 matches ||--o{ match_events : events57 matches }o--|| teams : home_team58 matches }o--|| teams : away_team59 matches }o--|| stadiums : venue60 match_events }o--|| players : player61 match_events }o--|| players : related_player62 match_events }o--|| teams : team63```6465---6667## Schema Development Workflow6869### 1. Modify Schema7071Edit `apps/api/prisma/schema.prisma`7273### 2. Create Migration7475```bash76cd apps/api77pnpm dlx prisma migrate dev --name descriptive_name78```7980### 3. Migration Naming Convention8182```83YYYYMMDDHHMMSS_descriptive_name84# Examples:8520260128113243_init_registration8620260129180318_add_auth_models8720260201000000_add_indexes88```8990### 4. Generate Client9192```bash93pnpm dlx prisma generate94```9596> **NOTE**: `postinstall` script auto-runs `prisma generate` after `pnpm install`.9798---99100## Current Migrations (13)101102| Migration | Purpose |103| ---------------------------------------- | ------------------------------------------- |104| `init_registration` | Teams, players, team_players tables |105| `init_matches` | Matches, match_events tables |106| `add_auth_models` | Users, refresh_tokens |107| `add_team_manager_referee_roles` | Role enum expansion |108| `add_roles_table` | Separate roles table |109| `add_otp_and_email_verification` | OTP codes, email verification |110| `add_session_profile_oauth` | Session tracking, OAuth fields, profile |111| `add_season_stadium_roster_models` | Seasons, stadiums, roster |112| `add_indexes` | Performance indexes |113| `add_regulations_season_teams_standings` | Regulations, season_teams, standings |114| `convert_text_to_uuid` | Migrate text IDs to native UUID |115| `add_role_id_to_users` | Add roleId FK to users |116| `add_penalty_event_types` | Add PENALTY, PENALTY_MISS to EventType enum |117118---119120## Database Seeding121122### Master Seeder (`prisma/seed.ts`)123124Chains all seed scripts in order:125126```bash127cd apps/api && pnpm run db:seed128```129130### Seed Scripts (7)131132| Script | Purpose |133| ----------------------------- | ------------------------------------------------------------- |134| `seed.ts` | Master orchestrator |135| `verify-demo-users.ts` | 5 demo accounts (admin, manager, referee, supervisor, user) |136| `seed-teams.ts` | 10 V-League teams with stadium mappings |137| `seed-players.ts` | 100+ Vietnamese + foreign players, randomized positions/stats |138| `seed-stadiums.ts` | 15 Vietnamese stadiums |139| `register-teams-to-season.ts` | Auto-register teams to active season |140| `cleanup-stale-matches.ts` | Remove orphaned match records |141142---143144## Common Database Operations145146```bash147# Reset database (drop all, re-migrate, re-seed)148pnpm dlx prisma migrate reset149150# Open Prisma Studio GUI151pnpm dlx prisma studio152153# Generate client after schema change154pnpm dlx prisma generate155156# Check migration status157pnpm dlx prisma migrate status158159# Apply migrations in production160pnpm dlx prisma migrate deploy161162# Format schema file163pnpm dlx prisma format164```165166---167168## Schema Patterns169170### Soft Delete (Roster)171172```prisma173model TeamPlayer {174 joinedAt DateTime @default(now()) @map("joined_at")175 leftAt DateTime? @map("left_at") // null = active, set = removed176}177```178179### Enum with Default180181```prisma182model Team {183 status TeamStatus @default(ACTIVE)184}185```186187### Many-to-Many via Join Table188189```prisma190model Team {191 roster TeamPlayer[] // Relation name: "roster"192}193model Player {194 roster TeamPlayer[] // Same relation name195}196model TeamPlayer {197 team Team @relation(fields: [teamId], references: [id])198 player Player @relation(fields: [playerId], references: [id])199}200```201202> **CRITICAL**: Prisma relation name is `roster`, not `teamPlayers`. Use `include: { roster: true }`.203204### Indexes205206```prisma207model Match {208 @@index([seasonId])209 @@index([homeTeamId])210 @@index([awayTeamId])211 @@index([status])212}213```214215---216217## Query Optimization Tips2182191. **Select only needed fields**: Use `select` instead of returning entire records2202. **Pagination**: Always use `skip`/`take` with `count()` for large tables2213. **Avoid N+1**: Use `include` for relations instead of separate queries2224. **Parallel queries**: Use `Promise.all([findMany(), count()])` for paginated results2235. **Indexes**: Key fields (seasonId, teamId, status) are already indexed2246. **Driver adapter**: Prisma 7's `@prisma/adapter-pg` uses raw `pg.Pool` for better performance225226---227228## Environment Configuration229230```env231DATABASE_URL="postgresql://vleague:password@localhost:5432/vleague_db"232```233234### Docker Database235236```bash237# Start PostgreSQL only238docker compose -f infra/docker-compose.db.yml up -d239240# Connection: postgresql://vleague:vleague@localhost:5432/vleague241```242243---244245## Backup & Restore246247```bash248# Backup249pg_dump -U vleague -h localhost -d vleague_db > backup.sql250251# Restore252psql -U vleague -h localhost -d vleague_db < backup.sql253```254255---256257## Troubleshooting258259| Problem | Solution |260| ----------------------------- | -------------------------------------------------------------- |261| "Prisma client not generated" | Run `pnpm dlx prisma generate` |262| Migration conflict | Run `pnpm dlx prisma migrate reset` (dev only) |263| Connection refused | Check PostgreSQL is running + DATABASE_URL is correct |264| Schema drift | Run `pnpm dlx prisma migrate dev` to reconcile |265| UUID type mismatch | Ensure all ID fields use `@db.Uuid` |266| Relation not found | Check Prisma relation names (e.g., `roster` not `teamPlayers`) |267268---269> Converted and distributed by [TomeVault](https://tomevault.io/claim/daithang-organization) — claim your Tome and manage your conversions.270<!-- tomevault:4.0:skill_md:2026-04-13 -->