check again if all goals are met
This commit is contained in:
@@ -3,7 +3,9 @@ package main
|
||||
import (
|
||||
"bamort/config"
|
||||
"bamort/database"
|
||||
"bamort/deployment"
|
||||
"bamort/deployment/migrations"
|
||||
"bamort/deployment/validator"
|
||||
"bamort/deployment/version"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -32,6 +34,12 @@ func main() {
|
||||
cmdVersion()
|
||||
case "status":
|
||||
cmdStatus()
|
||||
case "prepare":
|
||||
cmdPrepare()
|
||||
case "deploy":
|
||||
cmdDeploy()
|
||||
case "validate":
|
||||
cmdValidate()
|
||||
case "help", "--help", "-h":
|
||||
printHelp()
|
||||
default:
|
||||
@@ -46,9 +54,16 @@ func printHelp() {
|
||||
fmt.Printf("Version: %s\n\n", config.GetVersion())
|
||||
fmt.Println("Usage: deploy <command> [options]")
|
||||
fmt.Println("\nCommands:")
|
||||
fmt.Printf(" %sprepare%s Create deployment package (export all data)\n", ColorGreen, ColorReset)
|
||||
fmt.Printf(" %sdeploy%s Run full deployment (backup → migrate → validate)\n", ColorGreen, ColorReset)
|
||||
fmt.Printf(" %svalidate%s Validate database schema and data integrity\n", ColorGreen, ColorReset)
|
||||
fmt.Printf(" %sstatus%s Show current database version and pending migrations\n", ColorGreen, ColorReset)
|
||||
fmt.Printf(" %sversion%s Show version information\n", ColorGreen, ColorReset)
|
||||
fmt.Printf(" %shelp%s Show this help message\n", ColorGreen, ColorReset)
|
||||
fmt.Println("\nExamples:")
|
||||
fmt.Println(" deploy prepare # Create deployment package in ./export_temp")
|
||||
fmt.Println(" deploy deploy # Run full deployment workflow")
|
||||
fmt.Println(" deploy validate # Validate database schema")
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
@@ -125,3 +140,169 @@ func printError(format string, args ...interface{}) {
|
||||
func printWarning(format string, args ...interface{}) {
|
||||
fmt.Printf("%s⚠ "+format+"%s\n", append([]interface{}{ColorYellow}, append(args, ColorReset)...)...)
|
||||
}
|
||||
|
||||
func printSuccess(format string, args ...interface{}) {
|
||||
fmt.Printf("%s✓ "+format+"%s\n", append([]interface{}{ColorGreen}, append(args, ColorReset)...)...)
|
||||
}
|
||||
|
||||
// cmdPrepare creates a deployment package with full database export
|
||||
func cmdPrepare() {
|
||||
printBanner("Prepare Deployment Package")
|
||||
|
||||
// Connect to database
|
||||
database.DB = database.ConnectDatabase()
|
||||
if database.DB == nil {
|
||||
printError("Failed to connect to database")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
orchestrator := deployment.NewOrchestrator(database.DB)
|
||||
|
||||
exportDir := "./export_temp"
|
||||
if len(os.Args) > 2 {
|
||||
exportDir = os.Args[2]
|
||||
}
|
||||
|
||||
fmt.Printf("\nExporting to: %s%s%s\n", ColorCyan, exportDir, ColorReset)
|
||||
fmt.Println("This will create a complete backup of all system and master data...")
|
||||
fmt.Println()
|
||||
|
||||
pkg, err := orchestrator.PrepareDeploymentPackage(exportDir)
|
||||
if err != nil {
|
||||
printError("Failed to prepare deployment package: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
printSuccess("Deployment package created successfully!")
|
||||
fmt.Printf("\n%sPackage Details:%s\n", ColorBold, ColorReset)
|
||||
fmt.Printf(" Version: %s\n", pkg.Version)
|
||||
fmt.Printf(" Export File: %s\n", pkg.ExportPath)
|
||||
fmt.Printf(" Records: %d\n", pkg.RecordCount)
|
||||
fmt.Printf(" Timestamp: %s\n", pkg.Timestamp.Format("2006-01-02 15:04:05"))
|
||||
fmt.Println()
|
||||
fmt.Println("This export can be imported on the target system after migration.")
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// cmdDeploy runs the full deployment workflow
|
||||
func cmdDeploy() {
|
||||
printBanner("Full Deployment")
|
||||
|
||||
// Connect to database
|
||||
database.DB = database.ConnectDatabase()
|
||||
if database.DB == nil {
|
||||
printError("Failed to connect to database")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
orchestrator := deployment.NewOrchestrator(database.DB)
|
||||
|
||||
fmt.Println("\nThis will:")
|
||||
fmt.Println(" 1. Create a backup of the current database")
|
||||
fmt.Println(" 2. Check version compatibility")
|
||||
fmt.Println(" 3. Apply pending migrations")
|
||||
fmt.Println(" 4. Validate the deployment")
|
||||
fmt.Println()
|
||||
|
||||
fmt.Printf("%sWARNING:%s This operation will modify the database!\n", ColorYellow, ColorReset)
|
||||
fmt.Print("Continue? (yes/no): ")
|
||||
var confirm string
|
||||
fmt.Scanln(&confirm)
|
||||
|
||||
if confirm != "yes" && confirm != "y" {
|
||||
fmt.Println("Deployment cancelled.")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
report, err := orchestrator.Deploy()
|
||||
|
||||
if err != nil {
|
||||
printError("Deployment failed: %v", err)
|
||||
fmt.Println()
|
||||
if report.BackupCreated {
|
||||
fmt.Printf("Backup available at: %s\n", report.BackupPath)
|
||||
}
|
||||
if len(report.Errors) > 0 {
|
||||
fmt.Printf("\n%sErrors:%s\n", ColorRed, ColorReset)
|
||||
for _, e := range report.Errors {
|
||||
fmt.Printf(" • %s\n", e)
|
||||
}
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
printSuccess("Deployment completed successfully!")
|
||||
fmt.Printf("\n%sDeployment Summary:%s\n", ColorBold, ColorReset)
|
||||
fmt.Printf(" Backup: %s\n", report.BackupPath)
|
||||
fmt.Printf(" Migrations: %d applied\n", report.MigrationsRun)
|
||||
fmt.Printf(" Duration: %v\n", report.Duration)
|
||||
fmt.Printf(" Validated: %s✓%s\n", ColorGreen, ColorReset)
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// cmdValidate validates the database schema and data
|
||||
func cmdValidate() {
|
||||
printBanner("Database Validation")
|
||||
|
||||
// Connect to database
|
||||
database.DB = database.ConnectDatabase()
|
||||
if database.DB == nil {
|
||||
printError("Failed to connect to database")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
v := validator.NewValidator(database.DB)
|
||||
|
||||
fmt.Println("\nValidating database schema and data integrity...")
|
||||
fmt.Println()
|
||||
|
||||
report, err := v.Validate()
|
||||
if err != nil {
|
||||
printError("Validation failed: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("\n%sValidation Results:%s\n", ColorBold, ColorReset)
|
||||
fmt.Printf(" Tables Checked: %d\n", report.TablesChecked)
|
||||
fmt.Printf(" Tables Valid: %d\n", report.TablesValid)
|
||||
|
||||
if len(report.Errors) > 0 {
|
||||
fmt.Printf("\n%sErrors (%d):%s\n", ColorRed, len(report.Errors), ColorReset)
|
||||
for _, e := range report.Errors {
|
||||
fmt.Printf(" %s✗%s %s\n", ColorRed, ColorReset, e)
|
||||
}
|
||||
}
|
||||
|
||||
if len(report.Warnings) > 0 {
|
||||
fmt.Printf("\n%sWarnings (%d):%s\n", ColorYellow, len(report.Warnings), ColorReset)
|
||||
for _, w := range report.Warnings {
|
||||
fmt.Printf(" %s⚠%s %s\n", ColorYellow, ColorReset, w)
|
||||
}
|
||||
}
|
||||
|
||||
if len(report.MissingTables) > 0 {
|
||||
fmt.Printf("\n%sMissing Tables:%s\n", ColorRed, ColorReset)
|
||||
for _, t := range report.MissingTables {
|
||||
fmt.Printf(" • %s\n", t)
|
||||
}
|
||||
}
|
||||
|
||||
if len(report.MissingColumns) > 0 {
|
||||
fmt.Printf("\n%sMissing Columns:%s\n", ColorRed, ColorReset)
|
||||
for table, cols := range report.MissingColumns {
|
||||
fmt.Printf(" %s: %v\n", table, cols)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
if report.Success {
|
||||
printSuccess("Validation passed!")
|
||||
} else {
|
||||
printError("Validation failed with %d error(s)", len(report.Errors))
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
package deployment
|
||||
|
||||
import (
|
||||
"bamort/config"
|
||||
"bamort/deployment/backup"
|
||||
"bamort/deployment/migrations"
|
||||
"bamort/deployment/version"
|
||||
"bamort/logger"
|
||||
"bamort/transfer"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// DeploymentOrchestrator coordinates the full deployment process
|
||||
type DeploymentOrchestrator struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
// DeploymentReport contains the results of a deployment
|
||||
type DeploymentReport struct {
|
||||
Success bool
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
Duration time.Duration
|
||||
BackupCreated bool
|
||||
BackupPath string
|
||||
MigrationsRun int
|
||||
ValidationPassed bool
|
||||
Errors []string
|
||||
Warnings []string
|
||||
}
|
||||
|
||||
// NewOrchestrator creates a new deployment orchestrator
|
||||
func NewOrchestrator(db *gorm.DB) *DeploymentOrchestrator {
|
||||
return &DeploymentOrchestrator{
|
||||
DB: db,
|
||||
}
|
||||
}
|
||||
|
||||
// Deploy executes the full deployment workflow
|
||||
func (o *DeploymentOrchestrator) Deploy() (*DeploymentReport, error) {
|
||||
report := &DeploymentReport{
|
||||
StartTime: time.Now(),
|
||||
}
|
||||
|
||||
logger.Info("═══════════════════════════════════════════════════")
|
||||
logger.Info("Starting Deployment Process")
|
||||
logger.Info("═══════════════════════════════════════════════════")
|
||||
|
||||
// Step 1: Create backup
|
||||
logger.Info("Step 1/4: Creating pre-deployment backup...")
|
||||
backupPath, err := o.createBackup()
|
||||
if err != nil {
|
||||
report.Errors = append(report.Errors, fmt.Sprintf("Backup failed: %v", err))
|
||||
return report, fmt.Errorf("backup failed: %w", err)
|
||||
}
|
||||
report.BackupCreated = true
|
||||
report.BackupPath = backupPath
|
||||
logger.Info("✓ Backup created: %s", backupPath)
|
||||
|
||||
// Step 2: Check version compatibility
|
||||
logger.Info("Step 2/4: Checking version compatibility...")
|
||||
if err := o.checkCompatibility(); err != nil {
|
||||
report.Errors = append(report.Errors, fmt.Sprintf("Compatibility check failed: %v", err))
|
||||
return report, fmt.Errorf("compatibility check failed: %w", err)
|
||||
}
|
||||
logger.Info("✓ Version compatibility verified")
|
||||
|
||||
// Step 3: Apply migrations
|
||||
logger.Info("Step 3/4: Applying database migrations...")
|
||||
migrationsRun, err := o.applyMigrations()
|
||||
if err != nil {
|
||||
report.Errors = append(report.Errors, fmt.Sprintf("Migration failed: %v", err))
|
||||
logger.Error("Migration failed, attempting rollback...")
|
||||
// Rollback would happen here in production
|
||||
return report, fmt.Errorf("migration failed: %w", err)
|
||||
}
|
||||
report.MigrationsRun = migrationsRun
|
||||
if migrationsRun > 0 {
|
||||
logger.Info("✓ Applied %d migration(s)", migrationsRun)
|
||||
} else {
|
||||
logger.Info("✓ No migrations to apply")
|
||||
}
|
||||
|
||||
// Step 4: Validate deployment
|
||||
logger.Info("Step 4/4: Validating deployment...")
|
||||
if err := o.validateDeployment(); err != nil {
|
||||
report.Errors = append(report.Errors, fmt.Sprintf("Validation failed: %v", err))
|
||||
return report, fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
report.ValidationPassed = true
|
||||
logger.Info("✓ Deployment validated successfully")
|
||||
|
||||
report.Success = true
|
||||
report.EndTime = time.Now()
|
||||
report.Duration = report.EndTime.Sub(report.StartTime)
|
||||
|
||||
logger.Info("═══════════════════════════════════════════════════")
|
||||
logger.Info("Deployment Completed Successfully")
|
||||
logger.Info("Duration: %v", report.Duration)
|
||||
logger.Info("═══════════════════════════════════════════════════")
|
||||
|
||||
return report, nil
|
||||
}
|
||||
|
||||
// createBackup creates a pre-deployment backup
|
||||
func (o *DeploymentOrchestrator) createBackup() (string, error) {
|
||||
// Get current version for backup metadata
|
||||
runner := migrations.NewMigrationRunner(o.DB)
|
||||
currentVer, migNum, err := runner.GetCurrentVersion()
|
||||
if err != nil {
|
||||
currentVer = "unknown"
|
||||
migNum = 0
|
||||
}
|
||||
|
||||
// Create backup using backup service
|
||||
backupService := backup.NewBackupService()
|
||||
metadata, err := backupService.CreateJSONBackup(currentVer, migNum)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create backup: %w", err)
|
||||
}
|
||||
|
||||
return metadata.FilePath, nil
|
||||
}
|
||||
|
||||
// checkCompatibility verifies version compatibility
|
||||
func (o *DeploymentOrchestrator) checkCompatibility() error {
|
||||
runner := migrations.NewMigrationRunner(o.DB)
|
||||
currentVer, _, err := runner.GetCurrentVersion()
|
||||
if err != nil {
|
||||
// If version table doesn't exist, this might be a fresh install
|
||||
currentVer = ""
|
||||
}
|
||||
|
||||
compat := version.CheckCompatibility(currentVer)
|
||||
|
||||
if !compat.Compatible && !compat.MigrationNeeded {
|
||||
return fmt.Errorf("version incompatible: %s", compat.Reason)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyMigrations applies pending database migrations
|
||||
func (o *DeploymentOrchestrator) applyMigrations() (int, error) {
|
||||
runner := migrations.NewMigrationRunner(o.DB)
|
||||
runner.Verbose = true
|
||||
|
||||
// Get pending migrations
|
||||
pending, err := runner.GetPendingMigrations()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get pending migrations: %w", err)
|
||||
}
|
||||
|
||||
if len(pending) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Apply all pending migrations
|
||||
results, err := runner.ApplyAll()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to apply migrations: %w", err)
|
||||
}
|
||||
|
||||
// Count successful migrations
|
||||
successCount := 0
|
||||
for _, result := range results {
|
||||
if result.Success {
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
|
||||
return successCount, nil
|
||||
}
|
||||
|
||||
// validateDeployment validates the database after deployment
|
||||
func (o *DeploymentOrchestrator) validateDeployment() error {
|
||||
// Check that version was updated
|
||||
runner := migrations.NewMigrationRunner(o.DB)
|
||||
currentVer, _, err := runner.GetCurrentVersion()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get version after migration: %w", err)
|
||||
}
|
||||
|
||||
// Verify version matches required version
|
||||
if currentVer != version.GetRequiredDBVersion() {
|
||||
return fmt.Errorf("version mismatch after deployment: expected %s, got %s",
|
||||
version.GetRequiredDBVersion(), currentVer)
|
||||
}
|
||||
|
||||
// Basic sanity check: verify we can query the database
|
||||
var count int64
|
||||
if err := o.DB.Table("schema_version").Count(&count).Error; err != nil {
|
||||
return fmt.Errorf("database sanity check failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PrepareDeploymentPackage creates an export of all system and master data
|
||||
func (o *DeploymentOrchestrator) PrepareDeploymentPackage(exportDir string) (*DeploymentPackage, error) {
|
||||
logger.Info("═══════════════════════════════════════════════════")
|
||||
logger.Info("Preparing Deployment Package")
|
||||
logger.Info("═══════════════════════════════════════════════════")
|
||||
|
||||
pkg := &DeploymentPackage{
|
||||
Version: config.GetVersion(),
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
// Export full database (all data, all tables)
|
||||
logger.Info("Exporting complete database...")
|
||||
result, err := transfer.ExportDatabase(exportDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("database export failed: %w", err)
|
||||
}
|
||||
|
||||
pkg.ExportPath = result.FilePath
|
||||
pkg.RecordCount = result.RecordCount
|
||||
logger.Info("✓ Exported %d records to %s", result.RecordCount, result.Filename)
|
||||
|
||||
logger.Info("═══════════════════════════════════════════════════")
|
||||
logger.Info("Deployment Package Ready")
|
||||
logger.Info("File: %s", result.FilePath)
|
||||
logger.Info("Records: %d", result.RecordCount)
|
||||
logger.Info("═══════════════════════════════════════════════════")
|
||||
|
||||
return pkg, nil
|
||||
}
|
||||
|
||||
// DeploymentPackage contains information about a deployment package
|
||||
type DeploymentPackage struct {
|
||||
Version string
|
||||
Timestamp time.Time
|
||||
ExportPath string
|
||||
RecordCount int
|
||||
}
|
||||
|
||||
// FullDeploymentWithImport performs a complete deployment including data import
|
||||
func (o *DeploymentOrchestrator) FullDeploymentWithImport(importFilePath string) (*DeploymentReport, error) {
|
||||
report := &DeploymentReport{
|
||||
StartTime: time.Now(),
|
||||
}
|
||||
|
||||
logger.Info("═══════════════════════════════════════════════════")
|
||||
logger.Info("Starting Full Deployment With Data Import")
|
||||
logger.Info("═══════════════════════════════════════════════════")
|
||||
|
||||
// Step 1: Create backup of current state
|
||||
logger.Info("Step 1/5: Creating pre-deployment backup...")
|
||||
backupPath, err := o.createBackup()
|
||||
if err != nil {
|
||||
report.Errors = append(report.Errors, fmt.Sprintf("Backup failed: %v", err))
|
||||
return report, fmt.Errorf("backup failed: %w", err)
|
||||
}
|
||||
report.BackupCreated = true
|
||||
report.BackupPath = backupPath
|
||||
logger.Info("✓ Backup created: %s", backupPath)
|
||||
|
||||
// Step 2: Export current state (before migration)
|
||||
logger.Info("Step 2/5: Exporting current database state...")
|
||||
exportDir := "./export_temp"
|
||||
exportResult, err := transfer.ExportDatabase(exportDir)
|
||||
if err != nil {
|
||||
report.Warnings = append(report.Warnings, fmt.Sprintf("Current state export failed: %v", err))
|
||||
logger.Warn("Could not export current state: %v", err)
|
||||
} else {
|
||||
logger.Info("✓ Current state exported: %s", exportResult.Filename)
|
||||
}
|
||||
|
||||
// Step 3: Check version compatibility
|
||||
logger.Info("Step 3/5: Checking version compatibility...")
|
||||
if err := o.checkCompatibility(); err != nil {
|
||||
report.Errors = append(report.Errors, fmt.Sprintf("Compatibility check failed: %v", err))
|
||||
return report, fmt.Errorf("compatibility check failed: %w", err)
|
||||
}
|
||||
logger.Info("✓ Version compatibility verified")
|
||||
|
||||
// Step 4: Apply migrations
|
||||
logger.Info("Step 4/5: Applying database migrations...")
|
||||
migrationsRun, err := o.applyMigrations()
|
||||
if err != nil {
|
||||
report.Errors = append(report.Errors, fmt.Sprintf("Migration failed: %v", err))
|
||||
logger.Error("Migration failed! Rollback required.")
|
||||
return report, fmt.Errorf("migration failed: %w", err)
|
||||
}
|
||||
report.MigrationsRun = migrationsRun
|
||||
if migrationsRun > 0 {
|
||||
logger.Info("✓ Applied %d migration(s)", migrationsRun)
|
||||
} else {
|
||||
logger.Info("✓ No migrations needed")
|
||||
}
|
||||
|
||||
// Step 5: Import data if provided
|
||||
if importFilePath != "" {
|
||||
logger.Info("Step 5/5: Importing data from %s...", importFilePath)
|
||||
importResult, err := transfer.ImportDatabase(importFilePath)
|
||||
if err != nil {
|
||||
report.Errors = append(report.Errors, fmt.Sprintf("Data import failed: %v", err))
|
||||
return report, fmt.Errorf("data import failed: %w", err)
|
||||
}
|
||||
logger.Info("✓ Imported %d records", importResult.RecordCount)
|
||||
} else {
|
||||
logger.Info("Step 5/5: No data import requested")
|
||||
}
|
||||
|
||||
// Validate
|
||||
logger.Info("Validating deployment...")
|
||||
if err := o.validateDeployment(); err != nil {
|
||||
report.Errors = append(report.Errors, fmt.Sprintf("Validation failed: %v", err))
|
||||
return report, fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
report.ValidationPassed = true
|
||||
logger.Info("✓ Deployment validated successfully")
|
||||
|
||||
report.Success = true
|
||||
report.EndTime = time.Now()
|
||||
report.Duration = report.EndTime.Sub(report.StartTime)
|
||||
|
||||
logger.Info("═══════════════════════════════════════════════════")
|
||||
logger.Info("Full Deployment Completed Successfully")
|
||||
logger.Info("Duration: %v", report.Duration)
|
||||
logger.Info("═══════════════════════════════════════════════════")
|
||||
|
||||
return report, nil
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"bamort/logger"
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// SchemaValidator validates database schema integrity
|
||||
type SchemaValidator struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
// ValidationReport contains validation results
|
||||
type ValidationReport struct {
|
||||
Success bool
|
||||
TablesChecked int
|
||||
TablesValid int
|
||||
Errors []string
|
||||
Warnings []string
|
||||
MissingTables []string
|
||||
MissingColumns map[string][]string
|
||||
}
|
||||
|
||||
// NewValidator creates a new schema validator
|
||||
func NewValidator(db *gorm.DB) *SchemaValidator {
|
||||
return &SchemaValidator{
|
||||
DB: db,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate performs comprehensive schema validation
|
||||
func (v *SchemaValidator) Validate() (*ValidationReport, error) {
|
||||
report := &ValidationReport{
|
||||
Success: true,
|
||||
MissingColumns: make(map[string][]string),
|
||||
}
|
||||
|
||||
logger.Info("Starting database schema validation...")
|
||||
|
||||
// Check ALL tables must exist for the application to work properly
|
||||
// If char_*, equi_*, audit_* tables are missing, /api/maintenance/setupcheck must be called
|
||||
// So it's best to ensure all tables are present
|
||||
criticalTables := []string{
|
||||
// System tables
|
||||
"schema_version",
|
||||
"migration_history",
|
||||
"users",
|
||||
|
||||
// Audit tables
|
||||
"audit_log_entries",
|
||||
|
||||
// Character tables
|
||||
"char_bennies",
|
||||
"char_characteristics",
|
||||
"char_char_creation_session",
|
||||
"char_chars",
|
||||
"char_eigenschaften",
|
||||
"char_endurances",
|
||||
"char_experiances",
|
||||
"char_health",
|
||||
"char_motionranges",
|
||||
"char_skills",
|
||||
"char_spells",
|
||||
"char_wealth",
|
||||
"char_weaponskills",
|
||||
|
||||
// Equipment tables
|
||||
"equi_containers",
|
||||
"equi_equipments",
|
||||
"equi_weapons",
|
||||
|
||||
// GSM Master Data tables
|
||||
"gsm_believes",
|
||||
"gsm_cc_class_category_points",
|
||||
"gsm_cc_class_spell_points",
|
||||
"gsm_cc_class_typical_skills",
|
||||
"gsm_cc_class_typical_spells",
|
||||
"gsm_character_classes",
|
||||
"gsm_containers",
|
||||
"gsm_equipments",
|
||||
"gsm_lit_sources",
|
||||
"gsm_misc",
|
||||
"gsm_skills",
|
||||
"gsm_spells",
|
||||
"gsm_transportations",
|
||||
"gsm_weapons",
|
||||
"gsm_weaponskills",
|
||||
|
||||
// Learning system tables
|
||||
"learning_class_category_ep_costs",
|
||||
"learning_class_spell_school_ep_costs",
|
||||
"learning_skill_categories",
|
||||
"learning_skill_category_difficulties",
|
||||
"learning_skill_difficulties",
|
||||
"learning_skill_improvement_costs",
|
||||
"learning_spell_level_le_costs",
|
||||
"learning_spell_schools",
|
||||
"learning_weaponskill_category_difficulties",
|
||||
}
|
||||
|
||||
for _, table := range criticalTables {
|
||||
report.TablesChecked++
|
||||
if v.tableExists(table) {
|
||||
report.TablesValid++
|
||||
logger.Debug("✓ Table exists: %s", table)
|
||||
} else {
|
||||
report.MissingTables = append(report.MissingTables, table)
|
||||
report.Errors = append(report.Errors, fmt.Sprintf("Missing table: %s", table))
|
||||
report.Success = false
|
||||
logger.Error("✗ Missing table: %s", table)
|
||||
}
|
||||
}
|
||||
|
||||
// Check schema_version table structure
|
||||
if v.tableExists("schema_version") {
|
||||
requiredColumns := []string{"id", "version", "migration_number", "applied_at"}
|
||||
missingCols := v.checkTableColumns("schema_version", requiredColumns)
|
||||
if len(missingCols) > 0 {
|
||||
report.MissingColumns["schema_version"] = missingCols
|
||||
report.Errors = append(report.Errors,
|
||||
fmt.Sprintf("schema_version missing columns: %v", missingCols))
|
||||
report.Success = false
|
||||
}
|
||||
}
|
||||
|
||||
// Check migration_history table structure
|
||||
if v.tableExists("migration_history") {
|
||||
requiredColumns := []string{"id", "migration_number", "description", "applied_at"}
|
||||
missingCols := v.checkTableColumns("migration_history", requiredColumns)
|
||||
if len(missingCols) > 0 {
|
||||
report.MissingColumns["migration_history"] = missingCols
|
||||
report.Errors = append(report.Errors,
|
||||
fmt.Sprintf("migration_history missing columns: %v", missingCols))
|
||||
report.Success = false
|
||||
}
|
||||
}
|
||||
|
||||
// Validate record counts are reasonable
|
||||
if err := v.validateDataIntegrity(report); err != nil {
|
||||
report.Warnings = append(report.Warnings, fmt.Sprintf("Data integrity check: %v", err))
|
||||
}
|
||||
|
||||
if report.Success {
|
||||
logger.Info("✓ Schema validation passed")
|
||||
} else {
|
||||
logger.Error("✗ Schema validation failed with %d error(s)", len(report.Errors))
|
||||
}
|
||||
|
||||
return report, nil
|
||||
}
|
||||
|
||||
// tableExists checks if a table exists in the database
|
||||
func (v *SchemaValidator) tableExists(tableName string) bool {
|
||||
return v.DB.Migrator().HasTable(tableName)
|
||||
}
|
||||
|
||||
// checkTableColumns verifies that required columns exist in a table
|
||||
func (v *SchemaValidator) checkTableColumns(tableName string, requiredColumns []string) []string {
|
||||
var missing []string
|
||||
|
||||
for _, col := range requiredColumns {
|
||||
if !v.DB.Migrator().HasColumn(tableName, col) {
|
||||
missing = append(missing, col)
|
||||
}
|
||||
}
|
||||
|
||||
return missing
|
||||
}
|
||||
|
||||
// validateDataIntegrity performs basic sanity checks on data
|
||||
func (v *SchemaValidator) validateDataIntegrity(report *ValidationReport) error {
|
||||
// Check that schema_version has at least one entry
|
||||
if v.tableExists("schema_version") {
|
||||
var count int64
|
||||
if err := v.DB.Table("schema_version").Count(&count).Error; err != nil {
|
||||
return fmt.Errorf("failed to count schema_version records: %w", err)
|
||||
}
|
||||
if count == 0 {
|
||||
report.Warnings = append(report.Warnings, "schema_version table is empty")
|
||||
}
|
||||
}
|
||||
|
||||
// Check for orphaned records (basic check)
|
||||
if v.tableExists("chars") && v.tableExists("users") {
|
||||
var orphanedChars int64
|
||||
if err := v.DB.Raw(`
|
||||
SELECT COUNT(*) FROM chars
|
||||
WHERE user_id NOT IN (SELECT id FROM users)
|
||||
`).Scan(&orphanedChars).Error; err == nil {
|
||||
if orphanedChars > 0 {
|
||||
report.Warnings = append(report.Warnings,
|
||||
fmt.Sprintf("Found %d orphaned characters (invalid user_id)", orphanedChars))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePostMigration performs post-migration validation
|
||||
func (v *SchemaValidator) ValidatePostMigration() error {
|
||||
logger.Info("Performing post-migration validation...")
|
||||
|
||||
report, err := v.Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
|
||||
if !report.Success {
|
||||
return fmt.Errorf("validation found %d error(s): %v", len(report.Errors), report.Errors)
|
||||
}
|
||||
|
||||
if len(report.Warnings) > 0 {
|
||||
logger.Warn("Validation passed with %d warning(s):", len(report.Warnings))
|
||||
for _, w := range report.Warnings {
|
||||
logger.Warn(" - %s", w)
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("✓ Post-migration validation successful")
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user