Routes and individual flights are now written to the database as each query result arrives, rather than after all queries finish. The frontend already polls /scans/:id/routes while status=running, so routes appear progressively with no frontend changes needed. Changes: - database/schema.sql: UNIQUE INDEX uq_routes_scan_dest(scan_id, destination) - database/init_db.py: _migrate_add_routes_unique_index() migration - scan_processor.py: _write_route_incremental() helper; progress_callback now writes routes/flights immediately; Phase 2 bulk-write replaced with a lightweight totals query - searcher_v3.py: pass flights= kwarg to progress_callback on cache_hit and api_success paths Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
266 lines
8.4 KiB
SQL
266 lines
8.4 KiB
SQL
-- Flight Radar Web App - Database Schema
|
|
-- Version: 2.0
|
|
-- Date: 2026-02-23
|
|
-- Database: SQLite 3
|
|
--
|
|
-- This schema extends the existing cache.db with new tables for the web app.
|
|
-- Existing tables (flight_searches, flight_results) are preserved.
|
|
|
|
-- ============================================================================
|
|
-- CRITICAL: Enable Foreign Keys (SQLite default is OFF!)
|
|
-- ============================================================================
|
|
PRAGMA foreign_keys = ON;
|
|
|
|
-- ============================================================================
|
|
-- Table: scans
|
|
-- Purpose: Track flight scan requests and their status
|
|
-- ============================================================================
|
|
CREATE TABLE IF NOT EXISTS scans (
|
|
-- Primary key with auto-increment
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
-- Search parameters (validated by CHECK constraints)
|
|
origin TEXT NOT NULL CHECK(length(origin) = 3),
|
|
country TEXT NOT NULL CHECK(length(country) >= 2),
|
|
start_date TEXT NOT NULL, -- ISO 8601: YYYY-MM-DD
|
|
end_date TEXT NOT NULL,
|
|
|
|
-- Timestamps (auto-managed)
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
-- Scan status (enforced enum via CHECK)
|
|
status TEXT NOT NULL DEFAULT 'pending'
|
|
CHECK(status IN ('pending', 'running', 'completed', 'failed')),
|
|
|
|
-- Progress tracking
|
|
total_routes INTEGER NOT NULL DEFAULT 0 CHECK(total_routes >= 0),
|
|
routes_scanned INTEGER NOT NULL DEFAULT 0 CHECK(routes_scanned >= 0),
|
|
total_flights INTEGER NOT NULL DEFAULT 0 CHECK(total_flights >= 0),
|
|
|
|
-- Error information (NULL if no error)
|
|
error_message TEXT,
|
|
|
|
-- Additional search parameters
|
|
seat_class TEXT DEFAULT 'economy',
|
|
adults INTEGER DEFAULT 1 CHECK(adults > 0 AND adults <= 9),
|
|
|
|
-- Constraints across columns
|
|
CHECK(end_date >= start_date),
|
|
CHECK(routes_scanned <= total_routes OR total_routes = 0)
|
|
);
|
|
|
|
-- Performance indexes for scans table
|
|
CREATE INDEX IF NOT EXISTS idx_scans_origin_country
|
|
ON scans(origin, country);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_scans_status
|
|
ON scans(status)
|
|
WHERE status IN ('pending', 'running'); -- Partial index for active scans
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_scans_created_at
|
|
ON scans(created_at DESC); -- For recent scans query
|
|
|
|
-- ============================================================================
|
|
-- Table: routes
|
|
-- Purpose: Store discovered routes with flight statistics
|
|
-- ============================================================================
|
|
CREATE TABLE IF NOT EXISTS routes (
|
|
-- Primary key
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
-- Foreign key to scans (cascade delete)
|
|
scan_id INTEGER NOT NULL,
|
|
|
|
-- Destination airport
|
|
destination TEXT NOT NULL CHECK(length(destination) = 3),
|
|
destination_name TEXT NOT NULL,
|
|
destination_city TEXT,
|
|
|
|
-- Flight statistics
|
|
flight_count INTEGER NOT NULL DEFAULT 0 CHECK(flight_count >= 0),
|
|
airlines TEXT NOT NULL, -- JSON array: ["Ryanair", "Lufthansa"]
|
|
|
|
-- Price statistics (NULL if no flights)
|
|
min_price REAL CHECK(min_price >= 0),
|
|
max_price REAL CHECK(max_price >= 0),
|
|
avg_price REAL CHECK(avg_price >= 0),
|
|
|
|
-- Timestamp
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
-- Foreign key constraint with cascade delete
|
|
FOREIGN KEY (scan_id)
|
|
REFERENCES scans(id)
|
|
ON DELETE CASCADE,
|
|
|
|
-- Price consistency constraints
|
|
CHECK(max_price >= min_price OR max_price IS NULL),
|
|
CHECK(avg_price >= min_price OR avg_price IS NULL),
|
|
CHECK(avg_price <= max_price OR avg_price IS NULL)
|
|
);
|
|
|
|
-- Performance indexes for routes table
|
|
CREATE INDEX IF NOT EXISTS idx_routes_scan_id
|
|
ON routes(scan_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_routes_destination
|
|
ON routes(destination);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_routes_min_price
|
|
ON routes(min_price)
|
|
WHERE min_price IS NOT NULL; -- Partial index for routes with prices
|
|
|
|
-- One route row per (scan, destination) — enables incremental upsert writes
|
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_routes_scan_dest
|
|
ON routes(scan_id, destination);
|
|
|
|
-- ============================================================================
|
|
-- Triggers: Auto-update timestamps and aggregates
|
|
-- ============================================================================
|
|
|
|
-- Trigger: Update scans.updated_at on any update
|
|
CREATE TRIGGER IF NOT EXISTS update_scans_timestamp
|
|
AFTER UPDATE ON scans
|
|
FOR EACH ROW
|
|
BEGIN
|
|
UPDATE scans
|
|
SET updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = NEW.id;
|
|
END;
|
|
|
|
-- Trigger: Update total_flights count when routes are inserted
|
|
CREATE TRIGGER IF NOT EXISTS update_scan_flight_count_insert
|
|
AFTER INSERT ON routes
|
|
FOR EACH ROW
|
|
BEGIN
|
|
UPDATE scans
|
|
SET total_flights = (
|
|
SELECT COALESCE(SUM(flight_count), 0)
|
|
FROM routes
|
|
WHERE scan_id = NEW.scan_id
|
|
)
|
|
WHERE id = NEW.scan_id;
|
|
END;
|
|
|
|
-- Trigger: Update total_flights count when routes are updated
|
|
CREATE TRIGGER IF NOT EXISTS update_scan_flight_count_update
|
|
AFTER UPDATE OF flight_count ON routes
|
|
FOR EACH ROW
|
|
BEGIN
|
|
UPDATE scans
|
|
SET total_flights = (
|
|
SELECT COALESCE(SUM(flight_count), 0)
|
|
FROM routes
|
|
WHERE scan_id = NEW.scan_id
|
|
)
|
|
WHERE id = NEW.scan_id;
|
|
END;
|
|
|
|
-- Trigger: Update total_flights count when routes are deleted
|
|
CREATE TRIGGER IF NOT EXISTS update_scan_flight_count_delete
|
|
AFTER DELETE ON routes
|
|
FOR EACH ROW
|
|
BEGIN
|
|
UPDATE scans
|
|
SET total_flights = (
|
|
SELECT COALESCE(SUM(flight_count), 0)
|
|
FROM routes
|
|
WHERE scan_id = OLD.scan_id
|
|
)
|
|
WHERE id = OLD.scan_id;
|
|
END;
|
|
|
|
-- ============================================================================
|
|
-- Table: flights
|
|
-- Purpose: Store individual flights discovered per scan
|
|
-- ============================================================================
|
|
CREATE TABLE IF NOT EXISTS flights (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
-- Foreign key to scans (cascade delete)
|
|
scan_id INTEGER NOT NULL,
|
|
|
|
-- Route
|
|
destination TEXT NOT NULL CHECK(length(destination) = 3),
|
|
date TEXT NOT NULL, -- ISO 8601: YYYY-MM-DD
|
|
|
|
-- Flight details
|
|
airline TEXT,
|
|
departure_time TEXT, -- HH:MM
|
|
arrival_time TEXT, -- HH:MM
|
|
price REAL CHECK(price >= 0),
|
|
stops INTEGER NOT NULL DEFAULT 0,
|
|
|
|
-- Timestamp
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
FOREIGN KEY (scan_id)
|
|
REFERENCES scans(id)
|
|
ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_flights_scan_id
|
|
ON flights(scan_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_flights_scan_dest
|
|
ON flights(scan_id, destination);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_flights_price
|
|
ON flights(scan_id, price ASC)
|
|
WHERE price IS NOT NULL;
|
|
|
|
-- ============================================================================
|
|
-- Views: Useful queries
|
|
-- ============================================================================
|
|
|
|
-- View: Recent scans with route counts
|
|
CREATE VIEW IF NOT EXISTS recent_scans AS
|
|
SELECT
|
|
s.id,
|
|
s.origin,
|
|
s.country,
|
|
s.status,
|
|
s.created_at,
|
|
s.total_routes,
|
|
s.total_flights,
|
|
COUNT(r.id) as routes_found,
|
|
MIN(r.min_price) as cheapest_flight,
|
|
s.error_message
|
|
FROM scans s
|
|
LEFT JOIN routes r ON r.scan_id = s.id
|
|
GROUP BY s.id
|
|
ORDER BY s.created_at DESC
|
|
LIMIT 10;
|
|
|
|
-- View: Active scans (pending or running)
|
|
CREATE VIEW IF NOT EXISTS active_scans AS
|
|
SELECT *
|
|
FROM scans
|
|
WHERE status IN ('pending', 'running')
|
|
ORDER BY created_at ASC;
|
|
|
|
-- ============================================================================
|
|
-- Initial Data: None (tables start empty)
|
|
-- ============================================================================
|
|
|
|
-- Schema version tracking (for future migrations)
|
|
CREATE TABLE IF NOT EXISTS schema_version (
|
|
version INTEGER PRIMARY KEY,
|
|
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
description TEXT
|
|
);
|
|
|
|
INSERT OR IGNORE INTO schema_version (version, description)
|
|
VALUES (1, 'Initial web app schema with scans and routes tables');
|
|
|
|
-- ============================================================================
|
|
-- Verification Queries (for testing)
|
|
-- ============================================================================
|
|
|
|
-- Uncomment to verify schema creation:
|
|
-- SELECT name, type FROM sqlite_master WHERE type IN ('table', 'index', 'trigger', 'view') ORDER BY type, name;
|
|
-- PRAGMA foreign_keys;
|
|
-- PRAGMA table_info(scans);
|
|
-- PRAGMA table_info(routes);
|