- 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).
162 lines
4.2 KiB
Plaintext
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")
|
|
}
|