-- Migration 001: Initial schema -- Creates all tables for the pixa caching image proxy -- Source content blobs -- Files stored at: cache/sources/// CREATE TABLE IF NOT EXISTS source_content ( content_hash TEXT PRIMARY KEY, content_type TEXT NOT NULL, size_bytes INTEGER NOT NULL, fetched_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_accessed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_source_content_last_accessed ON source_content(last_accessed_at); -- Source URL metadata - maps URLs to content hashes -- JSON stored at: cache/metadata//.json CREATE TABLE IF NOT EXISTS source_metadata ( id INTEGER PRIMARY KEY AUTOINCREMENT, source_host TEXT NOT NULL, source_path TEXT NOT NULL, source_query TEXT NOT NULL DEFAULT '', path_hash TEXT NOT NULL, content_hash TEXT, status_code INTEGER NOT NULL, content_type TEXT, response_headers TEXT, fetched_at DATETIME DEFAULT CURRENT_TIMESTAMP, expires_at DATETIME, etag TEXT, last_modified TEXT, UNIQUE(source_host, source_path, source_query), FOREIGN KEY (content_hash) REFERENCES source_content(content_hash) ); CREATE INDEX IF NOT EXISTS idx_source_meta_host ON source_metadata(source_host); CREATE INDEX IF NOT EXISTS idx_source_meta_path_hash ON source_metadata(path_hash); CREATE INDEX IF NOT EXISTS idx_source_meta_expires ON source_metadata(expires_at); CREATE INDEX IF NOT EXISTS idx_source_meta_content_hash ON source_metadata(content_hash); -- Processed variant blobs -- Files stored at: cache/variants/// (plus a .meta -- sidecar with the content type). Tracked here (like source content -- blobs above) so total cache usage is known without a directory scan, -- and so LRU eviction has a timestamp to order on. CREATE TABLE IF NOT EXISTS variant_content ( cache_key TEXT PRIMARY KEY, size_bytes INTEGER NOT NULL, content_type TEXT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, last_accessed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_variant_content_last_accessed ON variant_content(last_accessed_at); -- Total cache usage: the sum of size_bytes over source_content and -- variant_content, kept by the triggers below in the same statement -- that adds, removes or resizes a row, so eviction reads this one row -- instead of summing both tables. Each trigger also adds one to -- change_count; the reconciliation pass, which sums both tables a page -- at a time, corrects total_size_bytes only if change_count did not -- move while it summed. CREATE TABLE IF NOT EXISTS cache_usage ( id INTEGER PRIMARY KEY CHECK (id = 1), total_size_bytes INTEGER NOT NULL DEFAULT 0, change_count INTEGER NOT NULL DEFAULT 0 ); INSERT OR IGNORE INTO cache_usage (id) VALUES (1); CREATE TRIGGER IF NOT EXISTS source_content_usage_insert AFTER INSERT ON source_content BEGIN UPDATE cache_usage SET total_size_bytes = total_size_bytes + NEW.size_bytes, change_count = change_count + 1 WHERE id = 1; END; CREATE TRIGGER IF NOT EXISTS source_content_usage_delete AFTER DELETE ON source_content BEGIN UPDATE cache_usage SET total_size_bytes = total_size_bytes - OLD.size_bytes, change_count = change_count + 1 WHERE id = 1; END; CREATE TRIGGER IF NOT EXISTS source_content_usage_update AFTER UPDATE OF size_bytes ON source_content BEGIN UPDATE cache_usage SET total_size_bytes = total_size_bytes - OLD.size_bytes + NEW.size_bytes, change_count = change_count + 1 WHERE id = 1; END; CREATE TRIGGER IF NOT EXISTS variant_content_usage_insert AFTER INSERT ON variant_content BEGIN UPDATE cache_usage SET total_size_bytes = total_size_bytes + NEW.size_bytes, change_count = change_count + 1 WHERE id = 1; END; CREATE TRIGGER IF NOT EXISTS variant_content_usage_delete AFTER DELETE ON variant_content BEGIN UPDATE cache_usage SET total_size_bytes = total_size_bytes - OLD.size_bytes, change_count = change_count + 1 WHERE id = 1; END; CREATE TRIGGER IF NOT EXISTS variant_content_usage_update AFTER UPDATE OF size_bytes ON variant_content BEGIN UPDATE cache_usage SET total_size_bytes = total_size_bytes - OLD.size_bytes + NEW.size_bytes, change_count = change_count + 1 WHERE id = 1; END; -- Output/transformed content blobs -- Not written: transformed images are stored in cache/variants and -- tracked in variant_content above. CREATE TABLE IF NOT EXISTS output_content ( content_hash TEXT PRIMARY KEY, content_type TEXT NOT NULL, size_bytes INTEGER NOT NULL, fetched_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- Request cache - maps full request params to output content CREATE TABLE IF NOT EXISTS request_cache ( id INTEGER PRIMARY KEY AUTOINCREMENT, cache_key TEXT NOT NULL UNIQUE, source_metadata_id INTEGER NOT NULL, output_hash TEXT NOT NULL, width INTEGER NOT NULL, height INTEGER NOT NULL, format TEXT NOT NULL, quality INTEGER NOT NULL DEFAULT 85, fit_mode TEXT NOT NULL DEFAULT 'cover', fetched_at DATETIME DEFAULT CURRENT_TIMESTAMP, access_count INTEGER NOT NULL DEFAULT 1, FOREIGN KEY (source_metadata_id) REFERENCES source_metadata(id), FOREIGN KEY (output_hash) REFERENCES output_content(content_hash) ); CREATE INDEX IF NOT EXISTS idx_request_cache_key ON request_cache(cache_key); CREATE INDEX IF NOT EXISTS idx_request_cache_source ON request_cache(source_metadata_id); CREATE INDEX IF NOT EXISTS idx_request_cache_output ON request_cache(output_hash); CREATE INDEX IF NOT EXISTS idx_request_cache_fetched ON request_cache(fetched_at); -- Negative cache for failed fetches (404s, timeouts, etc.) CREATE TABLE IF NOT EXISTS negative_cache ( id INTEGER PRIMARY KEY AUTOINCREMENT, source_host TEXT NOT NULL, source_path TEXT NOT NULL, source_query TEXT NOT NULL DEFAULT '', status_code INTEGER NOT NULL, error_message TEXT, fetched_at DATETIME DEFAULT CURRENT_TIMESTAMP, expires_at DATETIME NOT NULL, UNIQUE(source_host, source_path, source_query) ); CREATE INDEX IF NOT EXISTS idx_negative_cache_expires ON negative_cache(expires_at); -- Cache statistics for monitoring CREATE TABLE IF NOT EXISTS cache_stats ( id INTEGER PRIMARY KEY CHECK (id = 1), hit_count INTEGER NOT NULL DEFAULT 0, miss_count INTEGER NOT NULL DEFAULT 0, upstream_fetch_count INTEGER NOT NULL DEFAULT 0, upstream_fetch_bytes INTEGER NOT NULL DEFAULT 0, transform_count INTEGER NOT NULL DEFAULT 0, last_updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); INSERT OR IGNORE INTO cache_stats (id) VALUES (1);