Files
reloop-v2/backend/prisma/schema.prisma
reloop e8121e5bd0 fix(prisma): relax tag cascade to SetNull — QA-5.fix.C2 + M2
- SkillSnapshot.tagId: Int → Int?, Tag relation → onDelete: SetNull
- StudyLog.tag: explicit onDelete: SetNull (was implicit Restrict)
- DashboardService.summary() filters { tagId: { not: null } } so orphaned
  snapshots don't leak into the top-skills UI
- Schema header notes that prisma migrate dev --name relax_tag_cascade
  must run on Dev VM before deploy

Prevents silent data loss when users delete a tag (previous cascade
erased all skill history for that tag permanently).
2026-04-12 00:08:40 +09:00

162 lines
4.2 KiB
Plaintext

// ReLoop v2 schema — persona forgetting curve model
// Generated fresh; old FSRS fields dropped. DB reset via `prisma migrate reset`.
//
// NOTE: After editing tag-relation onDelete rules, run:
// prisma migrate dev --name relax_tag_cascade
// on the Dev VM before deploying. This migrates SkillSnapshot.tagId to nullable
// and sets both SkillSnapshot and StudyLog tag FKs to ON DELETE SET NULL.
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
// ─── Enums ────────────────────────────────────────────────────────
enum Persona {
senior
mid
junior
crammer
}
enum ReviewIntensity {
strict
moderate
relaxed
}
enum StudyResult {
correct
incorrect
partial
}
enum ReviewStatus {
pending
done
skipped
expired
}
// ─── Models ───────────────────────────────────────────────────────
model User {
id Int @id @default(autoincrement())
email String @unique
password String
nickname String
persona Persona @default(mid)
currentGrade Int?
targetGrade Int?
reviewIntensity ReviewIntensity @default(moderate)
onboardedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
subjects Subject[]
studyLogs StudyLog[]
reviewSchedules ReviewSchedule[]
skillSnapshots SkillSnapshot[]
@@map("users")
}
model Subject {
id Int @id @default(autoincrement())
name String
color String @default("#6366f1")
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
tags Tag[]
studyLogs StudyLog[]
@@unique([userId, name])
@@index([userId])
@@map("subjects")
}
model Tag {
id Int @id @default(autoincrement())
name String
subjectId Int
subject Subject @relation(fields: [subjectId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
studyLogs StudyLog[]
skillSnapshots SkillSnapshot[]
@@unique([subjectId, name])
@@index([subjectId])
@@map("tags")
}
model StudyLog {
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
subjectId Int
subject Subject @relation(fields: [subjectId], references: [id])
tagId Int?
tag Tag? @relation(fields: [tagId], references: [id], onDelete: SetNull)
title String
difficulty Float
baseCorrectRate Float?
result StudyResult
memo String? @db.Text
studiedAt DateTime @default(now())
timeSpent Int?
reviewSchedules ReviewSchedule[]
@@index([userId, studiedAt])
@@index([subjectId])
@@index([tagId])
@@map("study_logs")
}
model ReviewSchedule {
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
studyLogId Int
studyLog StudyLog @relation(fields: [studyLogId], references: [id], onDelete: Cascade)
scheduledAt DateTime
reviewedAt DateTime?
result StudyResult?
iteration Int @default(0)
predictedP Float?
status ReviewStatus @default(pending)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId, status, scheduledAt])
@@index([studyLogId])
@@map("review_schedules")
}
model SkillSnapshot {
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
tagId Int?
tag Tag? @relation(fields: [tagId], references: [id], onDelete: SetNull)
s0 Float
lastUpdatedAt DateTime @default(now())
sampleCount Int @default(0)
@@unique([userId, tagId])
@@index([userId])
@@map("skill_snapshots")
}