- Fix 71 invalid-syntax files (class-body newline-broken assignments) - Add from/None chain to 307 B904 raise-without-from sites - Add B008 ignore to ruff.toml (already in pyproject.toml) - Noqa F401 on __init__.py re-exports (137 sites) - Noqa E402 on deferred imports (63 sites) - Bulk-add stdlib/FastAPI/project imports for F821 (127 sites) - Replace ×→x, –→-, …→... in docstrings (4093 chars) - Manual refactor of 5 SIM103/SIM116 patterns Tests: 791 passed (66 deselected due to pre-existing Redis issues in test_rag.py) Co-authored-by: opencode <opencode@rugmunch.io>
175 lines
6 KiB
Python
175 lines
6 KiB
Python
"""Catalog database schema - initial migration.
|
|
|
|
Creates the tables referenced by the v4.0 catalog models.
|
|
Idempotent: safe to run multiple times.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import os
|
|
|
|
import asyncpg
|
|
|
|
from app.core.logging import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
SCHEMA = """
|
|
-- Tokens (T27)
|
|
CREATE TABLE IF NOT EXISTS tokens (
|
|
token_id TEXT PRIMARY KEY,
|
|
chain TEXT NOT NULL,
|
|
address TEXT NOT NULL,
|
|
symbol TEXT,
|
|
name TEXT,
|
|
decimals INT,
|
|
deployer_wallet_id TEXT,
|
|
deployed_at TIMESTAMPTZ NOT NULL,
|
|
initial_supply BIGINT,
|
|
current_supply BIGINT,
|
|
is_honeypot BOOLEAN,
|
|
is_mintable BOOLEAN,
|
|
is_proxy BOOLEAN,
|
|
tax_buy_bps INT,
|
|
tax_sell_bps INT,
|
|
risk_tier TEXT,
|
|
risk_score INT,
|
|
risk_factors TEXT[],
|
|
rag_embedding_id TEXT,
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_tokens_chain ON tokens(chain);
|
|
CREATE INDEX IF NOT EXISTS idx_tokens_deployer ON tokens(deployer_wallet_id);
|
|
CREATE INDEX IF NOT EXISTS idx_tokens_risk ON tokens(risk_score DESC) WHERE risk_score IS NOT NULL;
|
|
|
|
-- Alerts
|
|
CREATE TABLE IF NOT EXISTS alerts (
|
|
alert_id TEXT PRIMARY KEY,
|
|
token_id TEXT,
|
|
wallet_id TEXT,
|
|
chain TEXT,
|
|
alert_type TEXT NOT NULL,
|
|
severity TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
description TEXT,
|
|
evidence JSONB DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
resolved_at TIMESTAMPTZ
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_alerts_token ON alerts(token_id);
|
|
CREATE INDEX IF NOT EXISTS idx_alerts_wallet ON alerts(wallet_id);
|
|
CREATE INDEX IF NOT EXISTS idx_alerts_created ON alerts(created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_alerts_severity ON alerts(severity);
|
|
|
|
-- News items (T28)
|
|
CREATE TABLE IF NOT EXISTS news_items (
|
|
news_id TEXT PRIMARY KEY,
|
|
url TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
summary TEXT,
|
|
body_markdown TEXT,
|
|
source TEXT NOT NULL,
|
|
published_at TIMESTAMPTZ NOT NULL,
|
|
ingested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
chains_mentioned TEXT[] DEFAULT ARRAY[]::TEXT[],
|
|
tokens_mentioned TEXT[] DEFAULT ARRAY[]::TEXT[],
|
|
wallets_mentioned TEXT[] DEFAULT ARRAY[]::TEXT[],
|
|
sentiment_score REAL,
|
|
ai_analysis TEXT,
|
|
rag_embedding_id TEXT,
|
|
body_tsv tsvector
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_news_published ON news_items(published_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_news_source ON news_items(source);
|
|
CREATE INDEX IF NOT EXISTS idx_news_sentiment ON news_items(sentiment_score);
|
|
CREATE INDEX IF NOT EXISTS idx_news_body_tsv ON news_items USING gin(body_tsv);
|
|
|
|
-- Auto-update the body_tsv column on insert/update
|
|
CREATE OR REPLACE FUNCTION news_items_tsv_update() RETURNS trigger AS $$
|
|
BEGIN
|
|
NEW.body_tsv := to_tsvector('english', COALESCE(NEW.title, '') || ' ' ||
|
|
COALESCE(NEW.summary, '') || ' ' ||
|
|
COALESCE(NEW.body_markdown, ''));
|
|
RETURN NEW;
|
|
END
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
DROP TRIGGER IF EXISTS news_items_tsv_trigger ON news_items;
|
|
CREATE TRIGGER news_items_tsv_trigger
|
|
BEFORE INSERT OR UPDATE ON news_items
|
|
FOR EACH ROW EXECUTE FUNCTION news_items_tsv_update();
|
|
|
|
-- Scan reports (T29)
|
|
CREATE TABLE IF NOT EXISTS scan_reports (
|
|
report_id TEXT PRIMARY KEY,
|
|
subject_type TEXT NOT NULL,
|
|
subject_id TEXT NOT NULL,
|
|
generated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
generated_by_model TEXT NOT NULL,
|
|
risk_score INT NOT NULL,
|
|
risk_tier TEXT NOT NULL,
|
|
sections JSONB DEFAULT '{}'::jsonb,
|
|
markdown_url TEXT,
|
|
paid_via_x402 TEXT
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_reports_subject ON scan_reports(subject_type, subject_id);
|
|
CREATE INDEX IF NOT EXISTS idx_reports_generated ON scan_reports(generated_at DESC);
|
|
|
|
-- RAG findings metadata (Qdrant has the vectors; this is metadata for query)
|
|
CREATE TABLE IF NOT EXISTS rag_findings (
|
|
finding_id TEXT PRIMARY KEY,
|
|
source_type TEXT NOT NULL,
|
|
source_url TEXT,
|
|
source_token_id TEXT,
|
|
source_wallet_id TEXT,
|
|
claim TEXT NOT NULL,
|
|
confidence REAL NOT NULL,
|
|
extracted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
qdrant_point_id TEXT NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_findings_token ON rag_findings(source_token_id);
|
|
CREATE INDEX IF NOT EXISTS idx_findings_wallet ON rag_findings(source_wallet_id);
|
|
|
|
-- x402 receipts (T34)
|
|
CREATE TABLE IF NOT EXISTS x402_receipts (
|
|
tx_hash TEXT PRIMARY KEY,
|
|
agent_id TEXT,
|
|
tool TEXT NOT NULL,
|
|
amount_usd REAL NOT NULL,
|
|
chain TEXT,
|
|
paid_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
tier TEXT
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_x402_paid_at ON x402_receipts(paid_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_x402_tool ON x402_receipts(tool);
|
|
CREATE INDEX IF NOT EXISTS idx_x402_agent ON x402_receipts(agent_id);
|
|
"""
|
|
|
|
|
|
async def main():
|
|
"""Run the schema migration."""
|
|
cfg = {
|
|
"host": os.getenv("POSTGRES_HOST", "rmi-postgres"),
|
|
"port": int(os.getenv("POSTGRES_PORT", "5432")),
|
|
"user": os.getenv("POSTGRES_USER", "rmi"),
|
|
"password": os.getenv("POSTGRES_PASSWORD", "RMI_PROD_POSTGRES_2026"),
|
|
"database": os.getenv("POSTGRES_DB", "rmi"),
|
|
}
|
|
logger.info(f"Connecting to postgres at {cfg['host']}:{cfg['port']} db={cfg['database']}")
|
|
conn = await asyncpg.connect(**cfg)
|
|
try:
|
|
await conn.execute(SCHEMA)
|
|
logger.info("✓ Schema applied")
|
|
# Verify
|
|
tables = await conn.fetch(
|
|
"SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename"
|
|
)
|
|
logger.info(f"Tables ({len(tables)}):")
|
|
for t in tables:
|
|
logger.info(f" {t['tablename']}")
|
|
finally:
|
|
await conn.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|