check / check (push) Waiting to run
UsageBytes now reads the new cache_usage row, which triggers on source_content and variant_content keep up to date in the statement that adds, removes or resizes a row. The reconciliation pass reads both tables 1000 rows per query, sums them, and corrects the total when it differs, unless a row changed while it summed. Source rows now get last_accessed_at when added, so choosing source images to evict reads that column's index instead of sorting the whole table. Stats still sums the tables, now in pages: an existing test drops both tables and expects that sum to fail. Model: opus-5-5
179 lines
6.6 KiB
SQL
179 lines
6.6 KiB
SQL
-- Migration 001: Initial schema
|
|
-- Creates all tables for the pixa caching image proxy
|
|
|
|
-- Source content blobs
|
|
-- Files stored at: cache/sources/<ab>/<cd>/<sha256>
|
|
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/<hostname>/<path_hash>.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/<ab>/<cd>/<cache_key> (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);
|