-- ============================================================================= -- Fireside Communications — Tracksolid Pro Fleet Telemetry -- PostgreSQL 16 + PostGIS 3 + TimescaleDB 2.15 -- Complete Database Bootstrap Script (Revised & Unified) -- ============================================================================= -- Author: DevOps / Telematics Specialist -- Version: 2.0 (DWH + TimescaleDB Integrated) -- ============================================================================= -- ============================================================================= -- STEP 1 — DATABASE, EXTENSIONS, ROLES -- ============================================================================= -- Database creation (Run manually if needed, or logical script starts here) -- CREATE DATABASE tracksolid_db ENCODING = 'UTF8'; -- \connect tracksolid_db -- ── Extensions ──────────────────────────────────────────────────────────────── CREATE EXTENSION IF NOT EXISTS postgis CASCADE; CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE; CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE EXTENSION IF NOT EXISTS btree_gist; -- ── Roles ───────────────────────────────────────────────────────────────────── DO $$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'tracksolid_owner') THEN CREATE ROLE tracksolid_owner WITH LOGIN PASSWORD 'SET_PASSWORD_IN_ENV'; END IF; IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'grafana_ro') THEN CREATE ROLE grafana_ro WITH LOGIN PASSWORD 'SET_PASSWORD_IN_ENV'; END IF; END$$; GRANT CONNECT ON DATABASE tracksolid_db TO tracksolid_owner; GRANT CONNECT ON DATABASE tracksolid_db TO grafana_ro; -- ============================================================================= -- STEP 2 — SCHEMAS -- ============================================================================= CREATE SCHEMA IF NOT EXISTS tracksolid AUTHORIZATION tracksolid_owner; CREATE SCHEMA IF NOT EXISTS infrastructure AUTHORIZATION tracksolid_owner; CREATE SCHEMA IF NOT EXISTS dwh_gold AUTHORIZATION tracksolid_owner; ALTER DATABASE tracksolid_db SET search_path TO tracksolid, infrastructure, dwh_gold, public; -- ============================================================================= -- STEP 3 — OPERATIONAL TABLES (tracksolid) -- ============================================================================= -- 3.01 Master Device Registry CREATE TABLE IF NOT EXISTS tracksolid.devices ( imei TEXT PRIMARY KEY, device_name TEXT, mc_type TEXT, mc_type_use_scope TEXT, vehicle_name TEXT, vehicle_number TEXT, vehicle_models TEXT, vehicle_icon TEXT, vin TEXT, engine_number TEXT, vehicle_brand TEXT, fuel_100km NUMERIC(6,2), driver_name TEXT, driver_phone TEXT, sim TEXT, iccid TEXT, imsi TEXT, account TEXT, customer_name TEXT, device_group_id TEXT, device_group TEXT, activation_time TIMESTAMPTZ, expiration TIMESTAMPTZ, enabled_flag SMALLINT NOT NULL DEFAULT 1, status TEXT DEFAULT 'active', city TEXT, current_mileage_km NUMERIC(12,2), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), last_synced_at TIMESTAMPTZ ); -- 3.02 Token Cache CREATE TABLE IF NOT EXISTS tracksolid.api_token_cache ( id BIGSERIAL PRIMARY KEY, account TEXT NOT NULL UNIQUE, access_token TEXT NOT NULL, refresh_token TEXT, app_key TEXT, expires_at TIMESTAMPTZ NOT NULL, obtained_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- 3.03 Ingestion Health Log CREATE TABLE IF NOT EXISTS tracksolid.ingestion_log ( id BIGSERIAL PRIMARY KEY, run_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), endpoint TEXT NOT NULL, imei_count INTEGER NOT NULL DEFAULT 0, rows_upserted INTEGER NOT NULL DEFAULT 0, rows_inserted INTEGER NOT NULL DEFAULT 0, duration_ms INTEGER NOT NULL DEFAULT 0, success BOOLEAN NOT NULL DEFAULT TRUE, error_code TEXT, error_message TEXT ); -- 3.04 Live Positions (Hot Snapshots) CREATE TABLE IF NOT EXISTS tracksolid.live_positions ( imei TEXT PRIMARY KEY REFERENCES tracksolid.devices(imei), geom geometry(Point, 4326), lat DOUBLE PRECISION, lng DOUBLE PRECISION, pos_type TEXT, confidence SMALLINT, gps_time TIMESTAMPTZ, hb_time TIMESTAMPTZ, speed NUMERIC(7,2), direction NUMERIC(6,2), acc_status TEXT, gps_signal SMALLINT, gps_num SMALLINT, elec_quantity NUMERIC(5,2), power_value NUMERIC(5,2), battery_power_val NUMERIC(5,2), tracker_oil TEXT, temperature NUMERIC(8,2), current_mileage NUMERIC(12,2), device_status TEXT, expire_flag TEXT, activation_flag TEXT, loc_desc TEXT, recorded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- 3.05 Position History (Hypertable Source) CREATE TABLE IF NOT EXISTS tracksolid.position_history ( imei TEXT NOT NULL REFERENCES tracksolid.devices(imei), gps_time TIMESTAMPTZ NOT NULL, geom geometry(Point, 4326), lat DOUBLE PRECISION, lng DOUBLE PRECISION, speed NUMERIC(7,2), direction NUMERIC(6,2), acc_status TEXT, satellite SMALLINT, current_mileage NUMERIC(12,2), recorded_at TIMESTAMPTZ DEFAULT NOW(), PRIMARY KEY (imei, gps_time) ); -- 3.06 Trip Summaries CREATE TABLE IF NOT EXISTS tracksolid.trips ( id BIGSERIAL PRIMARY KEY, imei TEXT NOT NULL REFERENCES tracksolid.devices(imei), start_time TIMESTAMPTZ NOT NULL, end_time TIMESTAMPTZ, start_geom geometry(Point, 4326), end_geom geometry(Point, 4326), distance_m NUMERIC(12,2), -- QA-02: Stored in Meters avg_speed_kmh NUMERIC(7,2), max_speed_kmh NUMERIC(7,2), updated_at TIMESTAMPTZ DEFAULT NOW(), CONSTRAINT trips_imei_start_unique UNIQUE (imei, start_time) ); -- 3.07 Parking & Idling CREATE TABLE IF NOT EXISTS tracksolid.parking_events ( id BIGSERIAL PRIMARY KEY, imei TEXT NOT NULL REFERENCES tracksolid.devices(imei), event_type TEXT CHECK (event_type IN ('parking', 'idling')), start_time TIMESTAMPTZ NOT NULL, end_time TIMESTAMPTZ, duration_seconds INTEGER, geom geometry(Point, 4326), address TEXT, updated_at TIMESTAMPTZ DEFAULT NOW(), CONSTRAINT parking_dedup UNIQUE (imei, start_time, event_type) ); -- 3.08 Alarms, OBD, Fault Codes CREATE TABLE IF NOT EXISTS tracksolid.alarms ( id BIGSERIAL PRIMARY KEY, imei TEXT REFERENCES tracksolid.devices(imei), alarm_type TEXT, alarm_time TIMESTAMPTZ, geom geometry(Point, 4326), speed NUMERIC(7,2), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS tracksolid.obd_readings ( id BIGSERIAL PRIMARY KEY, imei TEXT REFERENCES tracksolid.devices(imei), reading_time TIMESTAMPTZ, engine_rpm INTEGER, fuel_level_pct NUMERIC(5,2), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- ============================================================================= -- STEP 4 — TIMESCALEDB CONFIGURATION -- ============================================================================= -- Convert to Hypertable SELECT create_hypertable('tracksolid.position_history', 'gps_time', chunk_time_interval => INTERVAL '7 days', if_not_exists => TRUE); -- Enable Compression ALTER TABLE tracksolid.position_history SET (timescaledb.compress, timescaledb.compress_segmentby = 'imei'); SELECT add_compression_policy('tracksolid.position_history', INTERVAL '14 days'); -- Retention: 90 Days for Hot History SELECT add_retention_policy('tracksolid.position_history', INTERVAL '90 days'); -- ============================================================================= -- STEP 5 — DATA WAREHOUSE GOLD LAYER (dwh_gold) -- ============================================================================= CREATE TABLE dwh_gold.dim_vehicles ( vehicle_key SERIAL PRIMARY KEY, imei TEXT UNIQUE, vehicle_number TEXT, is_active BOOLEAN DEFAULT TRUE ); CREATE TABLE dwh_gold.fact_daily_fleet_metrics ( day DATE NOT NULL, vehicle_key INTEGER REFERENCES dwh_gold.dim_vehicles(vehicle_key), total_distance_km NUMERIC(12,2), max_speed_kmh NUMERIC(7,2), idle_hours NUMERIC(5,2), PRIMARY KEY (day, vehicle_key) ); -- ============================================================================= -- STEP 6 — TRIGGERS & VIEWS -- ============================================================================= -- Updated_at Trigger Function CREATE OR REPLACE FUNCTION tracksolid.set_updated_at() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; -- Apply trigger to tables DO $$ DECLARE t TEXT; BEGIN FOR t IN SELECT tablename FROM pg_tables WHERE schemaname = 'tracksolid' AND has_column('tracksolid', tablename, 'updated_at') LOOP EXECUTE format('CREATE TRIGGER trg_upd_%I BEFORE UPDATE ON tracksolid.%I FOR EACH ROW EXECUTE FUNCTION tracksolid.set_updated_at()', t, t); END LOOP; END $$; -- Enriched Live View CREATE OR REPLACE VIEW tracksolid.v_fleet_status AS SELECT d.imei, d.vehicle_number, d.driver_name, lp.lat, lp.lng, lp.geom, lp.speed, lp.acc_status, lp.gps_time, CASE WHEN lp.gps_time >= NOW() - INTERVAL '5 minutes' THEN 'online' WHEN lp.gps_time >= NOW() - INTERVAL '30 minutes' THEN 'recent' ELSE 'offline' END AS connectivity_status, EXTRACT(EPOCH FROM (NOW() - lp.gps_time))::int AS seconds_since_fix FROM tracksolid.devices d LEFT JOIN tracksolid.live_positions lp USING (imei) WHERE d.enabled_flag = 1; -- Ingestion Health View CREATE OR REPLACE VIEW tracksolid.v_ingestion_health AS SELECT DISTINCT ON (endpoint) endpoint, run_at, success, error_message, EXTRACT(EPOCH FROM (NOW() - run_at))::int AS seconds_ago FROM tracksolid.ingestion_log ORDER BY endpoint, run_at DESC; -- ============================================================================= -- STEP 7 — CONTINUOUS AGGREGATES (Performance) -- ============================================================================= CREATE MATERIALIZED VIEW tracksolid.v_mileage_daily_cagg WITH (timescaledb.continuous) AS SELECT time_bucket('1 day', gps_time) AS bucket, imei, (MAX(current_mileage) - MIN(current_mileage)) AS dist_km, AVG(speed) AS avg_speed FROM tracksolid.position_history GROUP BY bucket, imei; SELECT add_continuous_aggregate_policy('tracksolid.v_mileage_daily_cagg', start_offset => INTERVAL '3 days', end_offset => INTERVAL '1 hour', schedule_interval => INTERVAL '1 hour'); -- ============================================================================= -- STEP 8 — PERMISSIONS -- ============================================================================= GRANT USAGE ON SCHEMA tracksolid, infrastructure, dwh_gold TO tracksolid_owner, grafana_ro; GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA tracksolid, infrastructure, dwh_gold TO tracksolid_owner; GRANT SELECT ON ALL TABLES IN SCHEMA tracksolid, infrastructure, dwh_gold TO grafana_ro; GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO grafana_ro; ALTER DEFAULT PRIVILEGES IN SCHEMA tracksolid GRANT ALL ON TABLES TO tracksolid_owner; ALTER DEFAULT PRIVILEGES IN SCHEMA tracksolid GRANT SELECT ON TABLES TO grafana_ro; -- ============================================================================= -- STEP 9 — VERIFICATION -- ============================================================================= SELECT PostGIS_Full_Version(); SELECT * FROM timescaledb_information.hypertables;