feat(db): surface upstream trips enrichment columns in PostgREST RPCs
Adds migrations 006/007/008 that wrap tracksolid.v_trips_enriched (introduced upstream in tracksolid_timescale_grafana_prod migration 09) and DROP+CREATE the trips_for_day / trips_for_range RPCs to return the new metadata columns: vehicle_plate, start_address, end_address, driving_time_s, idle_time_s, fuel_consumed_l, and daily_seq. Path/timestamps logic is unchanged — position_history is still scanned to produce the per-vertex timestamps_rel that drive deck.gl TripsLayer animation. route_geom can't replace this since it carries no per-vertex time, but the GIST index on it is available for future area filters. Day filter switches from start_time::date to v.trip_date_eat (the Africa/Nairobi local date already computed by v_trips_enriched), which fixes the latent bug where trips spanning midnight UTC could be misbucketed. Frontend Trip type still receives all original fields in compatible positions; the new fields are additive and ignored by the existing TripsLayer code. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
47c86c9d7a
commit
b0db22c669
3 changed files with 207 additions and 0 deletions
41
db/migrations/006_trips_viz_view_v2.sql
Normal file
41
db/migrations/006_trips_viz_view_v2.sql
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
-- 006_trips_viz_view_v2.sql
|
||||||
|
-- Extend trips_viz_v1 with the enrichment columns added in upstream
|
||||||
|
-- migration 09 (tracksolid.trips: route_geom, start_address, end_address,
|
||||||
|
-- vehicle_plate, waypoints_count) plus webhook-supplied trip metrics from
|
||||||
|
-- migration 03 (idle_time_s, driving_time_s, fuel_consumed_l), surfaced via
|
||||||
|
-- tracksolid.v_trips_enriched (which adds trip_date_eat + daily_seq).
|
||||||
|
--
|
||||||
|
-- Columns are appended only — CREATE OR REPLACE VIEW is safe (no column
|
||||||
|
-- removed, no type changed). Existing consumers see the same first ten
|
||||||
|
-- columns in the same positions.
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW public.trips_viz_v1 AS
|
||||||
|
SELECT
|
||||||
|
t.id AS trip_id,
|
||||||
|
t.imei,
|
||||||
|
d.vehicle_name,
|
||||||
|
d.vehicle_number,
|
||||||
|
COALESCE(NULLIF(TRIM(d.cost_centre), ''), 'Unassigned') AS cost_centre,
|
||||||
|
t.start_time,
|
||||||
|
t.end_time,
|
||||||
|
t.distance_km,
|
||||||
|
t.avg_speed_kmh,
|
||||||
|
t.max_speed_kmh,
|
||||||
|
-- ── Enrichment columns (upstream migration 09) ─────────────────────────
|
||||||
|
COALESCE(t.vehicle_plate, d.vehicle_number) AS vehicle_plate,
|
||||||
|
t.start_address,
|
||||||
|
t.end_address,
|
||||||
|
t.waypoints_count,
|
||||||
|
-- ── Webhook-supplied trip metrics (upstream migration 03) ──────────────
|
||||||
|
t.driving_time_s,
|
||||||
|
t.idle_time_s,
|
||||||
|
t.fuel_consumed_l,
|
||||||
|
-- ── Stable per-day trip ordinal (upstream migration 09) ────────────────
|
||||||
|
t.trip_date_eat,
|
||||||
|
t.daily_seq
|
||||||
|
FROM tracksolid.v_trips_enriched t
|
||||||
|
JOIN tracksolid.devices d ON d.imei = t.imei
|
||||||
|
WHERE t.start_time IS NOT NULL
|
||||||
|
AND t.end_time IS NOT NULL;
|
||||||
|
|
||||||
|
GRANT SELECT ON public.trips_viz_v1 TO viz_anon;
|
||||||
82
db/migrations/007_trips_for_day_v2.sql
Normal file
82
db/migrations/007_trips_for_day_v2.sql
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
-- 007_trips_for_day_v2.sql
|
||||||
|
-- Replaces 003_trips_for_day_rpc.sql to surface the enriched trip columns
|
||||||
|
-- exposed by 006_trips_viz_view_v2.sql.
|
||||||
|
--
|
||||||
|
-- Path/timestamps logic is unchanged: position_history is still scanned per
|
||||||
|
-- trip to build the LineString and per-vertex timestamps_rel needed by
|
||||||
|
-- deck.gl TripsLayer animation. (route_geom alone can't replace this — it
|
||||||
|
-- carries no per-vertex time.) Win is metadata: vehicle_plate, addresses,
|
||||||
|
-- daily_seq, drive/idle/fuel — all returned in one round-trip.
|
||||||
|
--
|
||||||
|
-- DROP + CREATE because the return type signature changes; CREATE OR REPLACE
|
||||||
|
-- FUNCTION cannot alter return columns.
|
||||||
|
|
||||||
|
DROP FUNCTION IF EXISTS public.trips_for_day(date, text[]);
|
||||||
|
|
||||||
|
CREATE FUNCTION public.trips_for_day(
|
||||||
|
p_date date,
|
||||||
|
p_cost_centres text[] DEFAULT NULL
|
||||||
|
) RETURNS TABLE (
|
||||||
|
trip_id bigint,
|
||||||
|
imei text,
|
||||||
|
vehicle_name text,
|
||||||
|
vehicle_number text,
|
||||||
|
cost_centre text,
|
||||||
|
start_time timestamptz,
|
||||||
|
end_time timestamptz,
|
||||||
|
distance_km numeric,
|
||||||
|
vehicle_plate text,
|
||||||
|
start_address text,
|
||||||
|
end_address text,
|
||||||
|
driving_time_s integer,
|
||||||
|
idle_time_s integer,
|
||||||
|
fuel_consumed_l numeric,
|
||||||
|
daily_seq bigint,
|
||||||
|
path_geojson json,
|
||||||
|
timestamps_rel int[]
|
||||||
|
)
|
||||||
|
LANGUAGE sql STABLE
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = public, tracksolid
|
||||||
|
AS $$
|
||||||
|
WITH day_trips AS (
|
||||||
|
SELECT v.*
|
||||||
|
FROM public.trips_viz_v1 v
|
||||||
|
WHERE v.trip_date_eat = p_date
|
||||||
|
AND (p_cost_centres IS NULL OR v.cost_centre = ANY(p_cost_centres))
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
dt.trip_id,
|
||||||
|
dt.imei,
|
||||||
|
dt.vehicle_name,
|
||||||
|
dt.vehicle_number,
|
||||||
|
dt.cost_centre,
|
||||||
|
dt.start_time,
|
||||||
|
dt.end_time,
|
||||||
|
dt.distance_km,
|
||||||
|
dt.vehicle_plate,
|
||||||
|
dt.start_address,
|
||||||
|
dt.end_address,
|
||||||
|
dt.driving_time_s,
|
||||||
|
dt.idle_time_s,
|
||||||
|
dt.fuel_consumed_l,
|
||||||
|
dt.daily_seq,
|
||||||
|
ST_AsGeoJSON(ST_MakeLine(ph.geom ORDER BY ph.gps_time))::json AS path_geojson,
|
||||||
|
array_agg(
|
||||||
|
EXTRACT(EPOCH FROM ph.gps_time - dt.start_time)::int
|
||||||
|
ORDER BY ph.gps_time
|
||||||
|
) AS timestamps_rel
|
||||||
|
FROM day_trips dt
|
||||||
|
JOIN tracksolid.position_history ph
|
||||||
|
ON ph.imei = dt.imei
|
||||||
|
AND ph.gps_time BETWEEN dt.start_time AND dt.end_time
|
||||||
|
AND ph.geom IS NOT NULL
|
||||||
|
GROUP BY dt.trip_id, dt.imei, dt.vehicle_name, dt.vehicle_number,
|
||||||
|
dt.cost_centre, dt.start_time, dt.end_time, dt.distance_km,
|
||||||
|
dt.vehicle_plate, dt.start_address, dt.end_address,
|
||||||
|
dt.driving_time_s, dt.idle_time_s, dt.fuel_consumed_l,
|
||||||
|
dt.daily_seq
|
||||||
|
HAVING count(ph.geom) >= 2;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
GRANT EXECUTE ON FUNCTION public.trips_for_day(date, text[]) TO viz_anon;
|
||||||
84
db/migrations/008_trips_for_range_v2.sql
Normal file
84
db/migrations/008_trips_for_range_v2.sql
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
-- 008_trips_for_range_v2.sql
|
||||||
|
-- Multi-day variant of 007_trips_for_day_v2. Same enriched return shape;
|
||||||
|
-- 14-day cap retained.
|
||||||
|
|
||||||
|
DROP FUNCTION IF EXISTS public.trips_for_range(date, date, text[]);
|
||||||
|
|
||||||
|
CREATE FUNCTION public.trips_for_range(
|
||||||
|
p_start date,
|
||||||
|
p_end date,
|
||||||
|
p_cost_centres text[] DEFAULT NULL
|
||||||
|
) RETURNS TABLE (
|
||||||
|
trip_id bigint,
|
||||||
|
imei text,
|
||||||
|
vehicle_name text,
|
||||||
|
vehicle_number text,
|
||||||
|
cost_centre text,
|
||||||
|
start_time timestamptz,
|
||||||
|
end_time timestamptz,
|
||||||
|
distance_km numeric,
|
||||||
|
vehicle_plate text,
|
||||||
|
start_address text,
|
||||||
|
end_address text,
|
||||||
|
driving_time_s integer,
|
||||||
|
idle_time_s integer,
|
||||||
|
fuel_consumed_l numeric,
|
||||||
|
daily_seq bigint,
|
||||||
|
path_geojson json,
|
||||||
|
timestamps_rel int[]
|
||||||
|
)
|
||||||
|
LANGUAGE plpgsql STABLE
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = public, tracksolid
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
IF p_start IS NULL OR p_end IS NULL OR p_end < p_start THEN
|
||||||
|
RAISE EXCEPTION 'Invalid date range: p_start=%, p_end=%', p_start, p_end;
|
||||||
|
END IF;
|
||||||
|
IF (p_end - p_start) > 13 THEN
|
||||||
|
RAISE EXCEPTION 'Range too large: max 14 days, got %', (p_end - p_start + 1);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN QUERY
|
||||||
|
WITH range_trips AS (
|
||||||
|
SELECT v.*
|
||||||
|
FROM public.trips_viz_v1 v
|
||||||
|
WHERE v.trip_date_eat BETWEEN p_start AND p_end
|
||||||
|
AND (p_cost_centres IS NULL OR v.cost_centre = ANY(p_cost_centres))
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
rt.trip_id,
|
||||||
|
rt.imei,
|
||||||
|
rt.vehicle_name,
|
||||||
|
rt.vehicle_number,
|
||||||
|
rt.cost_centre,
|
||||||
|
rt.start_time,
|
||||||
|
rt.end_time,
|
||||||
|
rt.distance_km,
|
||||||
|
rt.vehicle_plate,
|
||||||
|
rt.start_address,
|
||||||
|
rt.end_address,
|
||||||
|
rt.driving_time_s,
|
||||||
|
rt.idle_time_s,
|
||||||
|
rt.fuel_consumed_l,
|
||||||
|
rt.daily_seq,
|
||||||
|
ST_AsGeoJSON(ST_MakeLine(ph.geom ORDER BY ph.gps_time))::json AS path_geojson,
|
||||||
|
array_agg(
|
||||||
|
EXTRACT(EPOCH FROM ph.gps_time - rt.start_time)::int
|
||||||
|
ORDER BY ph.gps_time
|
||||||
|
) AS timestamps_rel
|
||||||
|
FROM range_trips rt
|
||||||
|
JOIN tracksolid.position_history ph
|
||||||
|
ON ph.imei = rt.imei
|
||||||
|
AND ph.gps_time BETWEEN rt.start_time AND rt.end_time
|
||||||
|
AND ph.geom IS NOT NULL
|
||||||
|
GROUP BY rt.trip_id, rt.imei, rt.vehicle_name, rt.vehicle_number,
|
||||||
|
rt.cost_centre, rt.start_time, rt.end_time, rt.distance_km,
|
||||||
|
rt.vehicle_plate, rt.start_address, rt.end_address,
|
||||||
|
rt.driving_time_s, rt.idle_time_s, rt.fuel_consumed_l,
|
||||||
|
rt.daily_seq
|
||||||
|
HAVING count(ph.geom) >= 2;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
GRANT EXECUTE ON FUNCTION public.trips_for_range(date, date, text[]) TO viz_anon;
|
||||||
Loading…
Reference in a new issue