mirror of
https://github.com/rishikanthc/Scriberr.git
synced 2026-06-29 15:26:02 +00:00
Fix database migration backfill regressions
This commit is contained in:
@@ -302,6 +302,46 @@ func TestSchemaUpgradeRunsVersionedBackfill(t *testing.T) {
|
||||
assert.Equal(t, "large-v3", reloadedB.Parameters.Model)
|
||||
}
|
||||
|
||||
func TestSchemaUpgradePreservesUpdatedAt(t *testing.T) {
|
||||
db := openUnmigratedTestDB(t, "schema-upgrade-updated-at.db")
|
||||
|
||||
originalUpdatedAt := time.Date(2024, 12, 31, 12, 0, 0, 0, time.UTC)
|
||||
user := models.User{
|
||||
ID: 77,
|
||||
Username: "preserve-updated-at",
|
||||
Password: "pw",
|
||||
UpdatedAt: originalUpdatedAt,
|
||||
}
|
||||
require.NoError(t, db.Session(&gorm.Session{SkipHooks: true}).Create(&user).Error)
|
||||
require.NoError(t, recordSchemaVersion(db, 1))
|
||||
require.NoError(t, db.Exec("UPDATE users SET settings_json = '' WHERE id = ?", user.ID).Error)
|
||||
|
||||
require.NoError(t, Migrate(db))
|
||||
|
||||
var reloaded models.User
|
||||
require.NoError(t, db.First(&reloaded, "id = ?", user.ID).Error)
|
||||
assert.True(t, reloaded.UpdatedAt.Equal(originalUpdatedAt))
|
||||
}
|
||||
|
||||
func TestDetectLegacySchemaWithLegacySameNameTables(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "legacy-same-name.db")
|
||||
createLegacyDatabase(t, dbPath, false)
|
||||
|
||||
db, err := Open(dbPath)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { require.NoError(t, closeDB(db)) })
|
||||
|
||||
require.NoError(t, db.Exec("DROP TABLE transcription_jobs").Error)
|
||||
require.NoError(t, db.Exec("DROP TABLE transcription_job_executions").Error)
|
||||
require.NoError(t, db.Exec("DROP TABLE multi_track_files").Error)
|
||||
require.NoError(t, db.Exec("DROP TABLE llm_configs").Error)
|
||||
require.NoError(t, db.Exec("DROP TABLE summary_settings").Error)
|
||||
|
||||
legacy, err := detectLegacySchema(db)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, legacy)
|
||||
}
|
||||
|
||||
func TestLegacyMigrationPreservesData(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "legacy.db")
|
||||
createLegacyDatabase(t, dbPath, true)
|
||||
|
||||
@@ -71,6 +71,66 @@ func detectLegacySchema(db *gorm.DB) (bool, error) {
|
||||
}
|
||||
}
|
||||
|
||||
sameNameTables := []string{
|
||||
"users",
|
||||
"api_keys",
|
||||
"refresh_tokens",
|
||||
"transcription_profiles",
|
||||
"speaker_mappings",
|
||||
"summary_templates",
|
||||
"summaries",
|
||||
"notes",
|
||||
"chat_sessions",
|
||||
"chat_messages",
|
||||
}
|
||||
for _, table := range sameNameTables {
|
||||
if !db.Migrator().HasTable(table) {
|
||||
continue
|
||||
}
|
||||
legacyLike, err := isLegacySameNameTable(db, table)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if legacyLike {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func isLegacySameNameTable(db *gorm.DB, table string) (bool, error) {
|
||||
columns, err := db.Migrator().ColumnTypes(table)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("inspect columns for %s: %w", table, err)
|
||||
}
|
||||
columnNames := make(map[string]struct{}, len(columns))
|
||||
for _, column := range columns {
|
||||
columnNames[column.Name()] = struct{}{}
|
||||
}
|
||||
|
||||
requiredCurrentColumns := map[string][]string{
|
||||
"users": {"settings_json", "password_hash"},
|
||||
"api_keys": {"key_hash", "metadata_json"},
|
||||
"refresh_tokens": {"token_hash", "revoked_at"},
|
||||
"transcription_profiles": {"config_json", "user_id"},
|
||||
"speaker_mappings": {"display_name", "user_id", "transcription_id"},
|
||||
"summary_templates": {"config_json", "user_id"},
|
||||
"summaries": {"model_name", "user_id"},
|
||||
"notes": {"start_ms", "end_ms", "metadata_json", "user_id"},
|
||||
"chat_sessions": {"system_prompt", "user_id"},
|
||||
"chat_messages": {"chat_session_id", "user_id"},
|
||||
}
|
||||
|
||||
required := requiredCurrentColumns[table]
|
||||
if len(required) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
for _, column := range required {
|
||||
if _, ok := columnNames[column]; !ok {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"scriberr/internal/models"
|
||||
|
||||
@@ -40,44 +41,284 @@ func migrateStepV1ToV2(tx *gorm.DB) error {
|
||||
}
|
||||
|
||||
func backfillCompatibilityColumns(tx *gorm.DB) error {
|
||||
if err := resaveAll(tx, &[]models.User{}); err != nil {
|
||||
if err := backfillUsers(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := resaveAll(tx, &[]models.APIKey{}); err != nil {
|
||||
if err := backfillAPIKeys(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := resaveAll(tx, &[]models.TranscriptionProfile{}); err != nil {
|
||||
if err := backfillTranscriptionProfiles(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := resaveAll(tx, &[]models.TranscriptionJob{}); err != nil {
|
||||
if err := backfillTranscriptionJobs(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := resaveAll(tx, &[]models.TranscriptionJobExecution{}); err != nil {
|
||||
if err := backfillTranscriptionExecutions(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := resaveAll(tx, &[]models.MultiTrackFile{}); err != nil {
|
||||
if err := backfillMultiTrackFiles(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := resaveAll(tx, &[]models.SummaryTemplate{}); err != nil {
|
||||
if err := backfillSummaryTemplates(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := resaveAll(tx, &[]models.Note{}); err != nil {
|
||||
if err := backfillNotes(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := resaveAll(tx, &[]models.LLMConfig{}); err != nil {
|
||||
if err := backfillLLMConfigs(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resaveAll[T any](tx *gorm.DB, rows *[]T) error {
|
||||
if err := tx.Find(rows).Error; err != 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 := tx.Save(&row).Error; err != nil {
|
||||
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
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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, row.CreatedAt).
|
||||
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.BeforeSave(tx); 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.BeforeSave(tx); 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,
|
||||
"merged_file_path": row.MergedFilePath,
|
||||
"output_json_path": row.OutputJSONPath,
|
||||
}
|
||||
if err := withPreservedUpdatedAt(tx.Model(&models.TranscriptionJobExecution{}).Where("id = ?", row.ID), updates, row.CreatedAt).
|
||||
Updates(updates).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func backfillMultiTrackFiles(tx *gorm.DB) error {
|
||||
var rows []models.MultiTrackFile
|
||||
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,
|
||||
"file_name": row.FileName,
|
||||
"file_path": row.FilePath,
|
||||
"track_index": row.TrackIndex,
|
||||
"label": row.Label,
|
||||
"speaker_hint": row.SpeakerHint,
|
||||
"metadata_json": row.MetadataJSON,
|
||||
}
|
||||
if err := withPreservedUpdatedAt(tx.Model(&models.MultiTrackFile{}).Where("id = ?", row.ID), updates, row.CreatedAt).
|
||||
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 backfillNotes(tx *gorm.DB) error {
|
||||
var rows []models.Note
|
||||
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,
|
||||
"transcription_id": row.TranscriptionID,
|
||||
"content": row.Content,
|
||||
"start_ms": row.StartMS,
|
||||
"end_ms": row.EndMS,
|
||||
"metadata_json": row.MetadataJSON,
|
||||
}
|
||||
if err := withPreservedUpdatedAt(tx.Model(&models.Note{}).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.Set("gorm:update_track_time", false)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user