turash/bugulma/backend/internal/domain/migrations.go
2025-12-15 10:06:41 +01:00

466 lines
13 KiB
Go

package domain
import (
"fmt"
"gorm.io/gorm"
)
// AutoMigrate runs all migrations for the database schema
func AutoMigrate(db *gorm.DB) error {
// Run AutoMigrate first to create tables
if err := db.AutoMigrate(
&Address{},
&Organization{},
&PublicTransportStop{},
&PublicTransportRoute{},
&Trip{},
&StopTime{},
&Frequency{},
&ServiceCalendar{},
&CalendarDate{},
&Site{},
&SiteOperatingBusiness{},
&ResourceFlow{},
&ResourceFlowVersion{},
&SharedAsset{},
&Match{},
&NegotiationHistoryEntry{},
&User{},
&Localization{},
&Product{},
&Service{},
&ServiceNeed{},
&TimelineItem{}, // Add timeline items table
&HeritageTitle{}, // Add heritage title table
&HeritageSource{}, // Add heritage sources table
); err != nil {
return err
}
// Fix JSONB column issues after tables are created
if err := FixJSONBColumns(db); err != nil {
return fmt.Errorf("failed to fix JSONB columns: %v", err)
}
return nil
}
// RunPostGISMigrations runs PostGIS-specific migrations only on PostgreSQL databases
func RunPostGISMigrations(db *gorm.DB) error {
// Only run PostGIS migrations for PostgreSQL databases
if db.Dialector.Name() != "postgres" {
return nil
}
// First, check if PostGIS extension is enabled
var postgisEnabled bool
if err := db.Raw("SELECT EXISTS(SELECT 1 FROM pg_extension WHERE extname = 'postgis')").Scan(&postgisEnabled).Error; err != nil {
return fmt.Errorf("failed to check PostGIS extension: %v", err)
}
if !postgisEnabled {
// PostGIS is not enabled - this is not an error in test environments
// Just skip PostGIS migrations gracefully
fmt.Printf("PostGIS not enabled, skipping PostGIS migrations\n")
return nil
}
// Verify PostGIS functions are available
var postgisVersion string
if err := db.Raw("SELECT PostGIS_Version()").Scan(&postgisVersion).Error; err != nil {
return fmt.Errorf("PostGIS not available: %v", err)
}
// Handle geometry column setup - ensure it exists with correct type
// First, check if column exists and what type it is
var columnExists bool
var dataType, udtName string
if err := db.Raw(`
SELECT EXISTS(
SELECT 1 FROM information_schema.columns
WHERE table_name = 'sites' AND column_name = 'location_geometry'
)
`).Scan(&columnExists).Error; err != nil {
return fmt.Errorf("failed to check geometry column: %v", err)
}
if columnExists {
// Get detailed column information
type ColumnInfo struct {
DataType string
UdtName string
}
var info ColumnInfo
if err := db.Raw(`
SELECT
c.data_type,
COALESCE(c.udt_name, '') as udt_name
FROM information_schema.columns c
WHERE c.table_name = 'sites' AND c.column_name = 'location_geometry'
`).Scan(&info).Error; err != nil {
return fmt.Errorf("failed to check column type: %v", err)
}
dataType = info.DataType
udtName = info.UdtName
// If column exists but is wrong type (not geometry), we need to handle it carefully
if dataType != "USER-DEFINED" || udtName != "geometry" {
// Drop any existing index first
if err := db.Exec("DROP INDEX IF EXISTS idx_site_geometry").Error; err != nil {
return fmt.Errorf("failed to drop existing geometry index: %v", err)
}
// Drop the column
if err := db.Exec("ALTER TABLE sites DROP COLUMN location_geometry").Error; err != nil {
return fmt.Errorf("failed to drop incorrect geometry column: %v", err)
}
columnExists = false
}
}
// Create column if it doesn't exist or was dropped
if !columnExists {
if err := db.Exec("ALTER TABLE sites ADD COLUMN location_geometry geometry(Point, 4326)").Error; err != nil {
return fmt.Errorf("failed to create geometry column: %v", err)
}
}
// Create spatial index for efficient queries
if err := db.Exec(`
CREATE INDEX IF NOT EXISTS idx_site_geometry ON sites USING GIST (location_geometry);
`).Error; err != nil {
return fmt.Errorf("failed to create spatial index: %v", err)
}
// Update existing records to populate geometry from lat/lng
// Using ST_SetSRID and ST_MakePoint (2025 best practice) instead of ST_GeogFromText
// This is more efficient and ensures proper SRID handling
if err := db.Exec(`
UPDATE sites
SET location_geometry = ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)
WHERE latitude IS NOT NULL
AND longitude IS NOT NULL
AND (location_geometry IS NULL OR NOT ST_IsValid(location_geometry));
`).Error; err != nil {
return fmt.Errorf("failed to populate geometry data: %v", err)
}
// Drop existing constraint if it exists and recreate
if err := db.Exec(`
ALTER TABLE sites DROP CONSTRAINT IF EXISTS check_location_geometry;
ALTER TABLE sites ADD CONSTRAINT check_location_geometry
CHECK (location_geometry IS NULL OR ST_IsValid(location_geometry));
`).Error; err != nil {
return fmt.Errorf("failed to add geometry constraint: %v", err)
}
// Add comment for documentation
if err := db.Exec(`
COMMENT ON COLUMN sites.location_geometry IS 'PostGIS geometry field for spatial operations (SRID 4326 = WGS84)';
`).Error; err != nil {
return fmt.Errorf("failed to add column comment: %v", err)
}
return nil
}
// cleanupGeometryColumns removes geometry-related columns and indexes when PostGIS is not available
func cleanupGeometryColumns(db *gorm.DB) error {
// Drop geometry column if it exists
if err := db.Exec("ALTER TABLE sites DROP COLUMN IF EXISTS location_geometry").Error; err != nil {
return fmt.Errorf("failed to drop geometry column: %v", err)
}
// Drop spatial indexes
if err := db.Exec("DROP INDEX IF EXISTS idx_site_geometry").Error; err != nil {
return fmt.Errorf("failed to drop geometry index: %v", err)
}
// Drop constraints
if err := db.Exec("ALTER TABLE sites DROP CONSTRAINT IF EXISTS check_location_geometry").Error; err != nil {
return fmt.Errorf("failed to drop geometry constraint: %v", err)
}
return nil
}
// FixJSONBColumns fixes JSONB column conversion issues that GORM can't handle automatically
func FixJSONBColumns(db *gorm.DB) error {
// Fix certifications column - remove default, convert to JSONB, add default back
if err := db.Exec(`
ALTER TABLE organizations ALTER COLUMN certifications DROP DEFAULT;
`).Error; err != nil {
return fmt.Errorf("failed to drop certifications default: %v", err)
}
if err := db.Exec(`
ALTER TABLE organizations ALTER COLUMN certifications TYPE JSONB USING certifications::JSONB;
`).Error; err != nil {
return fmt.Errorf("failed to convert certifications to JSONB: %v", err)
}
if err := db.Exec(`
ALTER TABLE organizations ALTER COLUMN certifications SET DEFAULT '[]'::jsonb;
`).Error; err != nil {
return fmt.Errorf("failed to set certifications default: %v", err)
}
// Fix other JSONB columns similarly
jsonbColumns := []string{
"business_focus",
"technical_expertise",
"available_technology",
"management_systems",
"trust_network",
"existing_symbiotic_relationships",
"gallery_images",
}
for _, col := range jsonbColumns {
if err := db.Exec(fmt.Sprintf(`
ALTER TABLE organizations ALTER COLUMN %s DROP DEFAULT;
`, col)).Error; err != nil {
return fmt.Errorf("failed to drop %s default: %v", col, err)
}
if err := db.Exec(fmt.Sprintf(`
ALTER TABLE organizations ALTER COLUMN %s TYPE JSONB USING %s::JSONB;
`, col, col)).Error; err != nil {
return fmt.Errorf("failed to convert %s to JSONB: %v", col, err)
}
if err := db.Exec(fmt.Sprintf(`
ALTER TABLE organizations ALTER COLUMN %s SET DEFAULT '[]'::jsonb;
`, col)).Error; err != nil {
return fmt.Errorf("failed to set %s default: %v", col, err)
}
}
return nil
}
// RunSearchMigrations enables pg_trgm extension and creates search indexes for PostgreSQL
func RunSearchMigrations(db *gorm.DB) error {
// Only run search migrations for PostgreSQL databases
if db.Dialector.Name() != "postgres" {
return nil
}
// Enable pg_trgm extension for fuzzy search
if err := db.Exec("CREATE EXTENSION IF NOT EXISTS pg_trgm").Error; err != nil {
return fmt.Errorf("failed to enable pg_trgm extension: %v", err)
}
// Create trigram indexes for fuzzy search on organizations
// These indexes significantly speed up similarity searches
if err := db.Exec(`
CREATE INDEX IF NOT EXISTS idx_org_name_trgm
ON organizations USING GIN (name gin_trgm_ops);
`).Error; err != nil {
return fmt.Errorf("failed to create name trigram index: %v", err)
}
if err := db.Exec(`
CREATE INDEX IF NOT EXISTS idx_org_description_trgm
ON organizations USING GIN (description gin_trgm_ops);
`).Error; err != nil {
return fmt.Errorf("failed to create description trigram index: %v", err)
}
if err := db.Exec(`
CREATE INDEX IF NOT EXISTS idx_org_sector_trgm
ON organizations USING GIN (sector gin_trgm_ops);
`).Error; err != nil {
return fmt.Errorf("failed to create sector trigram index: %v", err)
}
return nil
}
// CreateIndexes creates additional indexes that GORM doesn't handle automatically
func CreateIndexes(db *gorm.DB) error {
// Composite indexes for organizations
if err := db.Exec(`
CREATE INDEX IF NOT EXISTS idx_org_subtype_sector
ON organizations(subtype, sector);
`).Error; err != nil {
return err
}
if err := db.Exec(`
CREATE INDEX IF NOT EXISTS idx_org_verified_subtype
ON organizations(verified, subtype);
`).Error; err != nil {
return err
}
// GIN indexes for JSONB fields
if err := db.Exec(`
CREATE INDEX IF NOT EXISTS idx_org_products
ON organizations USING GIN (products);
`).Error; err != nil {
return err
}
if err := db.Exec(`
CREATE INDEX IF NOT EXISTS idx_org_certifications
ON organizations USING GIN (certifications);
`).Error; err != nil {
return err
}
// Indexes for products table
if err := db.Exec(`
CREATE INDEX IF NOT EXISTS idx_products_category
ON products(category);
`).Error; err != nil {
return err
}
if err := db.Exec(`
CREATE INDEX IF NOT EXISTS idx_products_organization
ON products(organization_id);
`).Error; err != nil {
return err
}
if err := db.Exec(`
CREATE INDEX IF NOT EXISTS idx_products_price
ON products(unit_price);
`).Error; err != nil {
return err
}
// Indexes for services table
if err := db.Exec(`
CREATE INDEX IF NOT EXISTS idx_services_type
ON services(type);
`).Error; err != nil {
return err
}
if err := db.Exec(`
CREATE INDEX IF NOT EXISTS idx_services_domain
ON services(domain);
`).Error; err != nil {
return err
}
if err := db.Exec(`
CREATE INDEX IF NOT EXISTS idx_services_organization
ON services(organization_id);
`).Error; err != nil {
return err
}
// Indexes for service_needs table
if err := db.Exec(`
CREATE INDEX IF NOT EXISTS idx_service_needs_type
ON service_needs(type);
`).Error; err != nil {
return err
}
if err := db.Exec(`
CREATE INDEX IF NOT EXISTS idx_service_needs_organization
ON service_needs(organization_id);
`).Error; err != nil {
return err
}
if err := db.Exec(`
CREATE INDEX IF NOT EXISTS idx_service_needs_urgency
ON service_needs(urgency);
`).Error; err != nil {
return err
}
if err := db.Exec(`
CREATE INDEX IF NOT EXISTS idx_site_utilities
ON sites USING GIN (available_utilities);
`).Error; err != nil {
return err
}
if err := db.Exec(`
CREATE INDEX IF NOT EXISTS idx_site_waste_mgmt
ON sites USING GIN (waste_management);
`).Error; err != nil {
return err
}
// Partial indexes for resource flows
if err := db.Exec(`
CREATE INDEX IF NOT EXISTS idx_rf_output_heat
ON resource_flows (site_id, (quality->>'temperature_celsius')::numeric)
WHERE direction = 'output' AND type = 'heat';
`).Error; err != nil {
return err
}
if err := db.Exec(`
CREATE INDEX IF NOT EXISTS idx_rf_input_heat
ON resource_flows (site_id, (quality->>'temperature_celsius')::numeric)
WHERE direction = 'input' AND type = 'heat';
`).Error; err != nil {
return err
}
// Composite index for match queries
if err := db.Exec(`
CREATE INDEX IF NOT EXISTS idx_match_status_score
ON matches(status, compatibility_score DESC);
`).Error; err != nil {
return err
}
if err := db.Exec(`
CREATE INDEX IF NOT EXISTS idx_match_economic_value
ON matches(economic_value DESC, status);
`).Error; err != nil {
return err
}
return nil
}
// CreateConstraints creates additional constraints
func CreateConstraints(db *gorm.DB) error {
// Unique constraint on resource flow version
if err := db.Exec(`
ALTER TABLE resource_flow_versions
ADD CONSTRAINT IF NOT EXISTS unique_flow_version
UNIQUE (resource_flow_id, version_number);
`).Error; err != nil {
return err
}
// Check constraint for match priority
if err := db.Exec(`
ALTER TABLE matches
ADD CONSTRAINT IF NOT EXISTS check_match_priority
CHECK (priority BETWEEN 1 AND 10);
`).Error; err != nil {
return err
}
// Check constraint for organization readiness maturity
if err := db.Exec(`
ALTER TABLE organizations
ADD CONSTRAINT IF NOT EXISTS check_readiness_maturity
CHECK (readiness_maturity BETWEEN 1 AND 5);
`).Error; err != nil {
return err
}
// Check constraint for shared asset utilization
if err := db.Exec(`
ALTER TABLE shared_assets
ADD CONSTRAINT IF NOT EXISTS check_utilization_rate
CHECK (utilization_rate BETWEEN 0 AND 1);
`).Error; err != nil {
return err
}
return nil
}