532 lines
21 KiB
C
532 lines
21 KiB
C
#ifndef PG_SCHEMA_H
|
|
#define PG_SCHEMA_H
|
|
|
|
#define EMBEDDED_PG_SCHEMA_VERSION "5"
|
|
|
|
static const char* const EMBEDDED_PG_SCHEMA_SQL =
|
|
"-- C-Relay-PG PostgreSQL Schema\n"
|
|
"-- Initial PostgreSQL backend schema\n"
|
|
"\n"
|
|
"BEGIN;\n"
|
|
"\n"
|
|
"CREATE TABLE IF NOT EXISTS schema_info (\n"
|
|
" key TEXT PRIMARY KEY,\n"
|
|
" value TEXT NOT NULL,\n"
|
|
" updated_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT\n"
|
|
");\n"
|
|
"\n"
|
|
"CREATE TABLE IF NOT EXISTS events (\n"
|
|
" id TEXT PRIMARY KEY,\n"
|
|
" pubkey TEXT NOT NULL,\n"
|
|
" created_at BIGINT NOT NULL,\n"
|
|
" kind INTEGER NOT NULL,\n"
|
|
" event_type TEXT NOT NULL CHECK (event_type IN ('regular', 'replaceable', 'ephemeral', 'addressable')),\n"
|
|
" content TEXT NOT NULL,\n"
|
|
" sig TEXT NOT NULL,\n"
|
|
" tags JSONB NOT NULL DEFAULT '[]'::jsonb,\n"
|
|
" d_tag_value TEXT NOT NULL DEFAULT '',\n"
|
|
" expires_at BIGINT,\n"
|
|
" event_json TEXT NOT NULL,\n"
|
|
" first_seen BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT\n"
|
|
");\n"
|
|
"\n"
|
|
"ALTER TABLE events\n"
|
|
" ADD COLUMN IF NOT EXISTS expires_at BIGINT;\n"
|
|
"\n"
|
|
"DO $$\n"
|
|
"BEGIN\n"
|
|
" IF EXISTS (\n"
|
|
" SELECT 1\n"
|
|
" FROM information_schema.columns\n"
|
|
" WHERE table_schema = current_schema()\n"
|
|
" AND table_name = 'events'\n"
|
|
" AND column_name = 'd_tag_value'\n"
|
|
" AND is_generated = 'ALWAYS'\n"
|
|
" ) THEN\n"
|
|
" DROP INDEX IF EXISTS uq_events_addressable_pubkey_kind_dtag;\n"
|
|
" DROP INDEX IF EXISTS idx_events_d_tag_value;\n"
|
|
" ALTER TABLE events DROP COLUMN d_tag_value;\n"
|
|
" ALTER TABLE events ADD COLUMN d_tag_value TEXT NOT NULL DEFAULT '';\n"
|
|
" ELSIF NOT EXISTS (\n"
|
|
" SELECT 1\n"
|
|
" FROM information_schema.columns\n"
|
|
" WHERE table_schema = current_schema()\n"
|
|
" AND table_name = 'events'\n"
|
|
" AND column_name = 'd_tag_value'\n"
|
|
" ) THEN\n"
|
|
" ALTER TABLE events ADD COLUMN d_tag_value TEXT NOT NULL DEFAULT '';\n"
|
|
" END IF;\n"
|
|
"\n"
|
|
" -- One-time backfill of d_tag_value from tags JSONB.\n"
|
|
" -- Guarded by schema_info version check so it only runs once on upgrade\n"
|
|
" -- from v3, NOT on every startup (avoids full-table scan + write on boot).\n"
|
|
" IF COALESCE((SELECT value FROM schema_info WHERE key = 'version'), '0') < '4' THEN\n"
|
|
" UPDATE events e\n"
|
|
" SET d_tag_value = COALESCE((\n"
|
|
" SELECT tag->>1\n"
|
|
" FROM jsonb_array_elements(e.tags) AS tag\n"
|
|
" WHERE jsonb_typeof(tag) = 'array'\n"
|
|
" AND jsonb_array_length(tag) >= 2\n"
|
|
" AND tag->>0 = 'd'\n"
|
|
" LIMIT 1\n"
|
|
" ), '')\n"
|
|
" WHERE COALESCE(e.d_tag_value, '') = '';\n"
|
|
" END IF;\n"
|
|
"END\n"
|
|
"$$;\n"
|
|
"\n"
|
|
"CREATE INDEX IF NOT EXISTS idx_events_pubkey ON events(pubkey);\n"
|
|
"CREATE INDEX IF NOT EXISTS idx_events_kind ON events(kind);\n"
|
|
"CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at DESC);\n"
|
|
"CREATE INDEX IF NOT EXISTS idx_events_event_type ON events(event_type);\n"
|
|
"CREATE INDEX IF NOT EXISTS idx_events_kind_created_at ON events(kind, created_at DESC);\n"
|
|
"CREATE INDEX IF NOT EXISTS idx_events_pubkey_created_at ON events(pubkey, created_at DESC);\n"
|
|
"CREATE INDEX IF NOT EXISTS idx_events_pubkey_kind ON events(pubkey, kind);\n"
|
|
"CREATE INDEX IF NOT EXISTS idx_events_tags_gin ON events USING GIN(tags);\n"
|
|
"CREATE INDEX IF NOT EXISTS idx_events_expires_at ON events(expires_at DESC) WHERE expires_at IS NOT NULL;\n"
|
|
"CREATE INDEX IF NOT EXISTS idx_events_d_tag_value ON events(d_tag_value) WHERE d_tag_value <> '';\n"
|
|
"CREATE UNIQUE INDEX IF NOT EXISTS uq_events_replaceable_pubkey_kind\n"
|
|
" ON events(pubkey, kind)\n"
|
|
" WHERE event_type = 'replaceable';\n"
|
|
"CREATE UNIQUE INDEX IF NOT EXISTS uq_events_addressable_pubkey_kind_dtag\n"
|
|
" ON events(pubkey, kind, d_tag_value)\n"
|
|
" WHERE event_type = 'addressable';\n"
|
|
"\n"
|
|
"-- Keep this table for compatibility with existing query paths during migration.\n"
|
|
"CREATE TABLE IF NOT EXISTS event_tags (\n"
|
|
" event_id TEXT NOT NULL REFERENCES events(id) ON DELETE CASCADE,\n"
|
|
" tag_name TEXT NOT NULL,\n"
|
|
" tag_value TEXT NOT NULL,\n"
|
|
" tag_index INTEGER NOT NULL DEFAULT 0,\n"
|
|
" PRIMARY KEY (event_id, tag_name, tag_value, tag_index)\n"
|
|
");\n"
|
|
"\n"
|
|
"CREATE INDEX IF NOT EXISTS idx_event_tags_lookup ON event_tags(tag_name, tag_value);\n"
|
|
"CREATE INDEX IF NOT EXISTS idx_event_tags_event ON event_tags(event_id);\n"
|
|
"CREATE INDEX IF NOT EXISTS idx_event_tags_value_name ON event_tags(tag_value, tag_name);\n"
|
|
"\n"
|
|
"CREATE OR REPLACE FUNCTION set_event_derived_fields()\n"
|
|
"RETURNS TRIGGER AS $$\n"
|
|
"DECLARE\n"
|
|
" expiration_value TEXT;\n"
|
|
" d_value TEXT;\n"
|
|
"BEGIN\n"
|
|
" NEW.expires_at := NULL;\n"
|
|
" NEW.d_tag_value := '';\n"
|
|
"\n"
|
|
" SELECT tag->>1\n"
|
|
" INTO expiration_value\n"
|
|
" FROM jsonb_array_elements(NEW.tags) AS tag\n"
|
|
" WHERE jsonb_typeof(tag) = 'array'\n"
|
|
" AND jsonb_array_length(tag) >= 2\n"
|
|
" AND tag->>0 = 'expiration'\n"
|
|
" LIMIT 1;\n"
|
|
"\n"
|
|
" IF expiration_value IS NOT NULL AND expiration_value ~ '^[0-9]+$' THEN\n"
|
|
" NEW.expires_at := expiration_value::BIGINT;\n"
|
|
" END IF;\n"
|
|
"\n"
|
|
"\n"
|
|
" SELECT tag->>1\n"
|
|
" INTO d_value\n"
|
|
" FROM jsonb_array_elements(NEW.tags) AS tag\n"
|
|
" WHERE jsonb_typeof(tag) = 'array'\n"
|
|
" AND jsonb_array_length(tag) >= 2\n"
|
|
" AND tag->>0 = 'd'\n"
|
|
" LIMIT 1;\n"
|
|
"\n"
|
|
" IF d_value IS NOT NULL THEN\n"
|
|
" NEW.d_tag_value := d_value;\n"
|
|
" END IF;\n"
|
|
"\n"
|
|
" RETURN NEW;\n"
|
|
"END;\n"
|
|
"$$ LANGUAGE plpgsql;\n"
|
|
"\n"
|
|
"CREATE OR REPLACE FUNCTION sync_event_tags_from_events()\n"
|
|
"RETURNS TRIGGER AS $$\n"
|
|
"BEGIN\n"
|
|
" DELETE FROM event_tags WHERE event_id = NEW.id;\n"
|
|
"\n"
|
|
" INSERT INTO event_tags (event_id, tag_name, tag_value, tag_index)\n"
|
|
" SELECT NEW.id,\n"
|
|
" tag->>0 AS tag_name,\n"
|
|
" tag->>1 AS tag_value,\n"
|
|
" (ord - 1)::INTEGER AS tag_index\n"
|
|
" FROM jsonb_array_elements(NEW.tags) WITH ORDINALITY AS expanded(tag, ord)\n"
|
|
" WHERE jsonb_typeof(tag) = 'array'\n"
|
|
" AND jsonb_array_length(tag) >= 2\n"
|
|
" AND COALESCE(tag->>0, '') <> ''\n"
|
|
" AND COALESCE(tag->>1, '') <> '';\n"
|
|
"\n"
|
|
" RETURN NEW;\n"
|
|
"END;\n"
|
|
"$$ LANGUAGE plpgsql;\n"
|
|
"\n"
|
|
"DROP TRIGGER IF EXISTS trg_events_set_derived_fields ON events;\n"
|
|
"CREATE TRIGGER trg_events_set_derived_fields\n"
|
|
"BEFORE INSERT OR UPDATE OF tags ON events\n"
|
|
"FOR EACH ROW\n"
|
|
"EXECUTE FUNCTION set_event_derived_fields();\n"
|
|
"\n"
|
|
"DROP TRIGGER IF EXISTS trg_events_sync_event_tags ON events;\n"
|
|
"CREATE TRIGGER trg_events_sync_event_tags\n"
|
|
"AFTER INSERT OR UPDATE OF tags ON events\n"
|
|
"FOR EACH ROW\n"
|
|
"EXECUTE FUNCTION sync_event_tags_from_events();\n"
|
|
"\n"
|
|
"CREATE TABLE IF NOT EXISTS relay_seckey (\n"
|
|
" id SMALLINT PRIMARY KEY DEFAULT 1,\n"
|
|
" private_key_hex TEXT NOT NULL CHECK (char_length(private_key_hex) = 64),\n"
|
|
" created_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT,\n"
|
|
" CONSTRAINT relay_seckey_singleton CHECK (id = 1)\n"
|
|
");\n"
|
|
"\n"
|
|
"CREATE TABLE IF NOT EXISTS auth_rules (\n"
|
|
" id BIGSERIAL PRIMARY KEY,\n"
|
|
" rule_type TEXT NOT NULL CHECK (rule_type IN ('whitelist', 'blacklist', 'rate_limit', 'auth_required', 'wot_whitelist')),\n"
|
|
" pattern_type TEXT NOT NULL CHECK (pattern_type IN ('pubkey', 'kind', 'ip', 'global', 'event_id', 'content', 'hash')),\n"
|
|
" pattern_value TEXT,\n"
|
|
" active INTEGER NOT NULL DEFAULT 1,\n"
|
|
" created_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT,\n"
|
|
" updated_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT\n"
|
|
");\n"
|
|
"\n"
|
|
"CREATE INDEX IF NOT EXISTS idx_auth_rules_pattern ON auth_rules(pattern_type, pattern_value);\n"
|
|
"CREATE INDEX IF NOT EXISTS idx_auth_rules_type ON auth_rules(rule_type);\n"
|
|
"CREATE INDEX IF NOT EXISTS idx_auth_rules_active ON auth_rules(active);\n"
|
|
"\n"
|
|
"CREATE TABLE IF NOT EXISTS config (\n"
|
|
" key TEXT PRIMARY KEY,\n"
|
|
" value TEXT NOT NULL,\n"
|
|
" data_type TEXT NOT NULL CHECK (data_type IN ('string', 'integer', 'boolean', 'json')),\n"
|
|
" description TEXT,\n"
|
|
" category TEXT DEFAULT 'general',\n"
|
|
" requires_restart INTEGER DEFAULT 0,\n"
|
|
" created_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT,\n"
|
|
" updated_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT\n"
|
|
");\n"
|
|
"\n"
|
|
"CREATE INDEX IF NOT EXISTS idx_config_category ON config(category);\n"
|
|
"CREATE INDEX IF NOT EXISTS idx_config_restart ON config(requires_restart);\n"
|
|
"CREATE INDEX IF NOT EXISTS idx_config_updated ON config(updated_at DESC);\n"
|
|
"\n"
|
|
"CREATE TABLE IF NOT EXISTS subscriptions (\n"
|
|
" id BIGSERIAL PRIMARY KEY,\n"
|
|
" subscription_id TEXT NOT NULL,\n"
|
|
" wsi_pointer TEXT NOT NULL,\n"
|
|
" client_ip TEXT NOT NULL,\n"
|
|
" event_type TEXT NOT NULL CHECK (event_type IN ('created', 'closed', 'expired', 'disconnected')),\n"
|
|
" filter_json TEXT,\n"
|
|
" events_sent INTEGER DEFAULT 0,\n"
|
|
" created_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT,\n"
|
|
" ended_at BIGINT,\n"
|
|
" duration BIGINT,\n"
|
|
" UNIQUE(subscription_id, wsi_pointer)\n"
|
|
");\n"
|
|
"\n"
|
|
"CREATE TABLE IF NOT EXISTS subscription_metrics (\n"
|
|
" id BIGSERIAL PRIMARY KEY,\n"
|
|
" date TEXT NOT NULL UNIQUE,\n"
|
|
" total_created INTEGER DEFAULT 0,\n"
|
|
" total_closed INTEGER DEFAULT 0,\n"
|
|
" total_events_broadcast INTEGER DEFAULT 0,\n"
|
|
" avg_duration DOUBLE PRECISION DEFAULT 0,\n"
|
|
" peak_concurrent INTEGER DEFAULT 0,\n"
|
|
" updated_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT\n"
|
|
");\n"
|
|
"\n"
|
|
"CREATE INDEX IF NOT EXISTS idx_subscriptions_id ON subscriptions(subscription_id);\n"
|
|
"CREATE INDEX IF NOT EXISTS idx_subscriptions_type ON subscriptions(event_type);\n"
|
|
"CREATE INDEX IF NOT EXISTS idx_subscriptions_created ON subscriptions(created_at DESC);\n"
|
|
"CREATE INDEX IF NOT EXISTS idx_subscriptions_client ON subscriptions(client_ip);\n"
|
|
"CREATE INDEX IF NOT EXISTS idx_subscriptions_wsi ON subscriptions(wsi_pointer);\n"
|
|
"CREATE INDEX IF NOT EXISTS idx_subscriptions_active_log ON subscriptions(event_type, ended_at, created_at DESC);\n"
|
|
"CREATE INDEX IF NOT EXISTS idx_subscription_metrics_date ON subscription_metrics(date DESC);\n"
|
|
"\n"
|
|
"CREATE TABLE IF NOT EXISTS ip_bans (\n"
|
|
" ip TEXT PRIMARY KEY,\n"
|
|
" failure_count INTEGER NOT NULL DEFAULT 0,\n"
|
|
" ban_count INTEGER NOT NULL DEFAULT 0,\n"
|
|
" banned_until BIGINT NOT NULL DEFAULT 0,\n"
|
|
" first_failure BIGINT NOT NULL DEFAULT 0,\n"
|
|
" has_authed_successfully INTEGER NOT NULL DEFAULT 0,\n"
|
|
" last_success_at BIGINT NOT NULL DEFAULT 0,\n"
|
|
" total_connections INTEGER NOT NULL DEFAULT 0,\n"
|
|
" total_failures INTEGER NOT NULL DEFAULT 0,\n"
|
|
" total_successes INTEGER NOT NULL DEFAULT 0,\n"
|
|
" first_seen BIGINT NOT NULL DEFAULT 0,\n"
|
|
" updated_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT,\n"
|
|
" idle_failure_count INTEGER NOT NULL DEFAULT 0,\n"
|
|
" idle_ban_count INTEGER NOT NULL DEFAULT 0,\n"
|
|
" idle_banned_until BIGINT NOT NULL DEFAULT 0,\n"
|
|
" idle_first_failure BIGINT NOT NULL DEFAULT 0\n"
|
|
");\n"
|
|
"\n"
|
|
"-- Helper views for common queries\n"
|
|
"CREATE OR REPLACE VIEW recent_events AS\n"
|
|
"SELECT id, pubkey, created_at, kind, event_type, content\n"
|
|
"FROM events\n"
|
|
"WHERE event_type <> 'ephemeral'\n"
|
|
"ORDER BY created_at DESC\n"
|
|
"LIMIT 1000;\n"
|
|
"\n"
|
|
"CREATE OR REPLACE VIEW event_stats AS\n"
|
|
"SELECT\n"
|
|
" event_type,\n"
|
|
" COUNT(*) AS count,\n"
|
|
" AVG(char_length(content)) AS avg_content_length,\n"
|
|
" MIN(created_at) AS earliest,\n"
|
|
" MAX(created_at) AS latest\n"
|
|
"FROM events\n"
|
|
"GROUP BY event_type;\n"
|
|
"\n"
|
|
"-- Configuration events view (kind 33334)\n"
|
|
"CREATE OR REPLACE VIEW configuration_events AS\n"
|
|
"SELECT\n"
|
|
" id,\n"
|
|
" pubkey AS admin_pubkey,\n"
|
|
" created_at,\n"
|
|
" content,\n"
|
|
" tags,\n"
|
|
" sig\n"
|
|
"FROM events\n"
|
|
"WHERE kind = 33334\n"
|
|
"ORDER BY created_at DESC;\n"
|
|
"\n"
|
|
"-- View for subscription analytics\n"
|
|
"CREATE OR REPLACE VIEW subscription_analytics AS\n"
|
|
"SELECT\n"
|
|
" to_timestamp(created_at)::date AS date,\n"
|
|
" COUNT(*) AS subscriptions_created,\n"
|
|
" COUNT(CASE WHEN ended_at IS NOT NULL THEN 1 END) AS subscriptions_ended,\n"
|
|
" AVG(CASE WHEN duration IS NOT NULL THEN duration END) AS avg_duration_seconds,\n"
|
|
" MAX(events_sent) AS max_events_sent,\n"
|
|
" AVG(events_sent) AS avg_events_sent,\n"
|
|
" COUNT(DISTINCT client_ip) AS unique_clients\n"
|
|
"FROM subscriptions\n"
|
|
"GROUP BY to_timestamp(created_at)::date\n"
|
|
"ORDER BY date DESC;\n"
|
|
"\n"
|
|
"-- View for current active subscriptions (from log perspective)\n"
|
|
"CREATE OR REPLACE VIEW active_subscriptions_log AS\n"
|
|
"SELECT\n"
|
|
" subscription_id,\n"
|
|
" client_ip,\n"
|
|
" filter_json,\n"
|
|
" events_sent,\n"
|
|
" created_at,\n"
|
|
" (EXTRACT(EPOCH FROM NOW())::BIGINT - created_at) AS duration_seconds,\n"
|
|
" wsi_pointer\n"
|
|
"FROM subscriptions\n"
|
|
"WHERE event_type = 'created'\n"
|
|
" AND ended_at IS NULL;\n"
|
|
"\n"
|
|
"-- Event kinds distribution view\n"
|
|
"CREATE OR REPLACE VIEW event_kinds_view AS\n"
|
|
"SELECT\n"
|
|
" kind,\n"
|
|
" COUNT(*) AS count,\n"
|
|
" ROUND((COUNT(*) * 100.0 / NULLIF((SELECT COUNT(*) FROM events), 0))::numeric, 2) AS percentage\n"
|
|
"FROM events\n"
|
|
"GROUP BY kind\n"
|
|
"ORDER BY count DESC;\n"
|
|
"\n"
|
|
"-- Top pubkeys by event count view\n"
|
|
"CREATE OR REPLACE VIEW top_pubkeys_view AS\n"
|
|
"SELECT\n"
|
|
" pubkey,\n"
|
|
" COUNT(*) AS event_count,\n"
|
|
" ROUND((COUNT(*) * 100.0 / NULLIF((SELECT COUNT(*) FROM events), 0))::numeric, 2) AS percentage\n"
|
|
"FROM events\n"
|
|
"GROUP BY pubkey\n"
|
|
"ORDER BY event_count DESC;\n"
|
|
"\n"
|
|
"-- Time-based statistics view\n"
|
|
"CREATE OR REPLACE VIEW time_stats_view AS\n"
|
|
"SELECT\n"
|
|
" 'total' AS period,\n"
|
|
" COUNT(*) AS total_events,\n"
|
|
" COUNT(DISTINCT pubkey) AS unique_pubkeys,\n"
|
|
" MIN(created_at) AS oldest_event,\n"
|
|
" MAX(created_at) AS newest_event\n"
|
|
"FROM events\n"
|
|
"UNION ALL\n"
|
|
"SELECT\n"
|
|
" '24h' AS period,\n"
|
|
" COUNT(*) AS total_events,\n"
|
|
" COUNT(DISTINCT pubkey) AS unique_pubkeys,\n"
|
|
" MIN(created_at) AS oldest_event,\n"
|
|
" MAX(created_at) AS newest_event\n"
|
|
"FROM events\n"
|
|
"WHERE created_at >= (EXTRACT(EPOCH FROM NOW())::BIGINT - 86400)\n"
|
|
"UNION ALL\n"
|
|
"SELECT\n"
|
|
" '7d' AS period,\n"
|
|
" COUNT(*) AS total_events,\n"
|
|
" COUNT(DISTINCT pubkey) AS unique_pubkeys,\n"
|
|
" MIN(created_at) AS oldest_event,\n"
|
|
" MAX(created_at) AS newest_event\n"
|
|
"FROM events\n"
|
|
"WHERE created_at >= (EXTRACT(EPOCH FROM NOW())::BIGINT - 604800)\n"
|
|
"UNION ALL\n"
|
|
"SELECT\n"
|
|
" '30d' AS period,\n"
|
|
" COUNT(*) AS total_events,\n"
|
|
" COUNT(DISTINCT pubkey) AS unique_pubkeys,\n"
|
|
" MIN(created_at) AS oldest_event,\n"
|
|
" MAX(created_at) AS newest_event\n"
|
|
"FROM events\n"
|
|
"WHERE created_at >= (EXTRACT(EPOCH FROM NOW())::BIGINT - 2592000);\n"
|
|
"\n"
|
|
"INSERT INTO schema_info(key, value, updated_at)\n"
|
|
"VALUES ('version', '5', EXTRACT(EPOCH FROM NOW())::BIGINT)\n"
|
|
"ON CONFLICT (key) DO UPDATE SET\n"
|
|
" value = EXCLUDED.value,\n"
|
|
" updated_at = EXCLUDED.updated_at;\n"
|
|
"\n"
|
|
"-- =====================================================================\n"
|
|
"-- Caching relay integration tables\n"
|
|
"-- These tables are written by an external caching application and\n"
|
|
"-- consumed by c-relay-pg. They are schema-only here; the database\n"
|
|
"-- abstraction functions are added in a later phase.\n"
|
|
"-- =====================================================================\n"
|
|
"\n"
|
|
"-- Queue table where the external caching application inserts raw event\n"
|
|
"-- JSON. c-relay-pg destructively dequeues from it.\n"
|
|
"CREATE TABLE IF NOT EXISTS caching_event_inbox (\n"
|
|
" queue_id BIGSERIAL PRIMARY KEY,\n"
|
|
" event_id TEXT NOT NULL UNIQUE,\n"
|
|
" event_json JSONB NOT NULL,\n"
|
|
" source_relay TEXT,\n"
|
|
" source_class TEXT NOT NULL DEFAULT 'backfill',\n"
|
|
" priority SMALLINT NOT NULL DEFAULT 1,\n"
|
|
" received_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT,\n"
|
|
"\n"
|
|
" CHECK (jsonb_typeof(event_json) = 'object'),\n"
|
|
" CHECK (jsonb_typeof(event_json->'id') = 'string'),\n"
|
|
" CHECK (length(event_json->>'id') = 64),\n"
|
|
" CHECK (event_id = event_json->>'id'),\n"
|
|
" CHECK (jsonb_typeof(event_json->'pubkey') = 'string'),\n"
|
|
" CHECK (length(event_json->>'pubkey') = 64),\n"
|
|
" CHECK (jsonb_typeof(event_json->'sig') = 'string'),\n"
|
|
" CHECK (length(event_json->>'sig') = 128),\n"
|
|
" CHECK (jsonb_typeof(event_json->'created_at') = 'number'),\n"
|
|
" CHECK (jsonb_typeof(event_json->'kind') = 'number'),\n"
|
|
" CHECK (jsonb_typeof(event_json->'tags') = 'array'),\n"
|
|
" CHECK (jsonb_typeof(event_json->'content') = 'string'),\n"
|
|
" CHECK (source_class IN ('live', 'discovery', 'backfill')),\n"
|
|
" CHECK (priority IN (0, 1))\n"
|
|
");\n"
|
|
"\n"
|
|
"CREATE INDEX IF NOT EXISTS idx_caching_inbox_dequeue\n"
|
|
" ON caching_event_inbox(priority, received_at, queue_id);\n"
|
|
"\n"
|
|
"-- Singleton status row for the external caching application. The caching\n"
|
|
"-- process overwrites this periodically.\n"
|
|
"CREATE TABLE IF NOT EXISTS caching_service_state (\n"
|
|
" id SMALLINT PRIMARY KEY DEFAULT 1,\n"
|
|
" service_version TEXT,\n"
|
|
" service_state TEXT NOT NULL DEFAULT 'stopped',\n"
|
|
" config_generation BIGINT NOT NULL DEFAULT 0,\n"
|
|
" heartbeat_at BIGINT NOT NULL DEFAULT 0,\n"
|
|
" followed_author_count INTEGER NOT NULL DEFAULT 0,\n"
|
|
" selected_relay_count INTEGER NOT NULL DEFAULT 0,\n"
|
|
" connected_relay_count INTEGER NOT NULL DEFAULT 0,\n"
|
|
" current_window_index INTEGER NOT NULL DEFAULT 0,\n"
|
|
" backfill_cursor INTEGER NOT NULL DEFAULT 0,\n"
|
|
" events_fetched BIGINT NOT NULL DEFAULT 0,\n"
|
|
" inbox_inserts BIGINT NOT NULL DEFAULT 0,\n"
|
|
" last_error TEXT,\n"
|
|
" last_error_at BIGINT,\n"
|
|
" updated_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT,\n"
|
|
"\n"
|
|
" CHECK (id = 1),\n"
|
|
" CHECK (service_state IN ('stopped', 'starting', 'running', 'degraded'))\n"
|
|
");\n"
|
|
"\n"
|
|
"-- Per-author backfill progress for the external caching application.\n"
|
|
"CREATE TABLE IF NOT EXISTS caching_backfill_progress (\n"
|
|
" author_pubkey TEXT NOT NULL,\n"
|
|
" window_index INTEGER NOT NULL,\n"
|
|
" window_anchor BIGINT NOT NULL,\n"
|
|
" until_cursor BIGINT NOT NULL DEFAULT 0,\n"
|
|
" complete BOOLEAN NOT NULL DEFAULT FALSE,\n"
|
|
" updated_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT,\n"
|
|
"\n"
|
|
" PRIMARY KEY (author_pubkey, window_index)\n"
|
|
");\n"
|
|
"\n"
|
|
"CREATE INDEX IF NOT EXISTS idx_caching_backfill_progress_window\n"
|
|
" ON caching_backfill_progress(window_index)\n"
|
|
" WHERE complete = FALSE;\n"
|
|
"\n"
|
|
"-- Durable followed-pubkey list with per-author backfill cursor state.\n"
|
|
"-- Replaces the window-coupled caching_backfill_progress table for the\n"
|
|
"-- per-author until-cursor drain backfill model.\n"
|
|
"CREATE TABLE IF NOT EXISTS caching_followed_pubkeys (\n"
|
|
" pubkey TEXT PRIMARY KEY,\n"
|
|
" is_root BOOLEAN NOT NULL DEFAULT FALSE,\n"
|
|
" until_cursor BIGINT NOT NULL DEFAULT 0,\n"
|
|
" backfill_complete BOOLEAN NOT NULL DEFAULT FALSE,\n"
|
|
" events_fetched INTEGER NOT NULL DEFAULT 0,\n"
|
|
" first_seen BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT,\n"
|
|
" last_seen BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT,\n"
|
|
" updated_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT\n"
|
|
");\n"
|
|
"\n"
|
|
"CREATE INDEX IF NOT EXISTS idx_caching_followed_incomplete\n"
|
|
" ON caching_followed_pubkeys(backfill_complete, last_seen)\n"
|
|
" WHERE backfill_complete = FALSE;\n"
|
|
"\n"
|
|
"-- Per-relay backfill cursor tracking for the relay-by-relay drain model.\n"
|
|
"-- Each (author, relay) pair has its own until_cursor and completion flag so\n"
|
|
"-- slow relays do not cause premature completion marking for the author.\n"
|
|
"CREATE TABLE IF NOT EXISTS caching_backfill_relay_progress (\n"
|
|
" author_pubkey TEXT NOT NULL,\n"
|
|
" relay_url TEXT NOT NULL,\n"
|
|
" until_cursor BIGINT NOT NULL DEFAULT 0,\n"
|
|
" complete BOOLEAN NOT NULL DEFAULT FALSE,\n"
|
|
" events_fetched INTEGER NOT NULL DEFAULT 0,\n"
|
|
" updated_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT,\n"
|
|
" PRIMARY KEY (author_pubkey, relay_url)\n"
|
|
");\n"
|
|
"\n"
|
|
"CREATE INDEX IF NOT EXISTS idx_caching_relay_progress_incomplete\n"
|
|
" ON caching_backfill_relay_progress(author_pubkey)\n"
|
|
" WHERE complete = FALSE;\n"
|
|
"\n"
|
|
"-- Replace window-based progress columns with author-based progress.\n"
|
|
"ALTER TABLE caching_service_state\n"
|
|
" DROP COLUMN IF EXISTS current_window_index,\n"
|
|
" DROP COLUMN IF EXISTS backfill_cursor,\n"
|
|
" ADD COLUMN IF NOT EXISTS backfill_authors_complete INTEGER NOT NULL DEFAULT 0,\n"
|
|
" ADD COLUMN IF NOT EXISTS backfill_authors_total INTEGER NOT NULL DEFAULT 0;\n"
|
|
"\n"
|
|
"-- =====================================================================\n"
|
|
"-- LISTEN/NOTIFY support for api-worker monitoring (Phase 5)\n"
|
|
"-- Fires a notification on the 'event_stored' channel whenever a new event\n"
|
|
"-- is inserted, so the api-worker thread can wake reactively instead of\n"
|
|
"-- polling on a timer. The payload is a small JSON object with kind and a\n"
|
|
"-- truncated pubkey prefix (kept tiny to minimize per-insert overhead).\n"
|
|
"-- =====================================================================\n"
|
|
"CREATE OR REPLACE FUNCTION notify_event_stored() RETURNS trigger AS $$\n"
|
|
"BEGIN\n"
|
|
" PERFORM pg_notify('event_stored', json_build_object(\n"
|
|
" 'kind', NEW.kind,\n"
|
|
" 'pubkey', substring(NEW.pubkey, 1, 8)\n"
|
|
" )::text);\n"
|
|
" RETURN NEW;\n"
|
|
"END;\n"
|
|
"$$ LANGUAGE plpgsql;\n"
|
|
"\n"
|
|
"DROP TRIGGER IF EXISTS trg_notify_event_stored ON events;\n"
|
|
"CREATE TRIGGER trg_notify_event_stored\n"
|
|
" AFTER INSERT ON events\n"
|
|
" FOR EACH ROW EXECUTE FUNCTION notify_event_stored();\n"
|
|
"\n"
|
|
"COMMIT;\n"
|
|
;
|
|
|
|
#endif // PG_SCHEMA_H
|