Files
2026-07-16 22:16:45 +07:00

829 lines
26 KiB
Go

package mysql
import (
"errors"
"fmt"
"log"
"os"
"strings"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/logger"
"wucher/internal/config"
actionsignoff "wucher/internal/domain/action_signoff"
otherperson "wucher/internal/domain/other_person"
afterflightinspection "wucher/internal/domain/after_flight_inspection"
airrescuechecklist "wucher/internal/domain/air_rescue_checklist"
"wucher/internal/domain/audit"
"wucher/internal/domain/auth"
"wucher/internal/domain/base"
beforeflightinspection "wucher/internal/domain/before_flight_inspection"
complaint "wucher/internal/domain/complaint"
"wucher/internal/domain/dul"
dutyroster "wucher/internal/domain/duty_roster"
easarelease "wucher/internal/domain/easa_release"
"wucher/internal/domain/facility"
"wucher/internal/domain/federal_state"
filemanager "wucher/internal/domain/file_manager"
fleethistory "wucher/internal/domain/fleet_history"
fleetstatus "wucher/internal/domain/fleet_status"
"wucher/internal/domain/flight"
flightdata "wucher/internal/domain/flight_data"
flightinspection "wucher/internal/domain/flight_inspection"
flightinspectionfilechecklist "wucher/internal/domain/flight_inspection_file_checklist"
flightprepcheck "wucher/internal/domain/flight_prep_check"
fmreport "wucher/internal/domain/fm_report"
"wucher/internal/domain/forces_present"
"wucher/internal/domain/health_insurance_companies"
"wucher/internal/domain/helicopter"
helicopterfile "wucher/internal/domain/helicopter_file"
helicopterusage "wucher/internal/domain/helicopter_usage"
"wucher/internal/domain/hospital"
"wucher/internal/domain/icao"
insurancepatientdata "wucher/internal/domain/insurance_patient_data"
"wucher/internal/domain/land"
mastersettings "wucher/internal/domain/master_settings"
"wucher/internal/domain/mcf"
"wucher/internal/domain/medicine"
"wucher/internal/domain/mission"
"wucher/internal/domain/opc"
"wucher/internal/domain/operation"
patientdata "wucher/internal/domain/patient_data"
reserveac "wucher/internal/domain/reserve_ac"
takeover "wucher/internal/domain/takeover"
"wucher/internal/domain/transient"
"wucher/internal/domain/vocation"
"wucher/internal/shared/pkg/uuidv7"
)
func Connect(cfg config.MySQLConfig) (*gorm.DB, error) {
if cfg.DSN == "" {
return nil, errors.New("MYSQL_DSN is required")
}
db, err := gorm.Open(mysql.Open(cfg.DSN), &gorm.Config{
Logger: logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
SlowThreshold: 200 * time.Millisecond,
LogLevel: logger.Warn,
IgnoreRecordNotFoundError: true,
Colorful: true,
},
),
NowFunc: func() time.Time {
return time.Now().UTC()
},
})
if err != nil {
return nil, err
}
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
sqlDB.SetConnMaxLifetime(cfg.ConnMaxLifetime)
sqlDB.SetConnMaxIdleTime(cfg.ConnMaxIdleTime)
return db, nil
}
func AutoMigrate(db *gorm.DB) error {
probe := newSchemaCache(db)
if db.Dialector.Name() == "mysql" &&
db.Migrator().HasColumn("flight_data", "hook_realeases") &&
!db.Migrator().HasColumn("flight_data", "hook_releases") {
if err := db.Exec("ALTER TABLE flight_data RENAME COLUMN hook_realeases TO hook_releases").Error; err != nil {
return err
}
}
if err := db.AutoMigrate(
&auth.Role{},
&auth.Permission{},
&auth.User{},
&auth.UserRole{},
&auth.EmailOutboxMessage{},
&auth.PilotProfile{},
&auth.DoctorProfile{},
&auth.AirRescuerProfile{},
&auth.TechnicianProfile{},
&auth.FlightAssistantProfile{},
&auth.StaffProfile{},
&auth.RolePermission{},
&auth.UserIdentity{},
&auth.UserMicrosoftOAuthToken{},
&auth.UserTOTP{},
&auth.UserEmailLoginOTP{},
&auth.UserWebAuthnCredential{},
&auth.PasswordResetToken{},
&auth.SecurityPINResetToken{},
&transient.TokenEntry{},
&transient.QueueIdempotencyRecord{},
&helicopter.Helicopter{},
&helicopterusage.HelicopterUsage{},
&flightinspection.FlightInspection{},
&flight.Flight{},
&flightdata.FlightData{},
&flightdata.HESLOFlight{},
&flightdata.HESLOSling{},
&flightdata.LoggingSling{},
&flightdata.HECFlight{},
&flightdata.HECSling{},
&flightdata.HECLoads{},
&mission.Mission{},
&mission.MissionFile{},
&reserveac.ReserveAc{},
&helicopterfile.HelicopterFile{},
&beforeflightinspection.BeforeFlightInspection{},
&afterflightinspection.AfterFlightInspection{},
&fmreport.Report{},
&fmreport.FleetHistory{},
&flightprepcheck.FlightPrepCheck{},
&flightinspectionfilechecklist.FlightInspectionFileChecklist{},
&takeover.TakeoverAc{},
&takeover.TakeoverRosterCrew{},
&takeover.TakeoverOtherPerson{},
&otherperson.OtherPerson{},
&takeover.TakeoverFile{},
&complaint.Complaint{},
&easarelease.EASARelease{},
&actionsignoff.ActionSignoff{},
&mcf.MaintenanceCheckFlight{},
&fleethistory.FleetHistory{},
&fleetstatus.FleetStatus{},
&fleetstatus.MaintenanceSchedule{},
&fleetstatus.FleetStatusFile{},
&fleetstatus.FleetStatusServiceLog{},
&facility.Facility{},
&filemanager.Folder{},
&filemanager.File{},
&filemanager.Attachment{},
&filemanager.FileUploadIntent{},
&filemanager.FileProcessingOutboxMessage{},
&filemanager.FileStatusRealtimeEvent{},
&base.BaseCategory{},
&base.Base{},
&base.BaseOperationalShiftTime{},
&base.BaseContactRole{},
&dul.DUL{},
&medicine.MedicineGroup{},
&medicine.MotorReaction{},
&medicine.MedicineMotorReaction{},
&medicine.Medicine{},
&vocation.Vocation{},
&opc.Opc{},
&forces_present.ForcesPresent{},
&hospital.Hospital{},
&health_insurance_companies.HealthInsuranceCompany{},
&federal_state.FederalState{},
&icao.ICAO{},
&land.Land{},
&operation.HEMSOperationalData{},
&operation.OperationalDataForcePresent{},
&operation.OperationalFile{},
&operation.HEMSOperationCategory{},
&operation.HEMSOperation{},
&mastersettings.MasterSettings{},
&mastersettings.MasterSettingValue{},
&dutyroster.DutyRoster{},
&dutyroster.DutyRosterCrew{},
&airrescuechecklist.AirRescuerChecklist{},
&airrescuechecklist.AirRescuerChecklistItem{},
&audit.AuditLog{},
&insurancepatientdata.InsurancePatientData{},
&patientdata.PatientData{},
); err != nil {
return err
}
if db.Dialector.Name() == "mysql" {
if err := ensureBaseOperationalShiftTimeColumns(db); err != nil {
return err
}
if err := ensureBaseDefaultShiftColumns(db); err != nil {
return err
}
if err := ensureHelicopterColumns(db); err != nil {
return err
}
if err := ensureHelicopterFileAttachmentColumns(db); err != nil {
return err
}
if err := ensureActionSignoffStandaloneFlightUnique(db); err != nil {
return err
}
}
if err := backfillFacilityCategories(db); err != nil {
return err
}
// AutoMigrate won't remove columns; explicitly drop deprecated columns.
deprecatedColumns := []struct {
model any
column string
}{
{model: &helicopter.Helicopter{}, column: "maintenance"},
{model: &helicopter.Helicopter{}, column: "counter"},
{model: &flightinspection.FlightInspection{}, column: "helicopter_id"},
{model: &flightinspection.FlightInspection{}, column: "operation_id"},
{model: "flights", column: "mission_type"},
{model: &complaint.Complaint{}, column: "mcf_required"},
{model: &complaint.Complaint{}, column: "mcf_decided_at"},
{model: &complaint.Complaint{}, column: "mcf_decided_by"},
{model: &complaint.Complaint{}, column: "mcf_completed_at"},
{model: &complaint.Complaint{}, column: "mcf_result"},
{model: &complaint.Complaint{}, column: "mcf_notes"},
{model: &mcf.MaintenanceCheckFlight{}, column: "flight_inspection_id"},
{model: &complaint.Complaint{}, column: "flight_inspection_id"},
{model: &easarelease.EASARelease{}, column: "flight_inspection_id"},
{model: &complaint.Complaint{}, column: "action_signed_at"},
{model: &complaint.Complaint{}, column: "action_signed_by"},
{model: &actionsignoff.ActionSignoff{}, column: "action_taken"},
{model: &actionsignoff.ActionSignoff{}, column: "created_by"},
{model: &actionsignoff.ActionSignoff{}, column: "updated_by"},
{model: &actionsignoff.ActionSignoff{}, column: "deleted_at"},
{model: &actionsignoff.ActionSignoff{}, column: "deleted_by"},
// EASA maintenance program field removed from the release + its fm-report snapshot.
{model: &easarelease.EASARelease{}, column: "easa_maintenance_program"},
{model: &fmreport.FleetHistory{}, column: "easa_maintenance_program"},
// medical_license_no, license_issued_at, and license_expired_at removed from doctor_profiles;
// is_chief_physician_in_charge removed from air_rescuer_profiles.
{model: &auth.DoctorProfile{}, column: "medical_license_no"},
{model: &auth.DoctorProfile{}, column: "license_issued_at"},
{model: &auth.DoctorProfile{}, column: "license_expired_at"},
{model: &auth.AirRescuerProfile{}, column: "is_chief_physician_in_charge"},
}
if db.Dialector.Name() == "mysql" {
deprecatedConstraints := []struct{ table, name string }{
{"complaints", "fk_complaints_flight_inspection"},
{"easa_releases", "fk_easa_releases_flight_inspection"},
{"maintenance_check_flights", "fk_maintenance_check_flights_flight_inspection"},
}
for _, fc := range deprecatedConstraints {
if db.Migrator().HasConstraint(fc.table, fc.name) {
if err := db.Migrator().DropConstraint(fc.table, fc.name); err != nil {
return err
}
}
}
for _, item := range deprecatedColumns {
if !probe.HasColumn(item.model, item.column) {
continue
}
if err := db.Migrator().DropColumn(item.model, item.column); err != nil {
return err
}
}
if db.Migrator().HasConstraint("patient_data", "fk_patient_data_place") {
if err := db.Migrator().DropConstraint("patient_data", "fk_patient_data_place"); err != nil {
return err
}
}
if probe.HasColumn("patient_data", "place_id") {
if err := db.Migrator().DropColumn("patient_data", "place_id"); err != nil {
return err
}
}
}
if probe.HasTable("places") {
if err := db.Migrator().DropTable("places"); err != nil {
return err
}
}
// Flight prep checks refactor: move from dynamic item rows to static boolean fields.
if db.Migrator().HasConstraint(&flightprepcheck.FlightPrepCheck{}, "fk_flight_prep_checks_flight_prep_item") {
if err := db.Migrator().DropConstraint(&flightprepcheck.FlightPrepCheck{}, "fk_flight_prep_checks_flight_prep_item"); err != nil {
return err
}
}
if db.Migrator().HasIndex(&flightprepcheck.FlightPrepCheck{}, "uk_flight_prep_checks_inspection_item") {
if err := db.Migrator().DropIndex(&flightprepcheck.FlightPrepCheck{}, "uk_flight_prep_checks_inspection_item"); err != nil {
return err
}
}
if db.Migrator().HasIndex(&flightprepcheck.FlightPrepCheck{}, "idx_flight_prep_checks_flight_inspection_sorted") {
if err := db.Migrator().DropIndex(&flightprepcheck.FlightPrepCheck{}, "idx_flight_prep_checks_flight_inspection_sorted"); err != nil {
return err
}
}
if probe.HasColumn(&flightprepcheck.FlightPrepCheck{}, "flight_prep_item_id") {
if err := db.Migrator().DropColumn(&flightprepcheck.FlightPrepCheck{}, "flight_prep_item_id"); err != nil {
return err
}
}
if probe.HasColumn(&flightprepcheck.FlightPrepCheck{}, "is_done") {
if err := db.Migrator().DropColumn(&flightprepcheck.FlightPrepCheck{}, "is_done"); err != nil {
return err
}
}
if probe.HasColumn(&flightprepcheck.FlightPrepCheck{}, "note") {
if err := db.Migrator().DropColumn(&flightprepcheck.FlightPrepCheck{}, "note"); err != nil {
return err
}
}
if !probe.HasColumn(&flightprepcheck.FlightPrepCheck{}, "notam_briefing") {
if err := db.Migrator().AddColumn(&flightprepcheck.FlightPrepCheck{}, "NOTAMBriefing"); err != nil {
return err
}
}
if !probe.HasColumn(&flightprepcheck.FlightPrepCheck{}, "weather_briefing") {
if err := db.Migrator().AddColumn(&flightprepcheck.FlightPrepCheck{}, "WeatherBriefing"); err != nil {
return err
}
}
if !probe.HasColumn(&flightprepcheck.FlightPrepCheck{}, "operational_flight_plan") {
if err := db.Migrator().AddColumn(&flightprepcheck.FlightPrepCheck{}, "OperationalFlightPlan"); err != nil {
return err
}
}
if !db.Migrator().HasIndex(&flightprepcheck.FlightPrepCheck{}, "uk_flight_prep_checks_inspection") {
if err := db.Migrator().CreateIndex(&flightprepcheck.FlightPrepCheck{}, "uk_flight_prep_checks_inspection"); err != nil {
return err
}
}
// Rename legacy duty roster tables to neutral names.
if probe.HasTable("hems_duty_rosters") && !probe.HasTable("duty_rosters") {
if err := db.Exec("RENAME TABLE hems_duty_rosters TO duty_rosters").Error; err != nil {
return err
}
}
if probe.HasTable("hems_duty_roster_crews") && !probe.HasTable("duty_roster_crews") {
if err := db.Exec("RENAME TABLE hems_duty_roster_crews TO duty_roster_crews").Error; err != nil {
return err
}
}
// Rename legacy duty roster base column to general name.
if probe.HasColumn(&dutyroster.DutyRoster{}, "hems_base_id") && !probe.HasColumn(&dutyroster.DutyRoster{}, "base_id") {
if err := db.Exec("ALTER TABLE duty_rosters RENAME COLUMN hems_base_id TO base_id").Error; err != nil {
return err
}
}
if err := db.Exec("UPDATE users SET username = NULL WHERE username IS NOT NULL AND TRIM(username) = ''").Error; err != nil {
return err
}
if db.Dialector.Name() == "mysql" && probe.HasColumn(&auth.User{}, "username") {
if err := db.Exec("ALTER TABLE users MODIFY COLUMN username varchar(100) NULL").Error; err != nil {
return err
}
}
if err := backfillUserRoles(db); err != nil {
return err
}
if err := backfillBaseCategories(db); err != nil {
return err
}
if err := backfillMissionTypes(db); err != nil {
return err
}
if err := backfillPermissionRequiresPIN(db); err != nil {
return err
}
if err := alignFlightDataHospitalConstraints(db); err != nil {
return err
}
if err := ensureFlightDataPilotNullable(db); err != nil {
return err
}
if err := ensureFlightDataStatusColumn(db); err != nil {
return err
}
if err := ensureMissionTimeColumns(db); err != nil {
return err
}
if db.Dialector.Name() == "mysql" {
if err := db.Exec("ALTER TABLE flights MODIFY COLUMN mission_code varchar(64) NULL").Error; err != nil {
return err
}
if err := db.Exec("UPDATE flights SET mission_code = NULL WHERE mission_code IS NOT NULL AND TRIM(mission_code) = ''").Error; err != nil {
return err
}
}
return nil
}
// ensureActionSignoffStandaloneFlightUnique enforces one standalone (complaint-independent)
// sign-off per flight via a functional unique index: the key is flight_id for standalone
// rows (complaint_id IS NULL) and NULL for complaint-linked rows (which are deduped by
// uidx_action_signoffs_complaint_id instead). Requires MySQL 8.0.13+ (functional index).
func ensureActionSignoffStandaloneFlightUnique(db *gorm.DB) error {
if !db.Migrator().HasTable(&actionsignoff.ActionSignoff{}) {
return nil
}
if db.Migrator().HasIndex(&actionsignoff.ActionSignoff{}, "uidx_action_signoffs_standalone_flight") {
return nil
}
return db.Exec(
"CREATE UNIQUE INDEX uidx_action_signoffs_standalone_flight " +
"ON action_signoffs ((IF(complaint_id IS NULL, flight_id, NULL)))",
).Error
}
func ensureBaseOperationalShiftTimeColumns(db *gorm.DB) error {
if !db.Migrator().HasTable(&base.BaseOperationalShiftTime{}) {
return nil
}
if !db.Migrator().HasColumn(&base.BaseOperationalShiftTime{}, "DateStart") {
if err := db.Exec(`ALTER TABLE base_operational_shift_times ADD COLUMN date_start DATE NULL`).Error; err != nil {
return err
}
}
if !db.Migrator().HasColumn(&base.BaseOperationalShiftTime{}, "DateEnd") {
if err := db.Exec(`ALTER TABLE base_operational_shift_times ADD COLUMN date_end DATE NULL`).Error; err != nil {
return err
}
}
if db.Migrator().HasColumn("base_operational_shift_times", "operation_date") {
if err := db.Exec(`
UPDATE base_operational_shift_times
SET date_start = COALESCE(date_start, operation_date),
date_end = COALESCE(date_end, operation_date)
WHERE deleted_at IS NULL
`).Error; err != nil {
return err
}
if db.Migrator().HasConstraint(&base.BaseOperationalShiftTime{}, "fk_base_operational_shift_times_base") {
if err := db.Exec(`ALTER TABLE base_operational_shift_times DROP FOREIGN KEY fk_base_operational_shift_times_base`).Error; err != nil {
return err
}
}
if db.Migrator().HasIndex(&base.BaseOperationalShiftTime{}, "idx_base_operational_shift_times_base_date") {
if err := db.Migrator().DropIndex(&base.BaseOperationalShiftTime{}, "idx_base_operational_shift_times_base_date"); err != nil {
return err
}
}
if err := db.Exec(`ALTER TABLE base_operational_shift_times DROP COLUMN operation_date`).Error; err != nil {
return err
}
if err := db.Migrator().CreateIndex(&base.BaseOperationalShiftTime{}, "idx_base_operational_shift_times_base_date"); err != nil {
return err
}
if err := db.Exec(`
ALTER TABLE base_operational_shift_times
ADD CONSTRAINT fk_base_operational_shift_times_base
FOREIGN KEY (base_id) REFERENCES bases(id)
ON UPDATE CASCADE
ON DELETE CASCADE
`).Error; err != nil {
return err
}
}
return db.Exec(`
ALTER TABLE base_operational_shift_times
MODIFY COLUMN date_start DATE NULL,
MODIFY COLUMN date_end DATE NULL,
MODIFY COLUMN shift_start TIME NULL,
MODIFY COLUMN shift_end TIME NULL
`).Error
}
func ensureBaseDefaultShiftColumns(db *gorm.DB) error {
if !db.Migrator().HasTable(&base.Base{}) {
return nil
}
if !db.Migrator().HasColumn(&base.Base{}, "DefaultStartTimeType") {
if err := db.Exec(`ALTER TABLE bases ADD COLUMN default_start_time_type VARCHAR(16) NOT NULL DEFAULT 'FIXED'`).Error; err != nil {
return err
}
}
if !db.Migrator().HasColumn(&base.Base{}, "DefaultEndTimeType") {
if err := db.Exec(`ALTER TABLE bases ADD COLUMN default_end_time_type VARCHAR(16) NOT NULL DEFAULT 'FIXED'`).Error; err != nil {
return err
}
}
return db.Exec(`
ALTER TABLE bases
MODIFY COLUMN default_shift_start TIME NULL,
MODIFY COLUMN default_shift_end TIME NULL
`).Error
}
func alignFlightDataHospitalConstraints(db *gorm.DB) error {
if db == nil || db.Dialector.Name() != "mysql" {
return nil
}
if err := alignFlightDataHospitalConstraint(db, "from_hospital_id", "FromHospital", "fk_flight_data_from_hospital"); err != nil {
return err
}
if err := alignFlightDataHospitalConstraint(db, "to_hospital_id", "ToHospital", "fk_flight_data_to_hospital"); err != nil {
return err
}
return nil
}
func alignFlightDataHospitalConstraint(db *gorm.DB, column, association, constraintName string) error {
type foreignKeyInfo struct {
ConstraintName string `gorm:"column:constraint_name"`
ReferencedTableName string `gorm:"column:referenced_table_name"`
}
var info foreignKeyInfo
err := db.Table("information_schema.key_column_usage").
Select("constraint_name, referenced_table_name").
Where("table_schema = DATABASE() AND table_name = ? AND column_name = ? AND referenced_table_name IS NOT NULL", "flight_data", column).
Order("ordinal_position ASC").
Limit(1).
Scan(&info).Error
if err != nil {
return err
}
if info.ReferencedTableName == "no_icao_codes" {
return nil
}
if info.ConstraintName != "" {
if err := db.Exec("ALTER TABLE `flight_data` DROP FOREIGN KEY `" + info.ConstraintName + "`").Error; err != nil {
return err
}
}
return db.Migrator().CreateConstraint(&flightdata.FlightData{}, association)
}
func ensureFlightDataPilotNullable(db *gorm.DB) error {
if db == nil || db.Dialector.Name() != "mysql" {
return nil
}
if !db.Migrator().HasTable(&flightdata.FlightData{}) {
return nil
}
type columnInfo struct {
IsNullable string `gorm:"column:is_nullable"`
}
var info columnInfo
if err := db.Table("information_schema.columns").
Select("is_nullable").
Where("table_schema = DATABASE() AND table_name = ? AND column_name = ?", "flight_data", "co_pilot_id").
Limit(1).
Scan(&info).Error; err != nil {
return err
}
// Column missing (e.g. not yet created by migrations) or already nullable: nothing to do.
if strings.TrimSpace(info.IsNullable) == "" || strings.EqualFold(strings.TrimSpace(info.IsNullable), "YES") {
return nil
}
return db.Exec("ALTER TABLE flight_data MODIFY COLUMN co_pilot_id binary(16) NULL").Error
}
func ensureFlightDataStatusColumn(db *gorm.DB) error {
if db == nil {
return nil
}
if !db.Migrator().HasTable(&flightdata.FlightData{}) {
return nil
}
if !db.Migrator().HasColumn(&flightdata.FlightData{}, "status") {
addColumnSQL := "ALTER TABLE flight_data ADD COLUMN status varchar(20) NOT NULL DEFAULT 'in_progress'"
if db.Dialector.Name() == "mysql" {
addColumnSQL = "ALTER TABLE flight_data ADD COLUMN status varchar(20) NOT NULL DEFAULT 'in_progress' AFTER id"
}
if err := db.Exec(addColumnSQL).Error; err != nil {
return err
}
}
if err := db.Exec("UPDATE flight_data SET status = 'in_progress' WHERE status IS NULL OR TRIM(status) = '' OR status IN ('on_going', 'on_progress')").Error; err != nil {
return err
}
return db.Exec("UPDATE flight_data SET status = 'completed' WHERE status = 'complete'").Error
}
func ensureMissionTimeColumns(db *gorm.DB) error {
if db == nil {
return nil
}
if !db.Migrator().HasTable(&mission.Mission{}) {
return nil
}
if db.Dialector.Name() != "mysql" {
return nil
}
return db.Exec(`
ALTER TABLE missions
MODIFY COLUMN start_time TIME NULL,
MODIFY COLUMN end_time TIME NULL
`).Error
}
func ensureHelicopterColumns(db *gorm.DB) error {
if !db.Migrator().HasTable(&helicopter.Helicopter{}) {
return nil
}
if !db.Migrator().HasColumn(&helicopter.Helicopter{}, "DeletedAt") {
if err := db.Exec(`ALTER TABLE helicopters ADD COLUMN deleted_at DATETIME(6) NULL AFTER updated_at`).Error; err != nil {
return err
}
}
return nil
}
func ensureHelicopterFileAttachmentColumns(db *gorm.DB) error {
if db == nil || db.Dialector.Name() != "mysql" {
return nil
}
if !db.Migrator().HasTable(&helicopterfile.HelicopterFile{}) {
return nil
}
if !db.Migrator().HasColumn(&helicopterfile.HelicopterFile{}, "SourceFileID") {
if err := db.Exec("ALTER TABLE helicopter_files ADD COLUMN source_file_id binary(16) NULL AFTER file_attachment_id").Error; err != nil {
return err
}
}
return db.Exec("ALTER TABLE helicopter_files MODIFY COLUMN file_attachment_id binary(16) NULL").Error
}
func backfillBaseCategories(db *gorm.DB) error {
now := time.Now().UTC()
seeds := []struct {
key string
name string
}{
{key: base.CategoryKeyRegular, name: "Regular"},
{key: base.CategoryKeyHEMS, name: "HEMS"},
}
for _, seed := range seeds {
if err := db.Exec(
`INSERT INTO base_categories (id, `+"`key`"+`, name, created_at, updated_at)
SELECT ?, ?, ?, ?, ?
WHERE NOT EXISTS (
SELECT 1 FROM base_categories WHERE `+"`key`"+` = ?
)`,
uuidv7.MustBytes(),
seed.key,
seed.name,
now,
now,
seed.key,
).Error; err != nil {
return fmt.Errorf("seed base category %q: %w", seed.key, err)
}
}
var regular struct {
ID []byte `gorm:"column:id"`
}
if err := db.Table("base_categories").
Select("id").
Where("`key` = ?", base.CategoryKeyRegular).
Take(&regular).Error; err != nil {
return err
}
return db.Exec(
"UPDATE bases SET base_category_id = ? WHERE base_category_id IS NULL OR LENGTH(base_category_id) = 0",
regular.ID,
).Error
}
func backfillFacilityCategories(db *gorm.DB) error {
if db == nil {
return nil
}
mappings := map[string]string{
"fuel-truck": "heslo-rope-label",
"heslo-sling": "heslo-rope-length",
"logging-sling": "heslo-hook",
"hec-sling": "hec-rope-label",
"hems-hec-sling": "hec-rope-length",
}
for oldValue, newValue := range mappings {
if err := db.Exec(
"UPDATE facilities SET category = ? WHERE LOWER(TRIM(category)) = ?",
newValue,
oldValue,
).Error; err != nil {
return fmt.Errorf("backfill facility category %q -> %q: %w", oldValue, newValue, err)
}
}
return nil
}
func backfillUserRoles(db *gorm.DB) error {
var users []struct {
ID []byte `gorm:"column:id"`
RoleID []byte `gorm:"column:role_id"`
}
if err := db.Table("users").
Select("id, role_id").
Where("role_id IS NOT NULL").
Find(&users).Error; err != nil {
return err
}
if len(users) == 0 {
return nil
}
items := make([]auth.UserRole, 0, len(users))
for i := range users {
if len(users[i].ID) != 16 || len(users[i].RoleID) != 16 {
continue
}
items = append(items, auth.UserRole{
UserID: append([]byte(nil), users[i].ID...),
RoleID: append([]byte(nil), users[i].RoleID...),
})
}
if len(items) == 0 {
return nil
}
return db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "user_id"}, {Name: "role_id"}},
DoNothing: true,
}).Create(&items).Error
}
func backfillPermissionRequiresPIN(db *gorm.DB) error {
// Intentionally no-op.
// Do not override existing permissions.requires_pin values on startup.
// Source of truth for requires_pin is the database row itself.
_ = db
return nil
}
func backfillMissionTypes(db *gorm.DB) error {
categories := []struct {
key string
name string
id []byte
}{
{key: "HEMS", name: "HEMS", id: uuidv7.MustBytes()},
{key: "CAT", name: "CAT", id: uuidv7.MustBytes()},
{key: "SPO", name: "SPO", id: uuidv7.MustBytes()},
{key: "NCO", name: "NCO", id: uuidv7.MustBytes()},
}
for i := range categories {
if err := db.Exec(
`INSERT INTO mission_categories (id, code_type, name)
SELECT ?, ?, ?
WHERE NOT EXISTS (
SELECT 1 FROM mission_categories WHERE code_type = ?
)`,
categories[i].id,
categories[i].key,
categories[i].name,
categories[i].key,
).Error; err != nil {
return fmt.Errorf("seed mission category %q: %w", categories[i].key, err)
}
}
var hems struct {
ID []byte `gorm:"column:id"`
}
if err := db.Table("mission_categories").
Select("id").
Where("code_type = ?", "HEMS").
Take(&hems).Error; err != nil {
return err
}
subtypes := []struct {
code string
name string
taskName string
id []byte
}{
{code: "PRIM", name: "Primary mission", taskName: "Primary mission task", id: uuidv7.MustBytes()},
{code: "SECONDARY", name: "Secondary mission", taskName: "Secondary mission task", id: uuidv7.MustBytes()},
}
for i := range subtypes {
if err := db.Exec(
`INSERT INTO mission_subcategories (id, mission_category_id, sub_type, name, task_name)
SELECT ?, ?, ?, ?, ?
WHERE NOT EXISTS (
SELECT 1 FROM mission_subcategories WHERE mission_category_id = ? AND sub_type = ?
)`,
subtypes[i].id,
hems.ID,
subtypes[i].code,
subtypes[i].name,
subtypes[i].taskName,
hems.ID,
subtypes[i].code,
).Error; err != nil {
return fmt.Errorf("seed mission subtype %q: %w", subtypes[i].code, err)
}
}
return nil
}