mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
459 lines
13 KiB
Go
459 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{},
|
|
&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
|
|
}
|