362 lines
16 KiB
PL/PgSQL
362 lines
16 KiB
PL/PgSQL
-- Fix existing SIBU transportation schema to match required structure
|
||
-- This migration updates the existing schema rather than recreating it
|
||
|
||
-- 1. First, let's add missing columns to routes table if they don't exist
|
||
DO $$
|
||
BEGIN
|
||
-- Add missing columns to routes table
|
||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'routes' AND column_name = 'description') THEN
|
||
ALTER TABLE public.routes ADD COLUMN description TEXT;
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'routes' AND column_name = 'origin_city') THEN
|
||
ALTER TABLE public.routes ADD COLUMN origin_city TEXT;
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'routes' AND column_name = 'destination_city') THEN
|
||
ALTER TABLE public.routes ADD COLUMN destination_city TEXT;
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'routes' AND column_name = 'distance_km') THEN
|
||
ALTER TABLE public.routes ADD COLUMN distance_km DECIMAL(6,2);
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'routes' AND column_name = 'estimated_duration_minutes') THEN
|
||
ALTER TABLE public.routes ADD COLUMN estimated_duration_minutes INTEGER;
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'routes' AND column_name = 'status') THEN
|
||
-- Create enum type if it doesn't exist
|
||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'route_status') THEN
|
||
CREATE TYPE public.route_status AS ENUM ('active', 'inactive', 'maintenance');
|
||
END IF;
|
||
ALTER TABLE public.routes ADD COLUMN status public.route_status DEFAULT 'active'::public.route_status;
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'routes' AND column_name = 'created_at') THEN
|
||
ALTER TABLE public.routes ADD COLUMN created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP;
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'routes' AND column_name = 'updated_at') THEN
|
||
ALTER TABLE public.routes ADD COLUMN updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP;
|
||
END IF;
|
||
END $$;
|
||
|
||
-- 2. Add missing columns to stops table
|
||
DO $$
|
||
BEGIN
|
||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'stops' AND column_name = 'city') THEN
|
||
ALTER TABLE public.stops ADD COLUMN city TEXT;
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'stops' AND column_name = 'address') THEN
|
||
ALTER TABLE public.stops ADD COLUMN address TEXT;
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'stops' AND column_name = 'stop_type') THEN
|
||
-- Create enum type if it doesn't exist
|
||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'stop_type') THEN
|
||
CREATE TYPE public.stop_type AS ENUM ('terminal', 'regular', 'express_only');
|
||
END IF;
|
||
ALTER TABLE public.stops ADD COLUMN stop_type public.stop_type DEFAULT 'regular'::public.stop_type;
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'stops' AND column_name = 'has_shelter') THEN
|
||
ALTER TABLE public.stops ADD COLUMN has_shelter BOOLEAN DEFAULT false;
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'stops' AND column_name = 'has_seating') THEN
|
||
ALTER TABLE public.stops ADD COLUMN has_seating BOOLEAN DEFAULT false;
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'stops' AND column_name = 'is_accessible') THEN
|
||
ALTER TABLE public.stops ADD COLUMN is_accessible BOOLEAN DEFAULT false;
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'stops' AND column_name = 'created_at') THEN
|
||
ALTER TABLE public.stops ADD COLUMN created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP;
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'stops' AND column_name = 'updated_at') THEN
|
||
ALTER TABLE public.stops ADD COLUMN updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP;
|
||
END IF;
|
||
END $$;
|
||
|
||
-- 3. Add missing columns to route_stops table
|
||
DO $$
|
||
BEGIN
|
||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'route_stops' AND column_name = 'id') THEN
|
||
ALTER TABLE public.route_stops ADD COLUMN id UUID DEFAULT gen_random_uuid();
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'route_stops' AND column_name = 'stop_order') THEN
|
||
ALTER TABLE public.route_stops ADD COLUMN stop_order INTEGER;
|
||
-- Update stop_order from existing seq column
|
||
UPDATE public.route_stops SET stop_order = seq WHERE stop_order IS NULL;
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'route_stops' AND column_name = 'travel_time_minutes') THEN
|
||
ALTER TABLE public.route_stops ADD COLUMN travel_time_minutes INTEGER;
|
||
-- Calculate travel time from dwell_sec (convert seconds to minutes)
|
||
UPDATE public.route_stops SET travel_time_minutes = COALESCE(dwell_sec / 60, 0) WHERE travel_time_minutes IS NULL;
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'route_stops' AND column_name = 'is_pickup_point') THEN
|
||
ALTER TABLE public.route_stops ADD COLUMN is_pickup_point BOOLEAN DEFAULT true;
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'route_stops' AND column_name = 'is_dropoff_point') THEN
|
||
ALTER TABLE public.route_stops ADD COLUMN is_dropoff_point BOOLEAN DEFAULT true;
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'route_stops' AND column_name = 'created_at') THEN
|
||
ALTER TABLE public.route_stops ADD COLUMN created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP;
|
||
END IF;
|
||
END $$;
|
||
|
||
-- 4. Create bus_schedules table based on existing timetable
|
||
CREATE TABLE IF NOT EXISTS public.bus_schedules (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
route_id TEXT REFERENCES public.routes(id) ON DELETE CASCADE,
|
||
departure_time TIME NOT NULL,
|
||
frequency_minutes INTEGER DEFAULT 30,
|
||
schedule_type TEXT DEFAULT 'weekday',
|
||
is_active BOOLEAN DEFAULT true,
|
||
notes TEXT,
|
||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
-- 5. Migrate data from timetable to bus_schedules
|
||
INSERT INTO public.bus_schedules (route_id, departure_time, frequency_minutes, schedule_type, is_active)
|
||
SELECT
|
||
route_id,
|
||
departure_time,
|
||
30 as frequency_minutes,
|
||
'weekday' as schedule_type,
|
||
true as is_active
|
||
FROM public.timetable
|
||
WHERE NOT EXISTS (
|
||
SELECT 1 FROM public.bus_schedules bs
|
||
WHERE bs.route_id = timetable.route_id
|
||
AND bs.departure_time = timetable.departure_time
|
||
);
|
||
|
||
-- 6. Update existing route data with proper values
|
||
UPDATE public.routes SET
|
||
description = CASE
|
||
WHEN name = 'Boquete – David' THEN 'Ruta desde Boquete hacia David con paradas principales'
|
||
WHEN name = 'David – Boquete' THEN 'Ruta desde David hacia Boquete con paradas principales'
|
||
ELSE 'Ruta de transporte público'
|
||
END,
|
||
origin_city = CASE
|
||
WHEN direction = 'outbound' AND name LIKE 'Boquete%' THEN 'Boquete'
|
||
WHEN direction = 'inbound' AND name LIKE 'David%' THEN 'David'
|
||
ELSE SPLIT_PART(name, ' – ', 1)
|
||
END,
|
||
destination_city = CASE
|
||
WHEN direction = 'outbound' AND name LIKE '%David' THEN 'David'
|
||
WHEN direction = 'inbound' AND name LIKE '%Boquete' THEN 'Boquete'
|
||
ELSE SPLIT_PART(name, ' – ', 2)
|
||
END,
|
||
distance_km = 38.5,
|
||
estimated_duration_minutes = 45,
|
||
status = 'active'::public.route_status
|
||
WHERE description IS NULL;
|
||
|
||
-- 7. Add additional routes for Panama SIBU system
|
||
INSERT INTO public.routes (id, name, description, color, direction, origin_city, destination_city, distance_km, estimated_duration_minutes, status) VALUES
|
||
('palmira-david-out', 'Palmira>David', 'Ruta desde Palmira hacia David', '#FEE715', 'outbound', 'Palmira', 'David', 25.2, 35, 'active'),
|
||
('david-palmira-in', 'David>Palmira', 'Ruta desde David hacia Palmira', '#FEE715', 'inbound', 'David', 'Palmira', 25.2, 35, 'active'),
|
||
('caldera-david-out', 'Caldera>David', 'Ruta desde Caldera hacia David', '#FEE715', 'outbound', 'Caldera', 'David', 42.8, 50, 'active'),
|
||
('david-caldera-in', 'David>Caldera', 'Ruta desde David hacia Caldera', '#FEE715', 'inbound', 'David', 'Caldera', 42.8, 50, 'active')
|
||
ON CONFLICT (id) DO NOTHING;
|
||
|
||
-- 8. Add missing bus stops for complete Panama routes
|
||
INSERT INTO public.stops (name, lat, lng, city, address, stop_type, has_shelter, has_seating) VALUES
|
||
-- Palmira stops
|
||
('Centro de Palmira', 8.3544, -82.3611, 'Palmira', 'Plaza Central', 'regular', true, true),
|
||
('Escuela de Palmira', 8.3567, -82.3598, 'Palmira', 'Zona Escolar', 'regular', false, true),
|
||
|
||
-- Caldera stops
|
||
('Puerto de Caldera', 8.2456, -81.7234, 'Caldera', 'Zona Portuaria', 'terminal', true, true),
|
||
('Centro de Caldera', 8.2478, -81.7198, 'Caldera', 'Centro del Pueblo', 'regular', true, false),
|
||
|
||
-- Additional David stops
|
||
('Terminal de David', 8.4177, -82.4270, 'David', 'Terminal de Transporte', 'terminal', true, true),
|
||
('Centro de David', 8.4194, -82.4255, 'David', 'Parque Cervantes', 'regular', true, true),
|
||
('Hospital Chiriquí', 8.4156, -82.4289, 'David', 'Complejo Hospitalario', 'regular', true, true),
|
||
('Chiriquí Mall', 8.4089, -82.4178, 'David', 'Centro Comercial', 'regular', true, true),
|
||
|
||
-- Additional Boquete stops
|
||
('Terminal de Boquete', 8.7697, -82.4328, 'Boquete', 'Centro de Boquete', 'terminal', true, true),
|
||
('Centro de Boquete', 8.7720, -82.4315, 'Boquete', 'Calle Central', 'regular', true, true),
|
||
('Parque Central Boquete', 8.7705, -82.4340, 'Boquete', 'Junto al Parque Central', 'regular', false, true),
|
||
('Hospital de Boquete', 8.7680, -82.4350, 'Boquete', 'Hospital Regional', 'regular', true, true),
|
||
('Escuela Primaria Boquete', 8.7740, -82.4300, 'Boquete', 'Zona Educativa', 'regular', false, false),
|
||
('Mercado Municipal Boquete', 8.7715, -82.4365, 'Boquete', 'Mercado Central', 'regular', true, true)
|
||
ON CONFLICT (name, lat, lng) DO NOTHING;
|
||
|
||
-- 9. Helper Functions
|
||
CREATE OR REPLACE FUNCTION public.calculate_next_bus_arrival(
|
||
p_route_id TEXT,
|
||
p_stop_id UUID,
|
||
p_current_time TIME DEFAULT CURRENT_TIME
|
||
)
|
||
RETURNS TABLE(
|
||
next_departure TIME,
|
||
estimated_arrival_time TIMESTAMPTZ,
|
||
minutes_until_arrival INTEGER
|
||
)
|
||
LANGUAGE plpgsql
|
||
STABLE
|
||
SECURITY DEFINER
|
||
AS $$
|
||
DECLARE
|
||
stop_position INTEGER;
|
||
time_to_stop INTEGER;
|
||
next_schedule_time TIME;
|
||
BEGIN
|
||
-- Get the next scheduled departure
|
||
SELECT bs.departure_time INTO next_schedule_time
|
||
FROM public.bus_schedules bs
|
||
WHERE bs.route_id = p_route_id
|
||
AND bs.is_active = true
|
||
AND bs.departure_time > p_current_time
|
||
ORDER BY bs.departure_time ASC
|
||
LIMIT 1;
|
||
|
||
-- If no more schedules today, get first schedule tomorrow
|
||
IF next_schedule_time IS NULL THEN
|
||
SELECT bs.departure_time INTO next_schedule_time
|
||
FROM public.bus_schedules bs
|
||
WHERE bs.route_id = p_route_id
|
||
AND bs.is_active = true
|
||
ORDER BY bs.departure_time ASC
|
||
LIMIT 1;
|
||
END IF;
|
||
|
||
-- Get stop position in route and calculate travel time
|
||
SELECT rs.stop_order INTO stop_position
|
||
FROM public.route_stops rs
|
||
WHERE rs.route_id = p_route_id AND rs.stop_id = p_stop_id;
|
||
|
||
-- Calculate estimated travel time to this stop (simplified calculation)
|
||
time_to_stop := COALESCE((stop_position - 1) * 2 + (stop_position * 3), 5); -- ~3 min between stops
|
||
|
||
RETURN QUERY SELECT
|
||
next_schedule_time,
|
||
(CURRENT_DATE + next_schedule_time + (time_to_stop || ' minutes')::INTERVAL)::TIMESTAMPTZ,
|
||
EXTRACT(EPOCH FROM (
|
||
(CURRENT_DATE + next_schedule_time + (time_to_stop || ' minutes')::INTERVAL) - NOW()
|
||
))::INTEGER / 60;
|
||
END;
|
||
$$;
|
||
|
||
CREATE OR REPLACE FUNCTION public.get_route_stops_ordered(p_route_id TEXT)
|
||
RETURNS TABLE(
|
||
stop_id UUID,
|
||
stop_name TEXT,
|
||
latitude DOUBLE PRECISION,
|
||
longitude DOUBLE PRECISION,
|
||
stop_order INTEGER,
|
||
travel_time_minutes INTEGER
|
||
)
|
||
LANGUAGE sql
|
||
STABLE
|
||
SECURITY DEFINER
|
||
AS $$
|
||
SELECT
|
||
s.id,
|
||
s.name,
|
||
s.lat,
|
||
s.lng,
|
||
rs.stop_order,
|
||
rs.travel_time_minutes
|
||
FROM public.route_stops rs
|
||
JOIN public.stops s ON rs.stop_id = s.id
|
||
WHERE rs.route_id = p_route_id
|
||
ORDER BY rs.stop_order;
|
||
$$;
|
||
|
||
-- 10. Enable RLS if not already enabled
|
||
DO $$
|
||
BEGIN
|
||
IF NOT EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'routes' AND rowsecurity = true) THEN
|
||
ALTER TABLE public.routes ENABLE ROW LEVEL SECURITY;
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'stops' AND rowsecurity = true) THEN
|
||
ALTER TABLE public.stops ENABLE ROW LEVEL SECURITY;
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'route_stops' AND rowsecurity = true) THEN
|
||
ALTER TABLE public.route_stops ENABLE ROW LEVEL SECURITY;
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'bus_schedules' AND rowsecurity = true) THEN
|
||
ALTER TABLE public.bus_schedules ENABLE ROW LEVEL SECURITY;
|
||
END IF;
|
||
END $$;
|
||
|
||
-- 11. Create RLS Policies (only if they don't exist)
|
||
DO $$
|
||
BEGIN
|
||
-- Routes policies
|
||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'routes' AND policyname = 'public_can_read_routes') THEN
|
||
CREATE POLICY "public_can_read_routes" ON public.routes FOR SELECT TO public USING (true);
|
||
END IF;
|
||
|
||
-- Stops policies
|
||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'stops' AND policyname = 'public_can_read_stops') THEN
|
||
CREATE POLICY "public_can_read_stops" ON public.stops FOR SELECT TO public USING (true);
|
||
END IF;
|
||
|
||
-- Route stops policies
|
||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'route_stops' AND policyname = 'public_can_read_route_stops') THEN
|
||
CREATE POLICY "public_can_read_route_stops" ON public.route_stops FOR SELECT TO public USING (true);
|
||
END IF;
|
||
|
||
-- Bus schedules policies
|
||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'bus_schedules' AND policyname = 'public_can_read_bus_schedules') THEN
|
||
CREATE POLICY "public_can_read_bus_schedules" ON public.bus_schedules FOR SELECT TO public USING (true);
|
||
END IF;
|
||
END $$;
|
||
|
||
-- 12. Create updated_at triggers
|
||
CREATE OR REPLACE FUNCTION public.update_updated_at_column()
|
||
RETURNS TRIGGER
|
||
LANGUAGE plpgsql
|
||
AS $$
|
||
BEGIN
|
||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||
RETURN NEW;
|
||
END;
|
||
$$;
|
||
|
||
DO $$
|
||
BEGIN
|
||
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'update_routes_updated_at') THEN
|
||
CREATE TRIGGER update_routes_updated_at
|
||
BEFORE UPDATE ON public.routes
|
||
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'update_stops_updated_at') THEN
|
||
CREATE TRIGGER update_stops_updated_at
|
||
BEFORE UPDATE ON public.stops
|
||
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
|
||
END IF;
|
||
END $$;
|
||
|
||
-- 13. Create missing indexes for performance
|
||
CREATE INDEX IF NOT EXISTS idx_routes_origin_destination ON public.routes(origin_city, destination_city);
|
||
CREATE INDEX IF NOT EXISTS idx_routes_status ON public.routes(status);
|
||
CREATE INDEX IF NOT EXISTS idx_stops_city ON public.stops(city);
|
||
CREATE INDEX IF NOT EXISTS idx_bus_schedules_route_id ON public.bus_schedules(route_id);
|
||
CREATE INDEX IF NOT EXISTS idx_bus_schedules_departure_time ON public.bus_schedules(departure_time);
|
||
|
||
-- 14. Final success message (wrapped in DO block to avoid syntax error)
|
||
DO $$
|
||
BEGIN
|
||
RAISE NOTICE 'SIBU Transportation System schema updated successfully!';
|
||
END $$; |