Files
ragflow/internal/dao/migration.go
Jin Hai 74866371ef Fix compatiblity issue (#13667)
### What problem does this PR solve?

1. Change go admin server port from 9385 to 9383 to avoid conflicts
2. Start go server after python servers are started completely, in
entrypoint.sh
3. Fix some database migration issue
4. Add more API routes in web to compliant with EE.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-03-18 11:51:03 +08:00

316 lines
11 KiB
Go

//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package dao
import (
"fmt"
"ragflow/internal/logger"
"strings"
"go.uber.org/zap"
"gorm.io/gorm"
)
// RunMigrations runs all manual database migrations
// These are migrations that cannot be handled by AutoMigrate alone
func RunMigrations(db *gorm.DB) error {
// Check if tenant_llm table has composite primary key and migrate to ID primary key
if err := migrateTenantLLMPrimaryKey(db); err != nil {
return fmt.Errorf("failed to migrate tenant_llm primary key: %w", err)
}
// Rename columns (correct typos)
if err := renameColumnIfExists(db, "task", "process_duation", "process_duration"); err != nil {
return fmt.Errorf("failed to rename task.process_duation: %w", err)
}
if err := renameColumnIfExists(db, "document", "process_duation", "process_duration"); err != nil {
return fmt.Errorf("failed to rename document.process_duation: %w", err)
}
// Add unique index on user.email
if err := migrateAddUniqueEmail(db); err != nil {
return fmt.Errorf("failed to add unique index on user.email: %w", err)
}
// Modify column types that AutoMigrate may not handle correctly
if err := modifyColumnTypes(db); err != nil {
return fmt.Errorf("failed to modify column types: %w", err)
}
logger.Info("All manual migrations completed successfully")
return nil
}
// migrateTenantLLMPrimaryKey migrates tenant_llm from composite primary key to ID primary key
// This corresponds to Python's update_tenant_llm_to_id_primary_key function
func migrateTenantLLMPrimaryKey(db *gorm.DB) error {
// Check if tenant_llm table exists
if !db.Migrator().HasTable("tenant_llm") {
return nil
}
// Check if 'id' column already exists using raw SQL
var idColumnExists int64
err := db.Raw(`
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'tenant_llm' AND COLUMN_NAME = 'id'
`).Scan(&idColumnExists).Error
if err != nil {
return err
}
if idColumnExists > 0 {
// Check if id is already a primary key with auto_increment
var count int64
err := db.Raw(`
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'tenant_llm'
AND COLUMN_NAME = 'id'
AND EXTRA LIKE '%auto_increment%'
`).Scan(&count).Error
if err != nil {
return err
}
if count > 0 {
// Already migrated
return nil
}
}
logger.Info("Migrating tenant_llm to use ID primary key...")
// Start transaction
return db.Transaction(func(tx *gorm.DB) error {
// Check for temp_id column and drop it if exists
var tempIdExists int64
tx.Raw(`SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'tenant_llm' AND COLUMN_NAME = 'temp_id'`).Scan(&tempIdExists)
if tempIdExists > 0 {
if err := tx.Exec("ALTER TABLE tenant_llm DROP COLUMN temp_id").Error; err != nil {
logger.Warn("Failed to drop temp_id column", zap.Error(err))
}
}
// Check if there's already an 'id' column
if idColumnExists > 0 {
// Modify existing id column to be auto_increment primary key
if err := tx.Exec(`
ALTER TABLE tenant_llm
MODIFY COLUMN id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY
`).Error; err != nil {
return fmt.Errorf("failed to modify id column: %w", err)
}
} else {
// Add id column as auto_increment primary key
if err := tx.Exec(`
ALTER TABLE tenant_llm
ADD COLUMN id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST
`).Error; err != nil {
return fmt.Errorf("failed to add id column: %w", err)
}
}
// Add unique index on (tenant_id, llm_factory, llm_name)
var idxExists int64
tx.Raw(`SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_NAME = 'tenant_llm' AND INDEX_NAME = 'idx_tenant_llm_unique'`).Scan(&idxExists)
if idxExists == 0 {
if err := tx.Exec(`
ALTER TABLE tenant_llm
ADD UNIQUE INDEX idx_tenant_llm_unique (tenant_id, llm_factory, llm_name)
`).Error; err != nil {
logger.Warn("Failed to add unique index idx_tenant_llm_unique", zap.Error(err))
}
}
logger.Info("tenant_llm primary key migration completed")
return nil
})
}
// migrateAddUniqueEmail adds unique index on user.email
func migrateAddUniqueEmail(db *gorm.DB) error {
if !db.Migrator().HasTable("user") {
return nil
}
// Check if unique index already exists using raw SQL
var count int64
db.Raw(`SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_NAME = 'user' AND INDEX_NAME = 'idx_user_email_unique'`).Scan(&count)
if count > 0 {
return nil
}
// Check if there's a duplicate email issue first
var duplicateCount int64
err := db.Raw(`
SELECT COUNT(*) FROM (
SELECT email FROM user GROUP BY email HAVING COUNT(*) > 1
) AS duplicates
`).Scan(&duplicateCount).Error
if err != nil {
return err
}
if duplicateCount > 0 {
logger.Warn("Found duplicate emails in user table, cannot add unique index", zap.Int64("count", duplicateCount))
return nil
}
logger.Info("Adding unique index on user.email...")
if err = db.Exec(`ALTER TABLE user ADD UNIQUE INDEX idx_user_email_unique (email)`).Error; err != nil {
// Check if error is MySQL duplicate index error (Error 1061)
errStr := err.Error()
if strings.Contains(errStr, "Error 1061") && strings.Contains(errStr, "Duplicate key name") {
logger.Info("Index already exists, skipping", zap.String("error", errStr))
return nil
}
return fmt.Errorf("failed to add unique index on email: %w", err)
}
return nil
}
// modifyColumnTypes modifies column types that need explicit ALTER statements
func modifyColumnTypes(db *gorm.DB) error {
// Helper function to check if column exists
columnExists := func(table, column string) bool {
var count int64
db.Raw(`SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = ? AND COLUMN_NAME = ?`, table, column).Scan(&count)
return count > 0
}
// dialog.top_k: ensure it's INTEGER with default 1024
if db.Migrator().HasTable("dialog") && columnExists("dialog", "top_k") {
if err := db.Exec(`ALTER TABLE dialog MODIFY COLUMN top_k BIGINT NOT NULL DEFAULT 1024`).Error; err != nil {
logger.Warn("Failed to modify dialog.top_k", zap.Error(err))
}
}
// tenant_llm.api_key: ensure it's TEXT type
if db.Migrator().HasTable("tenant_llm") && columnExists("tenant_llm", "api_key") {
if err := db.Exec(`ALTER TABLE tenant_llm MODIFY COLUMN api_key LONGTEXT`).Error; err != nil {
logger.Warn("Failed to modify tenant_llm.api_key", zap.Error(err))
}
}
// api_token.dialog_id: ensure it's varchar(32)
if db.Migrator().HasTable("api_token") && columnExists("api_token", "dialog_id") {
if err := db.Exec(`ALTER TABLE api_token MODIFY COLUMN dialog_id VARCHAR(32)`).Error; err != nil {
logger.Warn("Failed to modify api_token.dialog_id", zap.Error(err))
}
}
// canvas_template.title and description: ensure they're LONGTEXT type (same as Python JSONField)
// Note: Python's JSONField uses null=True with application-level default, not database DEFAULT
if db.Migrator().HasTable("canvas_template") {
if columnExists("canvas_template", "title") {
if err := db.Exec(`ALTER TABLE canvas_template MODIFY COLUMN title LONGTEXT NULL`).Error; err != nil {
logger.Warn("Failed to modify canvas_template.title", zap.Error(err))
}
}
if columnExists("canvas_template", "description") {
if err := db.Exec(`ALTER TABLE canvas_template MODIFY COLUMN description LONGTEXT NULL`).Error; err != nil {
logger.Warn("Failed to modify canvas_template.description", zap.Error(err))
}
}
}
// system_settings.value: ensure it's LONGTEXT
if db.Migrator().HasTable("system_settings") && columnExists("system_settings", "value") {
if err := db.Exec(`ALTER TABLE system_settings MODIFY COLUMN value LONGTEXT NOT NULL`).Error; err != nil {
logger.Warn("Failed to modify system_settings.value", zap.Error(err))
}
}
// knowledgebase.raptor_task_finish_at: ensure it's DateTime
if db.Migrator().HasTable("knowledgebase") && columnExists("knowledgebase", "raptor_task_finish_at") {
if err := db.Exec(`ALTER TABLE knowledgebase MODIFY COLUMN raptor_task_finish_at DATETIME`).Error; err != nil {
logger.Warn("Failed to modify knowledgebase.raptor_task_finish_at", zap.Error(err))
}
}
// knowledgebase.mindmap_task_finish_at: ensure it's DateTime
if db.Migrator().HasTable("knowledgebase") && columnExists("knowledgebase", "mindmap_task_finish_at") {
if err := db.Exec(`ALTER TABLE knowledgebase MODIFY COLUMN mindmap_task_finish_at DATETIME`).Error; err != nil {
logger.Warn("Failed to modify knowledgebase.mindmap_task_finish_at", zap.Error(err))
}
}
return nil
}
// renameColumnIfExists renames a column if it exists and the new column doesn't exist
func renameColumnIfExists(db *gorm.DB, tableName, oldName, newName string) error {
if !db.Migrator().HasTable(tableName) {
return nil
}
// Helper to check if column exists
columnExists := func(column string) bool {
var count int64
db.Raw(`SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = ? AND COLUMN_NAME = ?`, tableName, column).Scan(&count)
return count > 0
}
// Check if old column exists
if !columnExists(oldName) {
return nil
}
// Check if new column already exists
if columnExists(newName) {
// Both exist, drop the old one
logger.Warn("Both old and new columns exist, dropping old one",
zap.String("table", tableName),
zap.String("oldColumn", oldName),
zap.String("newColumn", newName))
return db.Migrator().DropColumn(tableName, oldName)
}
logger.Info("Renaming column",
zap.String("table", tableName),
zap.String("oldColumn", oldName),
zap.String("newColumn", newName))
return db.Migrator().RenameColumn(tableName, oldName, newName)
}
// addColumnIfNotExists adds a column if it doesn't exist
func addColumnIfNotExists(db *gorm.DB, tableName, columnName, columnDef string) error {
if !db.Migrator().HasTable(tableName) {
return nil
}
// Check if column exists using raw SQL
var count int64
db.Raw(`SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = ? AND COLUMN_NAME = ?`, tableName, columnName).Scan(&count)
if count > 0 {
return nil
}
logger.Info("Adding column",
zap.String("table", tableName),
zap.String("column", columnName))
sql := fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s", tableName, columnName, columnDef)
return db.Exec(sql).Error
}