Initial commit: Phase 1 & Phase 2 infrastructure complete
This commit is contained in:
@@ -0,0 +1,669 @@
|
||||
-- GigLez Database Schema
|
||||
-- PostgreSQL 16 + PostGIS 3.4
|
||||
-- Created: 2026-01-12
|
||||
-- Based on: docs/database_schema.md + Wigle analysis
|
||||
|
||||
-- =============================================================================
|
||||
-- EXTENSIONS
|
||||
-- =============================================================================
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS postgis;
|
||||
CREATE EXTENSION IF NOT EXISTS postgis_topology;
|
||||
CREATE EXTENSION IF NOT EXISTS cube; -- For earthdistance
|
||||
CREATE EXTENSION IF NOT EXISTS earthdistance;
|
||||
|
||||
-- =============================================================================
|
||||
-- CORE TABLES
|
||||
-- =============================================================================
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- USERS TABLE
|
||||
-- Optional user accounts for community features
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
|
||||
-- Authentication
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
|
||||
-- Profile
|
||||
display_name VARCHAR(100),
|
||||
avatar_url TEXT,
|
||||
bio TEXT,
|
||||
|
||||
-- Statistics
|
||||
total_captures INTEGER DEFAULT 0,
|
||||
total_identifications INTEGER DEFAULT 0,
|
||||
reputation_score INTEGER DEFAULT 0,
|
||||
|
||||
-- Settings
|
||||
api_key VARCHAR(64) UNIQUE,
|
||||
email_verified BOOLEAN DEFAULT false,
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
last_login TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_users_username ON users(username);
|
||||
CREATE INDEX idx_users_email ON users(email);
|
||||
CREATE INDEX idx_users_api_key ON users(api_key) WHERE api_key IS NOT NULL;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- SESSIONS TABLE
|
||||
-- Wardriving/capture sessions to group related captures
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
|
||||
-- Session Metadata
|
||||
session_uuid VARCHAR(64) UNIQUE NOT NULL, -- UUID for external reference
|
||||
name VARCHAR(200),
|
||||
description TEXT,
|
||||
started_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
ended_at TIMESTAMP,
|
||||
|
||||
-- Privacy Settings
|
||||
is_public BOOLEAN DEFAULT true,
|
||||
anonymize_gps BOOLEAN DEFAULT false,
|
||||
gps_precision_meters INTEGER DEFAULT 10,
|
||||
|
||||
-- Session Statistics (computed)
|
||||
total_captures INTEGER DEFAULT 0,
|
||||
unique_devices INTEGER DEFAULT 0,
|
||||
distance_km DECIMAL(10, 2),
|
||||
|
||||
-- Bounding Box (for quick filtering)
|
||||
min_latitude DECIMAL(10, 8),
|
||||
max_latitude DECIMAL(10, 8),
|
||||
min_longitude DECIMAL(11, 8),
|
||||
max_longitude DECIMAL(11, 8)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_sessions_user ON sessions(user_id);
|
||||
CREATE INDEX idx_sessions_uuid ON sessions(session_uuid);
|
||||
CREATE INDEX idx_sessions_started ON sessions(started_at DESC);
|
||||
CREATE INDEX idx_sessions_public ON sessions(is_public) WHERE is_public = true;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- DEVICES TABLE
|
||||
-- Known IoT device types with signature information
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS devices (
|
||||
id SERIAL PRIMARY KEY,
|
||||
|
||||
-- Device Identification
|
||||
manufacturer VARCHAR(200),
|
||||
model VARCHAR(200),
|
||||
device_type VARCHAR(100), -- 'garage_door', 'weather_station', 'key_fob', etc.
|
||||
description TEXT,
|
||||
|
||||
-- RF Characteristics
|
||||
typical_frequency INTEGER, -- Most common frequency in Hz
|
||||
frequency_range_low INTEGER,
|
||||
frequency_range_high INTEGER,
|
||||
modulation_types TEXT[], -- Array: ['OOK', '2FSK', 'GFSK']
|
||||
|
||||
-- Protocol Information
|
||||
protocol VARCHAR(100),
|
||||
bit_length INTEGER,
|
||||
encoding VARCHAR(50), -- 'PWM', 'PPM', 'Manchester', etc.
|
||||
|
||||
-- Metadata
|
||||
fcc_id VARCHAR(50),
|
||||
manufacturer_code VARCHAR(50),
|
||||
|
||||
-- Community Data
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
verified BOOLEAN DEFAULT false,
|
||||
verification_count INTEGER DEFAULT 0,
|
||||
|
||||
-- Source
|
||||
source VARCHAR(50), -- 'flipper', 'rtl433', 'urh', 'community'
|
||||
source_url TEXT,
|
||||
|
||||
-- Full-text search
|
||||
search_vector tsvector
|
||||
);
|
||||
|
||||
CREATE INDEX idx_devices_manufacturer ON devices(manufacturer);
|
||||
CREATE INDEX idx_devices_type ON devices(device_type);
|
||||
CREATE INDEX idx_devices_frequency ON devices(typical_frequency);
|
||||
CREATE INDEX idx_devices_protocol ON devices(protocol);
|
||||
CREATE INDEX idx_devices_source ON devices(source);
|
||||
CREATE INDEX idx_devices_verified ON devices(verified) WHERE verified = true;
|
||||
CREATE INDEX idx_devices_search ON devices USING GIN(search_vector);
|
||||
|
||||
-- Trigger to update search vector
|
||||
CREATE OR REPLACE FUNCTION devices_search_vector_update() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
NEW.search_vector :=
|
||||
setweight(to_tsvector('english', COALESCE(NEW.manufacturer, '')), 'A') ||
|
||||
setweight(to_tsvector('english', COALESCE(NEW.model, '')), 'A') ||
|
||||
setweight(to_tsvector('english', COALESCE(NEW.device_type, '')), 'B') ||
|
||||
setweight(to_tsvector('english', COALESCE(NEW.description, '')), 'C');
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER tsvector_update BEFORE INSERT OR UPDATE ON devices
|
||||
FOR EACH ROW EXECUTE FUNCTION devices_search_vector_update();
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- CAPTURES TABLE
|
||||
-- Primary table storing RF signal captures with GPS coordinates
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS captures (
|
||||
-- Primary Key: SHA256 hash of file contents (Wigle pattern adapted)
|
||||
file_hash VARCHAR(64) PRIMARY KEY,
|
||||
|
||||
-- Foreign Keys
|
||||
session_id INTEGER REFERENCES sessions(id) ON DELETE CASCADE,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
|
||||
-- GPS Data
|
||||
latitude DECIMAL(10, 8) NOT NULL,
|
||||
longitude DECIMAL(11, 8) NOT NULL,
|
||||
altitude DECIMAL(8, 2),
|
||||
gps_accuracy DECIMAL(6, 2),
|
||||
geom geometry(Point, 4326), -- PostGIS geometry for spatial queries
|
||||
|
||||
-- Timestamp
|
||||
captured_at TIMESTAMP NOT NULL,
|
||||
uploaded_at TIMESTAMP DEFAULT NOW(),
|
||||
|
||||
-- RF Signal Data
|
||||
frequency INTEGER NOT NULL, -- in Hz
|
||||
rssi INTEGER, -- Received Signal Strength Indicator
|
||||
modulation VARCHAR(50), -- OOK, 2FSK, GFSK, etc.
|
||||
preset VARCHAR(100),
|
||||
|
||||
-- Protocol Information (if decoded)
|
||||
protocol VARCHAR(100),
|
||||
bit_length INTEGER,
|
||||
key_data BYTEA,
|
||||
timing_element INTEGER, -- TE value in microseconds
|
||||
|
||||
-- Raw Signal Data
|
||||
raw_data TEXT, -- RAW_Data timings or BinRAW data
|
||||
raw_format VARCHAR(20), -- 'RAW', 'BinRAW', 'KEY'
|
||||
|
||||
-- File Storage
|
||||
file_path VARCHAR(500), -- Path to .sub file
|
||||
file_size INTEGER, -- File size in bytes
|
||||
|
||||
-- Automatic Matching Results (best match)
|
||||
device_id INTEGER REFERENCES devices(id) ON DELETE SET NULL,
|
||||
match_confidence DECIMAL(5, 4), -- 0.0 to 1.0
|
||||
match_method VARCHAR(50), -- 'exact', 'partial', 'pattern', 'timing'
|
||||
|
||||
-- Constraints
|
||||
CONSTRAINT valid_latitude CHECK (latitude >= -90 AND latitude <= 90),
|
||||
CONSTRAINT valid_longitude CHECK (longitude >= -180 AND longitude <= 180),
|
||||
CONSTRAINT valid_confidence CHECK (match_confidence >= 0 AND match_confidence <= 1),
|
||||
CONSTRAINT valid_frequency CHECK (frequency >= 300000000 AND frequency <= 928000000) -- 300-928 MHz
|
||||
);
|
||||
|
||||
-- Geospatial Indexes (critical for performance)
|
||||
CREATE INDEX idx_captures_geom ON captures USING GIST(geom);
|
||||
CREATE INDEX idx_captures_location ON captures USING GIST(ll_to_earth(latitude, longitude));
|
||||
|
||||
-- Query Indexes
|
||||
CREATE INDEX idx_captures_session ON captures(session_id);
|
||||
CREATE INDEX idx_captures_user ON captures(user_id);
|
||||
CREATE INDEX idx_captures_device ON captures(device_id);
|
||||
CREATE INDEX idx_captures_frequency ON captures(frequency);
|
||||
CREATE INDEX idx_captures_protocol ON captures(protocol);
|
||||
CREATE INDEX idx_captures_timestamp ON captures(captured_at DESC);
|
||||
CREATE INDEX idx_captures_uploaded ON captures(uploaded_at DESC);
|
||||
|
||||
-- Trigger to auto-populate geom from lat/lon
|
||||
CREATE OR REPLACE FUNCTION captures_geom_update() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
NEW.geom := ST_SetSRID(ST_MakePoint(NEW.longitude, NEW.latitude), 4326);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER captures_geom_trigger BEFORE INSERT OR UPDATE ON captures
|
||||
FOR EACH ROW EXECUTE FUNCTION captures_geom_update();
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- SIGNATURES TABLE
|
||||
-- Protocol signatures for device matching
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS signatures (
|
||||
id SERIAL PRIMARY KEY,
|
||||
device_id INTEGER REFERENCES devices(id) ON DELETE CASCADE,
|
||||
|
||||
-- Signature Pattern
|
||||
protocol VARCHAR(100) NOT NULL,
|
||||
frequency INTEGER,
|
||||
modulation VARCHAR(50),
|
||||
|
||||
-- Matching Criteria
|
||||
bit_pattern BYTEA,
|
||||
bit_mask BYTEA, -- Which bits to match (1=match, 0=ignore)
|
||||
timing_min INTEGER, -- TE range in microseconds
|
||||
timing_max INTEGER,
|
||||
|
||||
-- Raw Pattern (for regex-like matching)
|
||||
pattern_regex TEXT,
|
||||
|
||||
-- Confidence Weighting
|
||||
weight DECIMAL(4, 3) DEFAULT 1.0, -- Higher = more reliable signature
|
||||
|
||||
-- Metadata
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
source VARCHAR(50), -- 'flipper', 'rtl433', 'urh', 'community'
|
||||
source_file VARCHAR(500), -- Original .sub or config file
|
||||
|
||||
UNIQUE(device_id, protocol, bit_pattern)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_signatures_device ON signatures(device_id);
|
||||
CREATE INDEX idx_signatures_protocol ON signatures(protocol);
|
||||
CREATE INDEX idx_signatures_frequency ON signatures(frequency);
|
||||
CREATE INDEX idx_signatures_source ON signatures(source);
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- CAPTURE_MATCHES TABLE
|
||||
-- All device identification results (many-to-many with captures)
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS capture_matches (
|
||||
id SERIAL PRIMARY KEY,
|
||||
capture_id VARCHAR(64) REFERENCES captures(file_hash) ON DELETE CASCADE,
|
||||
device_id INTEGER REFERENCES devices(id) ON DELETE CASCADE,
|
||||
signature_id INTEGER REFERENCES signatures(id) ON DELETE SET NULL,
|
||||
|
||||
-- Match Details
|
||||
confidence DECIMAL(5, 4) NOT NULL, -- 0.0 to 1.0
|
||||
match_method VARCHAR(50) NOT NULL, -- 'exact', 'partial', 'pattern', 'timing'
|
||||
match_details JSONB, -- Additional matching information
|
||||
|
||||
-- Timestamps
|
||||
matched_at TIMESTAMP DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT valid_match_confidence CHECK (confidence >= 0 AND confidence <= 1)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_capture_matches_capture ON capture_matches(capture_id);
|
||||
CREATE INDEX idx_capture_matches_device ON capture_matches(device_id);
|
||||
CREATE INDEX idx_capture_matches_signature ON capture_matches(signature_id);
|
||||
CREATE INDEX idx_capture_matches_confidence ON capture_matches(confidence DESC);
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- IDENTIFICATIONS TABLE
|
||||
-- User-submitted device identifications (community contributions)
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS identifications (
|
||||
id SERIAL PRIMARY KEY,
|
||||
capture_id VARCHAR(64) REFERENCES captures(file_hash) ON DELETE CASCADE,
|
||||
device_id INTEGER REFERENCES devices(id),
|
||||
user_id INTEGER REFERENCES users(id),
|
||||
|
||||
-- Identification Details
|
||||
confidence VARCHAR(20), -- 'certain', 'likely', 'guess'
|
||||
notes TEXT,
|
||||
|
||||
-- Visual Evidence
|
||||
photo_urls TEXT[], -- Array of image URLs
|
||||
|
||||
-- Community Validation
|
||||
upvotes INTEGER DEFAULT 0,
|
||||
downvotes INTEGER DEFAULT 0,
|
||||
verified BOOLEAN DEFAULT false,
|
||||
verified_by INTEGER REFERENCES users(id),
|
||||
verified_at TIMESTAMP,
|
||||
|
||||
-- Timestamps
|
||||
submitted_at TIMESTAMP DEFAULT NOW(),
|
||||
|
||||
UNIQUE(capture_id, user_id, device_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_identifications_capture ON identifications(capture_id);
|
||||
CREATE INDEX idx_identifications_device ON identifications(device_id);
|
||||
CREATE INDEX idx_identifications_user ON identifications(user_id);
|
||||
CREATE INDEX idx_identifications_verified ON identifications(verified) WHERE verified = true;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- VOTES TABLE
|
||||
-- Voting on device identifications
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS votes (
|
||||
id SERIAL PRIMARY KEY,
|
||||
identification_id INTEGER REFERENCES identifications(id) ON DELETE CASCADE,
|
||||
user_id INTEGER REFERENCES users(id),
|
||||
|
||||
vote_type INTEGER NOT NULL, -- 1 = upvote, -1 = downvote
|
||||
voted_at TIMESTAMP DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT valid_vote CHECK (vote_type IN (1, -1)),
|
||||
UNIQUE(identification_id, user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_votes_identification ON votes(identification_id);
|
||||
CREATE INDEX idx_votes_user ON votes(user_id);
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- UPLOAD_MARKERS TABLE
|
||||
-- Track last uploaded capture per user/session for incremental sync
|
||||
-- Wigle pattern for efficient resumable uploads
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS upload_markers (
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
session_id INTEGER REFERENCES sessions(id) ON DELETE CASCADE,
|
||||
last_capture_hash VARCHAR(64),
|
||||
last_upload_time TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
|
||||
PRIMARY KEY (user_id, session_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_upload_markers_user ON upload_markers(user_id);
|
||||
CREATE INDEX idx_upload_markers_session ON upload_markers(session_id);
|
||||
|
||||
-- =============================================================================
|
||||
-- FLIPPER ZERO SPECIFIC TABLES
|
||||
-- =============================================================================
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- FLIPPER_SIGNATURES TABLE
|
||||
-- Imported Flipper Zero .sub file signatures
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS flipper_signatures (
|
||||
id SERIAL PRIMARY KEY,
|
||||
signature_id INTEGER REFERENCES signatures(id) ON DELETE CASCADE,
|
||||
|
||||
-- Flipper-Specific Fields
|
||||
filetype VARCHAR(50), -- 'Flipper SubGhz Key File' or 'RAW File'
|
||||
version INTEGER,
|
||||
preset VARCHAR(100),
|
||||
|
||||
-- Custom Preset Data
|
||||
custom_preset_module VARCHAR(50),
|
||||
custom_preset_data BYTEA,
|
||||
|
||||
-- Protocol Data
|
||||
protocol VARCHAR(100),
|
||||
bit INTEGER,
|
||||
key BYTEA,
|
||||
te INTEGER, -- Timing element
|
||||
|
||||
-- RAW Data
|
||||
raw_data TEXT,
|
||||
bin_raw_bit INTEGER,
|
||||
bin_raw_te INTEGER,
|
||||
bin_raw_data BYTEA,
|
||||
|
||||
-- Source
|
||||
source_file VARCHAR(500),
|
||||
imported_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_flipper_protocol ON flipper_signatures(protocol);
|
||||
CREATE INDEX idx_flipper_preset ON flipper_signatures(preset);
|
||||
CREATE INDEX idx_flipper_signature ON flipper_signatures(signature_id);
|
||||
|
||||
-- =============================================================================
|
||||
-- RTL_433 SPECIFIC TABLES
|
||||
-- =============================================================================
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- RTL433_PROTOCOLS TABLE
|
||||
-- Imported RTL_433 protocol definitions
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS rtl433_protocols (
|
||||
id SERIAL PRIMARY KEY,
|
||||
signature_id INTEGER REFERENCES signatures(id) ON DELETE CASCADE,
|
||||
|
||||
-- Protocol Identification
|
||||
protocol_number INTEGER,
|
||||
protocol_name VARCHAR(200),
|
||||
model VARCHAR(200),
|
||||
|
||||
-- RF Characteristics
|
||||
frequency INTEGER,
|
||||
modulation VARCHAR(50), -- 'OOK_PWM', 'FSK_PCM', etc.
|
||||
|
||||
-- Timing Information (in microseconds)
|
||||
short_width INTEGER,
|
||||
long_width INTEGER,
|
||||
reset_limit INTEGER,
|
||||
gap_limit INTEGER,
|
||||
|
||||
-- Decoding
|
||||
decoder_type VARCHAR(50),
|
||||
bit_count INTEGER,
|
||||
|
||||
-- JSON Fields Mapping
|
||||
json_fields JSONB, -- Expected output fields
|
||||
|
||||
-- Source
|
||||
source_file VARCHAR(500),
|
||||
imported_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_rtl433_protocol_num ON rtl433_protocols(protocol_number);
|
||||
CREATE INDEX idx_rtl433_model ON rtl433_protocols(model);
|
||||
CREATE INDEX idx_rtl433_modulation ON rtl433_protocols(modulation);
|
||||
CREATE INDEX idx_rtl433_signature ON rtl433_protocols(signature_id);
|
||||
|
||||
-- =============================================================================
|
||||
-- MATERIALIZED VIEWS
|
||||
-- =============================================================================
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- DEVICE_STATISTICS VIEW
|
||||
-- Pre-computed statistics for device types
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS device_statistics AS
|
||||
SELECT
|
||||
d.id as device_id,
|
||||
d.manufacturer,
|
||||
d.model,
|
||||
d.device_type,
|
||||
COUNT(c.file_hash) as total_captures,
|
||||
COUNT(DISTINCT c.session_id) as total_sessions,
|
||||
COUNT(DISTINCT c.user_id) as total_users,
|
||||
MIN(c.captured_at) as first_seen,
|
||||
MAX(c.captured_at) as last_seen,
|
||||
AVG(c.rssi) as avg_rssi,
|
||||
AVG(c.match_confidence) as avg_confidence
|
||||
FROM devices d
|
||||
LEFT JOIN captures c ON c.device_id = d.id
|
||||
GROUP BY d.id, d.manufacturer, d.model, d.device_type;
|
||||
|
||||
CREATE UNIQUE INDEX idx_device_stats_device ON device_statistics(device_id);
|
||||
CREATE INDEX idx_device_stats_captures ON device_statistics(total_captures DESC);
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- GEOGRAPHIC_HEATMAP VIEW
|
||||
-- Aggregated capture density for mapping
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS geographic_heatmap AS
|
||||
SELECT
|
||||
ROUND(latitude::numeric, 3) as lat_bucket,
|
||||
ROUND(longitude::numeric, 3) as lon_bucket,
|
||||
COUNT(*) as capture_count,
|
||||
COUNT(DISTINCT device_id) as unique_devices,
|
||||
COUNT(DISTINCT session_id) as unique_sessions,
|
||||
array_agg(DISTINCT protocol) FILTER (WHERE protocol IS NOT NULL) as protocols_seen,
|
||||
AVG(rssi) as avg_rssi
|
||||
FROM captures
|
||||
GROUP BY lat_bucket, lon_bucket;
|
||||
|
||||
CREATE UNIQUE INDEX idx_heatmap_location ON geographic_heatmap(lat_bucket, lon_bucket);
|
||||
CREATE INDEX idx_heatmap_count ON geographic_heatmap(capture_count DESC);
|
||||
|
||||
-- =============================================================================
|
||||
-- FUNCTIONS
|
||||
-- =============================================================================
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- CALCULATE_DISTANCE FUNCTION
|
||||
-- Calculate distance between two GPS coordinates (Haversine formula)
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE OR REPLACE FUNCTION calculate_distance(
|
||||
lat1 DECIMAL, lon1 DECIMAL,
|
||||
lat2 DECIMAL, lon2 DECIMAL
|
||||
) RETURNS DECIMAL AS $$
|
||||
DECLARE
|
||||
R DECIMAL := 6371.0; -- Earth radius in km
|
||||
dLat DECIMAL;
|
||||
dLon DECIMAL;
|
||||
a DECIMAL;
|
||||
c DECIMAL;
|
||||
BEGIN
|
||||
dLat := radians(lat2 - lat1);
|
||||
dLon := radians(lon2 - lon1);
|
||||
a := sin(dLat/2) * sin(dLat/2) +
|
||||
cos(radians(lat1)) * cos(radians(lat2)) *
|
||||
sin(dLon/2) * sin(dLon/2);
|
||||
c := 2 * atan2(sqrt(a), sqrt(1-a));
|
||||
RETURN R * c;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql IMMUTABLE;
|
||||
|
||||
-- =============================================================================
|
||||
-- TRIGGERS
|
||||
-- =============================================================================
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- UPDATE_SESSION_STATISTICS TRIGGER
|
||||
-- Automatically update session statistics when captures are added
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE OR REPLACE FUNCTION update_session_stats()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
UPDATE sessions SET
|
||||
total_captures = (
|
||||
SELECT COUNT(*)
|
||||
FROM captures
|
||||
WHERE session_id = NEW.session_id
|
||||
),
|
||||
unique_devices = (
|
||||
SELECT COUNT(DISTINCT device_id)
|
||||
FROM captures
|
||||
WHERE session_id = NEW.session_id
|
||||
AND device_id IS NOT NULL
|
||||
),
|
||||
min_latitude = LEAST(
|
||||
COALESCE(min_latitude, NEW.latitude),
|
||||
NEW.latitude
|
||||
),
|
||||
max_latitude = GREATEST(
|
||||
COALESCE(max_latitude, NEW.latitude),
|
||||
NEW.latitude
|
||||
),
|
||||
min_longitude = LEAST(
|
||||
COALESCE(min_longitude, NEW.longitude),
|
||||
NEW.longitude
|
||||
),
|
||||
max_longitude = GREATEST(
|
||||
COALESCE(max_longitude, NEW.longitude),
|
||||
NEW.longitude
|
||||
)
|
||||
WHERE id = NEW.session_id;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trigger_update_session_stats
|
||||
AFTER INSERT ON captures
|
||||
FOR EACH ROW
|
||||
WHEN (NEW.session_id IS NOT NULL)
|
||||
EXECUTE FUNCTION update_session_stats();
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- UPDATE_USER_STATISTICS TRIGGER
|
||||
-- Update user capture count
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE OR REPLACE FUNCTION update_user_stats()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF NEW.user_id IS NOT NULL THEN
|
||||
UPDATE users SET
|
||||
total_captures = (
|
||||
SELECT COUNT(*)
|
||||
FROM captures
|
||||
WHERE user_id = NEW.user_id
|
||||
)
|
||||
WHERE id = NEW.user_id;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trigger_update_user_stats
|
||||
AFTER INSERT ON captures
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_user_stats();
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- UPDATE_IDENTIFICATION_VOTES TRIGGER
|
||||
-- Update upvotes/downvotes count on identifications
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE OR REPLACE FUNCTION update_identification_votes()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
up_count INTEGER;
|
||||
down_count INTEGER;
|
||||
BEGIN
|
||||
SELECT
|
||||
COUNT(*) FILTER (WHERE vote_type = 1),
|
||||
COUNT(*) FILTER (WHERE vote_type = -1)
|
||||
INTO up_count, down_count
|
||||
FROM votes
|
||||
WHERE identification_id = COALESCE(NEW.identification_id, OLD.identification_id);
|
||||
|
||||
UPDATE identifications SET
|
||||
upvotes = up_count,
|
||||
downvotes = down_count,
|
||||
verified = (up_count - down_count >= 5) -- Auto-verify at +5 net votes
|
||||
WHERE id = COALESCE(NEW.identification_id, OLD.identification_id);
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trigger_update_identification_votes
|
||||
AFTER INSERT OR UPDATE OR DELETE ON votes
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_identification_votes();
|
||||
|
||||
-- =============================================================================
|
||||
-- INITIAL DATA / SEED
|
||||
-- =============================================================================
|
||||
|
||||
-- Insert a default "anonymous" user for uploads without accounts
|
||||
INSERT INTO users (id, username, email, password_hash, display_name)
|
||||
VALUES (0, 'anonymous', 'anonymous@giglez.local', '', 'Anonymous User')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- Reset sequence to avoid conflicts
|
||||
SELECT setval('users_id_seq', GREATEST(1, (SELECT MAX(id) + 1 FROM users)));
|
||||
|
||||
-- =============================================================================
|
||||
-- PERMISSIONS (Optional - uncomment if using specific user)
|
||||
-- =============================================================================
|
||||
|
||||
-- GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO giglez_user;
|
||||
-- GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO giglez_user;
|
||||
-- GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO giglez_user;
|
||||
|
||||
-- =============================================================================
|
||||
-- COMPLETION
|
||||
-- =============================================================================
|
||||
|
||||
-- Refresh materialized views
|
||||
REFRESH MATERIALIZED VIEW device_statistics;
|
||||
REFRESH MATERIALIZED VIEW geographic_heatmap;
|
||||
|
||||
-- Analyze tables for query optimization
|
||||
ANALYZE;
|
||||
|
||||
SELECT 'GigLez database schema created successfully!' as status;
|
||||
Executable
+55
@@ -0,0 +1,55 @@
|
||||
#!/bin/bash
|
||||
# GigLez Database Setup Script
|
||||
# This script creates the PostgreSQL database and user for GigLez
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
echo "🚀 GigLez Database Setup"
|
||||
echo "========================"
|
||||
echo ""
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Database configuration
|
||||
DB_NAME="giglez"
|
||||
DB_USER="giglez_user"
|
||||
DB_PASSWORD="giglez_secure_password_2026" # Change this in production!
|
||||
|
||||
echo -e "${BLUE}Step 1: Creating PostgreSQL user${NC}"
|
||||
sudo -u postgres psql -c "CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASSWORD}';" 2>/dev/null || echo " User already exists, skipping..."
|
||||
|
||||
echo -e "${BLUE}Step 2: Creating database${NC}"
|
||||
sudo -u postgres psql -c "CREATE DATABASE ${DB_NAME} OWNER ${DB_USER};" 2>/dev/null || echo " Database already exists, skipping..."
|
||||
|
||||
echo -e "${BLUE}Step 3: Granting privileges${NC}"
|
||||
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE ${DB_NAME} TO ${DB_USER};"
|
||||
|
||||
echo -e "${BLUE}Step 4: Enabling PostGIS extension${NC}"
|
||||
sudo -u postgres psql -d ${DB_NAME} -c "CREATE EXTENSION IF NOT EXISTS postgis;"
|
||||
sudo -u postgres psql -d ${DB_NAME} -c "CREATE EXTENSION IF NOT EXISTS postgis_topology;"
|
||||
|
||||
echo -e "${BLUE}Step 5: Granting schema permissions${NC}"
|
||||
sudo -u postgres psql -d ${DB_NAME} -c "GRANT ALL ON SCHEMA public TO ${DB_USER};"
|
||||
sudo -u postgres psql -d ${DB_NAME} -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ${DB_USER};"
|
||||
sudo -u postgres psql -d ${DB_NAME} -c "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ${DB_USER};"
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}✅ Database setup complete!${NC}"
|
||||
echo ""
|
||||
echo "Connection details:"
|
||||
echo " Database: ${DB_NAME}"
|
||||
echo " User: ${DB_USER}"
|
||||
echo " Password: ${DB_PASSWORD}"
|
||||
echo " Host: localhost"
|
||||
echo " Port: 5432"
|
||||
echo ""
|
||||
echo "Connection string:"
|
||||
echo " postgresql://${DB_USER}:${DB_PASSWORD}@localhost:5432/${DB_NAME}"
|
||||
echo ""
|
||||
echo "Test connection:"
|
||||
echo " psql -U ${DB_USER} -d ${DB_NAME} -h localhost"
|
||||
echo ""
|
||||
Reference in New Issue
Block a user