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{}, &Site{}, &SiteOperatingBusiness{}, &ResourceFlow{}, &ResourceFlowVersion{}, &SharedAsset{}, &Match{}, &NegotiationHistoryEntry{}, &User{}, &Localization{}, &Product{}, &Service{}, &ServiceNeed{}, ); 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 { return fmt.Errorf("PostGIS extension is not enabled in this database") } // 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 } // 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 }