Files
Scriberr/internal/database/steps.go
2026-05-03 15:04:44 -07:00

402 lines
11 KiB
Go

package database
import (
"fmt"
"time"
"scriberr/internal/models"
"scriberr/internal/transcription/scheduler"
"gorm.io/gorm"
)
type migrationStep func(*gorm.DB) error
var schemaSteps = map[int]migrationStep{
2: migrateStepV1ToV2,
3: migrateStepV2ToV3,
4: migrateStepV3ToV4,
5: migrateStepV4ToV5,
6: migrateStepV5ToV6,
7: migrateStepV6ToV7,
8: migrateStepV7ToV8,
9: migrateStepV8ToV9,
10: migrateStepV9ToV10,
11: migrateStepV10ToV11,
12: migrateStepV11ToV12,
13: migrateStepV12ToV13,
}
func runSchemaSteps(tx *gorm.DB, currentVersion int) error {
for version := currentVersion + 1; version <= latestSchemaVersion; version++ {
step := schemaSteps[version]
if step != nil {
if err := step(tx); err != nil {
return fmt.Errorf("apply schema migration v%d: %w", version, err)
}
}
if err := recordSchemaVersion(tx, version); err != nil {
return fmt.Errorf("record schema version %d: %w", version, err)
}
}
return nil
}
func migrateStepV1ToV2(tx *gorm.DB) error {
if err := ensureSingleDefaultPerUser(tx); err != nil {
return err
}
if err := backfillCompatibilityColumns(tx); err != nil {
return err
}
return nil
}
func migrateStepV2ToV3(tx *gorm.DB) error {
return nil
}
func migrateStepV3ToV4(tx *gorm.DB) error {
return nil
}
func migrateStepV4ToV5(tx *gorm.DB) error {
return nil
}
func migrateStepV5ToV6(tx *gorm.DB) error {
return nil
}
func migrateStepV6ToV7(tx *gorm.DB) error {
return nil
}
func migrateStepV7ToV8(tx *gorm.DB) error {
return nil
}
func migrateStepV8ToV9(tx *gorm.DB) error {
if err := tx.AutoMigrate(&models.RecordingSession{}); err != nil {
return err
}
return tx.Exec(`CREATE INDEX IF NOT EXISTS idx_recording_sessions_artifact_cleanup ON recording_sessions(status, temporary_artifacts_cleaned_at)`).Error
}
func migrateStepV9ToV10(tx *gorm.DB) error {
if err := tx.AutoMigrate(&models.UserSettings{}); err != nil {
return err
}
return backfillUserSettings(tx)
}
func migrateStepV10ToV11(tx *gorm.DB) error {
if err := tx.AutoMigrate(&models.SystemSetting{}); err != nil {
return err
}
return backfillSystemSettings(tx)
}
func migrateStepV11ToV12(tx *gorm.DB) error {
statements := []string{
`CREATE INDEX IF NOT EXISTS idx_transcriptions_queue_fifo ON transcriptions(status, queued_at, created_at, id)`,
`CREATE INDEX IF NOT EXISTS idx_transcriptions_queue_user_status ON transcriptions(user_id, status, queued_at)`,
`CREATE INDEX IF NOT EXISTS idx_transcriptions_queue_duration ON transcriptions(status, source_duration_ms, queued_at, created_at, id)`,
`CREATE INDEX IF NOT EXISTS idx_transcriptions_user_status_updated ON transcriptions(user_id, status, updated_at DESC, id)`,
}
for _, statement := range statements {
if err := tx.Exec(statement).Error; err != nil {
return err
}
}
return nil
}
func migrateStepV12ToV13(tx *gorm.DB) error {
if err := tx.AutoMigrate(&models.UserSettings{}); err != nil {
return err
}
if err := backfillUserSettings(tx); err != nil {
return err
}
statements := []string{
`CREATE INDEX IF NOT EXISTS idx_transcriptions_user_source_created ON transcriptions(user_id, source_file_hash, created_at DESC, id)`,
`CREATE INDEX IF NOT EXISTS idx_transcriptions_user_files_created ON transcriptions(user_id, created_at DESC, id DESC) WHERE source_file_hash IS NOT NULL AND deleted_at IS NULL`,
}
for _, statement := range statements {
if err := tx.Exec(statement).Error; err != nil {
return err
}
}
return nil
}
func backfillCompatibilityColumns(tx *gorm.DB) error {
if err := backfillUsers(tx); err != nil {
return err
}
if err := backfillAPIKeys(tx); err != nil {
return err
}
if err := backfillTranscriptionProfiles(tx); err != nil {
return err
}
if err := backfillTranscriptionJobs(tx); err != nil {
return err
}
if err := backfillTranscriptionExecutions(tx); err != nil {
return err
}
if err := backfillSummaryTemplates(tx); err != nil {
return err
}
if err := backfillLLMConfigs(tx); err != nil {
return err
}
return nil
}
func backfillUsers(tx *gorm.DB) error {
var rows []models.User
if err := tx.Find(&rows).Error; err != nil {
return err
}
for _, row := range rows {
if err := row.BeforeSave(tx); err != nil {
return err
}
updates := map[string]any{
"settings_json": row.SettingsJSON,
}
if err := withPreservedUpdatedAt(tx.Model(&models.User{}).Where("id = ?", row.ID), updates, row.UpdatedAt).
Updates(updates).Error; err != nil {
return err
}
}
if err := backfillUserSettings(tx); err != nil {
return err
}
return nil
}
func backfillUserSettings(tx *gorm.DB) error {
var users []models.User
if err := tx.Find(&users).Error; err != nil {
return err
}
for _, user := range users {
settings := models.UserSettings{
UserID: user.ID,
DefaultProfileID: user.DefaultProfileID,
AutoTranscriptionEnabled: user.AutoTranscriptionEnabled,
AutoRenameEnabled: user.AutoRenameEnabled,
SummaryDefaultModel: user.SummaryDefaultModel,
}
if settings.UserID == 0 {
continue
}
if !settings.AutoTranscriptionEnabled && user.SettingsJSON == "" {
settings.AutoTranscriptionEnabled = true
}
if !settings.AutoRenameEnabled && user.SettingsJSON == "" {
settings.AutoRenameEnabled = true
}
if err := tx.Where("user_id = ?", settings.UserID).Assign(settings).FirstOrCreate(&settings).Error; err != nil {
return err
}
}
return nil
}
func backfillSystemSettings(tx *gorm.DB) error {
raw, err := scheduler.Marshal(scheduler.DefaultConfig())
if err != nil {
return err
}
setting := models.SystemSetting{
Key: scheduler.SettingKey,
ValueJSON: raw,
}
return tx.Where("key = ?", setting.Key).FirstOrCreate(&setting).Error
}
func backfillAPIKeys(tx *gorm.DB) error {
var rows []models.APIKey
if err := tx.Find(&rows).Error; err != nil {
return err
}
for _, row := range rows {
if err := row.BeforeSave(tx); err != nil {
return err
}
updates := map[string]any{
"key_hash": row.KeyHash,
"key_prefix": row.KeyPrefix,
"metadata_json": row.MetadataJSON,
}
if err := withPreservedUpdatedAt(tx.Model(&models.APIKey{}).Where("id = ?", row.ID), updates, time.Time{}).
Updates(updates).Error; err != nil {
return err
}
}
return nil
}
func backfillTranscriptionProfiles(tx *gorm.DB) error {
var rows []models.TranscriptionProfile
if err := tx.Find(&rows).Error; err != nil {
return err
}
for _, row := range rows {
if err := row.BeforeSave(tx); err != nil {
return err
}
updates := map[string]any{
"user_id": row.UserID,
"model_name": row.ModelName,
"model_family": row.ModelFamily,
"language": row.Language,
"diarization_enabled": row.DiarizationEnabled,
"device": row.Device,
"compute_type": row.ComputeType,
"config_json": row.ConfigJSON,
"is_default": row.IsDefault,
}
if err := withPreservedUpdatedAt(tx.Model(&models.TranscriptionProfile{}).Where("id = ?", row.ID), updates, row.UpdatedAt).
Updates(updates).Error; err != nil {
return err
}
}
return nil
}
func backfillTranscriptionJobs(tx *gorm.DB) error {
var rows []models.TranscriptionJob
if err := tx.Find(&rows).Error; err != nil {
return err
}
for _, row := range rows {
if err := row.SyncColumnsForMigration(); err != nil {
return err
}
updates := map[string]any{
"user_id": row.UserID,
"source_file_path": row.AudioPath,
"source_file_name": row.SourceFileName,
"source_file_hash": row.SourceFileHash,
"source_duration_ms": row.SourceDurationMs,
"language": row.Language,
"transcript_text": row.Transcript,
"output_json_path": row.OutputJSONPath,
"output_srt_path": row.OutputSRTPath,
"output_vtt_path": row.OutputVTTPath,
"latest_execution_id": row.LatestExecutionID,
"last_error": row.ErrorMessage,
"metadata_json": row.MetadataJSON,
"completed_at": row.CompletedAt,
"status": row.Status,
"title": row.Title,
}
if err := withPreservedUpdatedAt(tx.Model(&models.TranscriptionJob{}).Where("id = ?", row.ID), updates, row.UpdatedAt).
Updates(updates).Error; err != nil {
return err
}
}
return nil
}
func backfillTranscriptionExecutions(tx *gorm.DB) error {
var rows []models.TranscriptionJobExecution
if err := tx.Find(&rows).Error; err != nil {
return err
}
for _, row := range rows {
if err := row.SyncColumnsForMigration(); err != nil {
return err
}
updates := map[string]any{
"user_id": row.UserID,
"execution_number": row.ExecutionNumber,
"trigger_type": row.TriggerType,
"status": row.Status,
"profile_id": row.ProfileID,
"model_name": row.ModelName,
"model_family": row.ModelFamily,
"provider": row.Provider,
"device": row.Device,
"compute_type": row.ComputeType,
"request_json": row.RequestJSON,
"config_json": row.ConfigJSON,
"started_at": row.StartedAt,
"completed_at": row.CompletedAt,
"failed_at": row.FailedAt,
"error_message": row.ErrorMessage,
"logs_path": row.LogsPath,
"output_json_path": row.OutputJSONPath,
}
if err := withPreservedUpdatedAt(tx.Model(&models.TranscriptionJobExecution{}).Where("id = ?", row.ID), updates, time.Time{}).
Updates(updates).Error; err != nil {
return err
}
}
return nil
}
func backfillSummaryTemplates(tx *gorm.DB) error {
var rows []models.SummaryTemplate
if err := tx.Find(&rows).Error; err != nil {
return err
}
for _, row := range rows {
if err := row.BeforeSave(tx); err != nil {
return err
}
updates := map[string]any{
"user_id": row.UserID,
"name": row.Name,
"prompt": row.Prompt,
"description": row.Description,
"config_json": row.ConfigJSON,
"is_default": row.IsDefault,
}
if err := withPreservedUpdatedAt(tx.Model(&models.SummaryTemplate{}).Where("id = ?", row.ID), updates, row.UpdatedAt).
Updates(updates).Error; err != nil {
return err
}
}
return nil
}
func backfillLLMConfigs(tx *gorm.DB) error {
var rows []models.LLMConfig
if err := tx.Find(&rows).Error; err != nil {
return err
}
for _, row := range rows {
if err := row.BeforeSave(tx); err != nil {
return err
}
updates := map[string]any{
"user_id": row.UserID,
"name": row.Name,
"provider": row.Provider,
"model_name": row.ModelName,
"base_url": row.BaseURL,
"config_json": row.ConfigJSON,
"is_default": row.IsDefault,
}
if err := withPreservedUpdatedAt(tx.Model(&models.LLMConfig{}).Where("id = ?", row.ID), updates, row.UpdatedAt).
Updates(updates).Error; err != nil {
return err
}
}
return nil
}
func withPreservedUpdatedAt(db *gorm.DB, updates map[string]any, updatedAt time.Time) *gorm.DB {
if !updatedAt.IsZero() {
updates["updated_at"] = updatedAt
}
return db.Session(&gorm.Session{SkipHooks: true}).Set("gorm:update_track_time", false)
}