feat(db): add teacher schedule tables and Room v4 migration

This commit is contained in:
2026-09-19 14:47:35 +03:00
parent a5ea516713
commit 1a4bff73d0
11 changed files with 1198 additions and 2 deletions
@@ -3,6 +3,8 @@ package agzam.vyatsu.data.local
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
@Database(
entities = [
@@ -11,9 +13,14 @@ import androidx.room.TypeConverters
ScheduleEntity::class,
DayEntity::class,
LessonEntity::class,
PreferenceEntity::class
PreferenceEntity::class,
DepartmentEntity::class,
DepartmentPeriodEntity::class,
TeacherScheduleEntity::class,
TeacherDayEntity::class,
TeacherLessonEntity::class
],
version = 3,
version = 4,
exportSchema = true
)
@TypeConverters(Converters::class)
@@ -21,4 +28,91 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun groupDao(): GroupDao
abstract fun scheduleDao(): ScheduleDao
abstract fun preferencesDao(): PreferencesDao
abstract fun teacherDao(): TeacherDao
abstract fun teacherScheduleDao(): TeacherScheduleDao
companion object {
val MIGRATION_3_4 = object : Migration(3, 4) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"CREATE TABLE IF NOT EXISTS `departments` (" +
"`departmentId` TEXT NOT NULL, `name` TEXT NOT NULL, " +
"`faculty` TEXT NOT NULL, `lastSyncedAt` INTEGER NOT NULL, " +
"PRIMARY KEY(`departmentId`))"
)
db.execSQL(
"CREATE TABLE IF NOT EXISTS `department_periods` (" +
"`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " +
"`departmentId` TEXT NOT NULL, `season` TEXT NOT NULL, " +
"`weekStart` INTEGER NOT NULL, `weekEnd` INTEGER NOT NULL, " +
"FOREIGN KEY(`departmentId`) REFERENCES `departments`(`departmentId`) " +
"ON UPDATE NO ACTION ON DELETE CASCADE )"
)
db.execSQL(
"CREATE INDEX IF NOT EXISTS `index_department_periods_departmentId` " +
"ON `department_periods` (`departmentId`)"
)
db.execSQL(
"CREATE TABLE IF NOT EXISTS `teacher_schedules` (" +
"`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " +
"`departmentId` TEXT NOT NULL, `season` TEXT NOT NULL, " +
"`weekStart` INTEGER NOT NULL, `weekEnd` INTEGER NOT NULL, " +
"`lastSyncedAt` INTEGER NOT NULL, " +
"FOREIGN KEY(`departmentId`) REFERENCES `departments`(`departmentId`) " +
"ON UPDATE NO ACTION ON DELETE CASCADE )"
)
db.execSQL(
"CREATE INDEX IF NOT EXISTS `index_teacher_schedules_departmentId` " +
"ON `teacher_schedules` (`departmentId`)"
)
db.execSQL(
"CREATE UNIQUE INDEX IF NOT EXISTS `index_teacher_schedules_departmentId_season_weekStart` " +
"ON `teacher_schedules` (`departmentId`, `season`, `weekStart`)"
)
db.execSQL(
"CREATE TABLE IF NOT EXISTS `teacher_days` (" +
"`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " +
"`scheduleId` INTEGER NOT NULL, `date` INTEGER NOT NULL, " +
"FOREIGN KEY(`scheduleId`) REFERENCES `teacher_schedules`(`id`) " +
"ON UPDATE NO ACTION ON DELETE CASCADE )"
)
db.execSQL(
"CREATE INDEX IF NOT EXISTS `index_teacher_days_scheduleId` " +
"ON `teacher_days` (`scheduleId`)"
)
db.execSQL(
"CREATE INDEX IF NOT EXISTS `index_teacher_days_date` " +
"ON `teacher_days` (`date`)"
)
db.execSQL(
"CREATE TABLE IF NOT EXISTS `teacher_lessons` (" +
"`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " +
"`dayId` INTEGER NOT NULL, `date` INTEGER NOT NULL, " +
"`slotIndex` INTEGER NOT NULL, `start` INTEGER NOT NULL, " +
"`end` INTEGER NOT NULL, `lessonIndex` INTEGER NOT NULL, " +
"`raw` TEXT NOT NULL, `room` TEXT, `groupName` TEXT, " +
"`subgroup` TEXT, `subject` TEXT, `type` TEXT NOT NULL, " +
"`teacher` TEXT NOT NULL, " +
"FOREIGN KEY(`dayId`) REFERENCES `teacher_days`(`id`) " +
"ON UPDATE NO ACTION ON DELETE CASCADE )"
)
db.execSQL(
"CREATE INDEX IF NOT EXISTS `index_teacher_lessons_dayId` " +
"ON `teacher_lessons` (`dayId`)"
)
db.execSQL(
"CREATE INDEX IF NOT EXISTS `index_teacher_lessons_teacher` " +
"ON `teacher_lessons` (`teacher`)"
)
db.execSQL(
"CREATE INDEX IF NOT EXISTS `index_teacher_lessons_room` " +
"ON `teacher_lessons` (`room`)"
)
db.execSQL(
"CREATE INDEX IF NOT EXISTS `index_teacher_lessons_subject` " +
"ON `teacher_lessons` (`subject`)"
)
}
}
}
}
@@ -0,0 +1,12 @@
package agzam.vyatsu.data.local
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "departments")
data class DepartmentEntity(
@PrimaryKey val departmentId: String,
val name: String,
val faculty: String,
val lastSyncedAt: Long
)
@@ -0,0 +1,28 @@
package agzam.vyatsu.data.local
import agzam.vyatsu.domain.model.Season
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
// Все опубликованные на сайте 2-недельные периоды кафедры, включая ещё не начавшиеся.
@Entity(
tableName = "department_periods",
foreignKeys = [
ForeignKey(
entity = DepartmentEntity::class,
parentColumns = ["departmentId"],
childColumns = ["departmentId"],
onDelete = ForeignKey.CASCADE
)
],
indices = [Index("departmentId")]
)
data class DepartmentPeriodEntity(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val departmentId: String,
val season: Season,
val weekStart: Long,
val weekEnd: Long
)
@@ -28,6 +28,36 @@ interface PreferencesDao {
@Query("INSERT OR REPLACE INTO preferences(key, value) VALUES('selectedDayDate', :date)")
suspend fun setSelectedDayDate(date: String)
@Query("SELECT value FROM preferences WHERE key = 'selectedTeacherId'")
suspend fun selectedTeacherId(): String?
@Query(
"INSERT OR REPLACE INTO preferences(key, value) VALUES('selectedTeacherId', :teacherId)"
)
suspend fun setSelectedTeacherId(teacherId: String)
@Query("SELECT value FROM preferences WHERE key = 'selectedTeacherWeekStart'")
suspend fun selectedTeacherWeekStart(): String?
@Query(
"INSERT OR REPLACE INTO preferences(key, value) VALUES('selectedTeacherWeekStart', :weekStart)"
)
suspend fun setSelectedTeacherWeekStart(weekStart: String)
@Query("SELECT value FROM preferences WHERE key = 'selectedTeacherDayDate'")
suspend fun selectedTeacherDayDate(): String?
@Query(
"INSERT OR REPLACE INTO preferences(key, value) VALUES('selectedTeacherDayDate', :date)"
)
suspend fun setSelectedTeacherDayDate(date: String)
@Query(
"DELETE FROM preferences WHERE key IN " +
"('selectedTeacherId', 'selectedTeacherWeekStart', 'selectedTeacherDayDate')"
)
suspend fun clearSelectedTeacher()
@Query("SELECT value FROM preferences WHERE key = 'autoUpdateEnabled'")
suspend fun autoUpdateEnabled(): String?
@@ -0,0 +1,39 @@
package agzam.vyatsu.data.local
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
@Dao
interface TeacherDao {
@Query("SELECT * FROM departments ORDER BY name")
fun observeDepartments(): Flow<List<DepartmentEntity>>
@Query("SELECT * FROM departments WHERE departmentId = :id")
suspend fun getDepartment(id: String): DepartmentEntity?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(departments: List<DepartmentEntity>)
@Query("SELECT MAX(lastSyncedAt) FROM departments")
suspend fun maxSyncedAt(): Long?
@Query("SELECT DISTINCT faculty FROM departments ORDER BY faculty")
suspend fun getFaculties(): List<String>
@Query("DELETE FROM department_periods")
suspend fun deleteAllPeriods()
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertPeriods(periods: List<DepartmentPeriodEntity>)
@Query(
"SELECT * FROM department_periods WHERE weekStart <= :epochDay AND weekEnd >= :epochDay ORDER BY departmentId"
)
suspend fun getPeriodsForDate(epochDay: Long): List<DepartmentPeriodEntity>
@Query("SELECT * FROM department_periods WHERE departmentId = :departmentId ORDER BY weekStart")
suspend fun getPeriodsForDepartment(departmentId: String): List<DepartmentPeriodEntity>
}
@@ -0,0 +1,27 @@
package agzam.vyatsu.data.local
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(
tableName = "teacher_days",
foreignKeys = [
ForeignKey(
entity = TeacherScheduleEntity::class,
parentColumns = ["id"],
childColumns = ["scheduleId"],
onDelete = ForeignKey.CASCADE
)
],
indices = [
Index("scheduleId"),
Index("date")
]
)
data class TeacherDayEntity(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val scheduleId: Long,
val date: Long
)
@@ -0,0 +1,41 @@
package agzam.vyatsu.data.local
import agzam.vyatsu.domain.model.LessonType
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(
tableName = "teacher_lessons",
foreignKeys = [
ForeignKey(
entity = TeacherDayEntity::class,
parentColumns = ["id"],
childColumns = ["dayId"],
onDelete = ForeignKey.CASCADE
)
],
indices = [
Index("dayId"),
Index("teacher"),
Index("room"),
Index("subject")
]
)
data class TeacherLessonEntity(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val dayId: Long,
val date: Long,
val slotIndex: Int,
val start: Int,
val end: Int,
val lessonIndex: Int,
val raw: String,
val room: String?,
val groupName: String?,
val subgroup: String?,
val subject: String?,
val type: LessonType,
val teacher: String
)
@@ -0,0 +1,66 @@
package agzam.vyatsu.data.local
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
@Dao
interface TeacherScheduleDao {
@Query("SELECT * FROM teacher_schedules ORDER BY weekStart DESC")
fun observeSchedules(): Flow<List<TeacherScheduleEntity>>
@Query("SELECT * FROM teacher_schedules ORDER BY weekStart DESC")
suspend fun getSchedules(): List<TeacherScheduleEntity>
@Query("DELETE FROM teacher_days WHERE scheduleId = :scheduleId")
suspend fun deleteDaysForSchedule(scheduleId: Long)
@Insert(onConflict = OnConflictStrategy.ABORT)
suspend fun insertSchedule(schedule: TeacherScheduleEntity): Long
@Query(
"DELETE FROM teacher_schedules WHERE departmentId = :departmentId AND season = :season AND weekStart = :weekStart"
)
suspend fun deleteSchedule(departmentId: String, season: String, weekStart: Long)
@Query("DELETE FROM teacher_schedules WHERE season = :season AND weekStart = :weekStart")
suspend fun deletePeriod(season: String, weekStart: Long)
@Query("DELETE FROM teacher_schedules")
suspend fun deleteAll()
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertDays(days: List<TeacherDayEntity>): List<Long>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertLessons(lessons: List<TeacherLessonEntity>)
@Query(
"SELECT teacher_days.* FROM teacher_days INNER JOIN teacher_schedules ON teacher_schedules.id = teacher_days.scheduleId " +
"WHERE teacher_schedules.season = :season AND teacher_schedules.weekStart = :weekStart ORDER BY teacher_days.date"
)
fun observeDaysForPeriod(season: String, weekStart: Long): Flow<List<TeacherDayEntity>>
@Query(
"SELECT teacher_lessons.* FROM teacher_lessons INNER JOIN teacher_days ON teacher_days.id = teacher_lessons.dayId " +
"INNER JOIN teacher_schedules ON teacher_schedules.id = teacher_days.scheduleId " +
"WHERE teacher_schedules.season = :season AND teacher_schedules.weekStart = :weekStart " +
"ORDER BY teacher_lessons.date, teacher_lessons.slotIndex, teacher_lessons.lessonIndex"
)
fun observeLessonsForPeriod(season: String, weekStart: Long): Flow<List<TeacherLessonEntity>>
@Query(
"SELECT DISTINCT teacher FROM teacher_lessons INNER JOIN teacher_days ON teacher_days.id = teacher_lessons.dayId " +
"INNER JOIN teacher_schedules ON teacher_schedules.id = teacher_days.scheduleId " +
"WHERE teacher_schedules.season = :season AND teacher_schedules.weekStart = :weekStart ORDER BY teacher"
)
fun observeDistinctTeachers(season: String, weekStart: Long): Flow<List<String>>
@Query(
"SELECT EXISTS(SELECT 1 FROM teacher_schedules WHERE season = :season AND weekStart = :weekStart)"
)
suspend fun hasSchedulesForPeriod(season: String, weekStart: Long): Boolean
}
@@ -0,0 +1,31 @@
package agzam.vyatsu.data.local
import agzam.vyatsu.domain.model.Season
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(
tableName = "teacher_schedules",
foreignKeys = [
ForeignKey(
entity = DepartmentEntity::class,
parentColumns = ["departmentId"],
childColumns = ["departmentId"],
onDelete = ForeignKey.CASCADE
)
],
indices = [
Index("departmentId"),
Index(value = ["departmentId", "season", "weekStart"], unique = true)
]
)
data class TeacherScheduleEntity(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val departmentId: String,
val season: Season,
val weekStart: Long,
val weekEnd: Long,
val lastSyncedAt: Long
)