2026-09-02 17:38:50 +02:00
|
|
|
PRAGMA foreign_keys = ON;
|
2026-09-02 17:45:57 +02:00
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS organizations (
|
2026-09-02 17:57:17 +02:00
|
|
|
id TEXT PRIMARY KEY, name TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
2026-09-02 17:45:57 +02:00
|
|
|
);
|
|
|
|
|
CREATE TABLE IF NOT EXISTS users (
|
2026-09-02 17:57:17 +02:00
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, organization_id TEXT NOT NULL REFERENCES organizations(id), email TEXT NOT NULL UNIQUE,
|
|
|
|
|
password_hash TEXT NOT NULL, password_salt TEXT NOT NULL, role TEXT NOT NULL CHECK(role IN ('viewer','owner','admin','researcher')),
|
2026-09-02 17:45:57 +02:00
|
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
|
|
|
);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_users_org ON users(organization_id);
|
|
|
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
2026-09-02 17:57:17 +02:00
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
|
|
|
token_hash TEXT NOT NULL UNIQUE, expires_at TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
2026-09-02 17:45:57 +02:00
|
|
|
);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(token_hash);
|
|
|
|
|
CREATE TABLE IF NOT EXISTS audit_log (
|
2026-09-02 17:57:17 +02:00
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, organization_id TEXT REFERENCES organizations(id), user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
|
|
|
|
action TEXT NOT NULL, details TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
2026-09-02 17:45:57 +02:00
|
|
|
);
|
2026-09-02 17:57:17 +02:00
|
|
|
CREATE INDEX IF NOT EXISTS idx_audit_org ON audit_log(organization_id,created_at);
|
2026-09-02 17:38:50 +02:00
|
|
|
CREATE TABLE IF NOT EXISTS businesses (
|
2026-09-02 17:57:17 +02:00
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, organization_id TEXT NOT NULL, name TEXT NOT NULL, website TEXT NOT NULL DEFAULT '', website_domain TEXT NOT NULL DEFAULT '',
|
|
|
|
|
email TEXT NOT NULL DEFAULT '', phone TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '', score INTEGER NOT NULL DEFAULT 0,
|
|
|
|
|
score_version TEXT NOT NULL DEFAULT 'mvp-1', score_factors TEXT NOT NULL DEFAULT '[]', website_class TEXT NOT NULL DEFAULT 'missing',
|
|
|
|
|
verified INTEGER NOT NULL DEFAULT 0, verified_at TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
2026-09-02 17:38:50 +02:00
|
|
|
);
|
2026-09-03 08:46:22 +02:00
|
|
|
-- Phase 6 normalized location and reversible merge ledger.
|
|
|
|
|
CREATE TABLE IF NOT EXISTS merge_history (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, organization_id TEXT NOT NULL REFERENCES organizations(id),
|
|
|
|
|
source_business_id INTEGER NOT NULL REFERENCES businesses(id), target_business_id INTEGER NOT NULL REFERENCES businesses(id),
|
|
|
|
|
source_snapshot_json TEXT NOT NULL, child_reassignment_json TEXT NOT NULL DEFAULT '{}', actor_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
|
|
|
|
reversible INTEGER NOT NULL DEFAULT 1, reversed_at TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
|
|
|
);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_merge_history_org ON merge_history(organization_id,created_at DESC,id DESC);
|
2026-09-02 17:38:50 +02:00
|
|
|
CREATE INDEX IF NOT EXISTS idx_businesses_org ON businesses(organization_id);
|
2026-09-02 17:57:17 +02:00
|
|
|
CREATE INDEX IF NOT EXISTS idx_businesses_score ON businesses(organization_id,score DESC,id);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_businesses_class ON businesses(organization_id,website_class);
|
2026-09-02 17:38:50 +02:00
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_business_domain ON businesses(organization_id, website_domain) WHERE website_domain <> '';
|
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_business_email ON businesses(organization_id, email) WHERE email <> '';
|
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_business_phone ON businesses(organization_id, phone) WHERE phone <> '';
|
|
|
|
|
CREATE TABLE IF NOT EXISTS suppressions (
|
2026-09-02 17:57:17 +02:00
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, organization_id TEXT NOT NULL, kind TEXT NOT NULL CHECK(kind IN ('email','domain','phone')),
|
|
|
|
|
value TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE(organization_id, kind, value)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS business_identifiers (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
|
|
|
|
|
organization_id TEXT NOT NULL REFERENCES organizations(id), kind TEXT NOT NULL, value TEXT NOT NULL,
|
|
|
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
|
|
|
);
|
|
|
|
|
CREATE TABLE IF NOT EXISTS contacts (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
|
|
|
|
|
organization_id TEXT NOT NULL REFERENCES organizations(id), name TEXT NOT NULL DEFAULT '', email TEXT NOT NULL DEFAULT '', phone TEXT NOT NULL DEFAULT '',
|
|
|
|
|
title TEXT NOT NULL DEFAULT '', do_not_contact INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
|
|
|
);
|
|
|
|
|
CREATE TABLE IF NOT EXISTS domains (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
|
|
|
|
|
organization_id TEXT NOT NULL REFERENCES organizations(id), domain TEXT NOT NULL, kind TEXT NOT NULL DEFAULT 'other', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
|
|
|
);
|
|
|
|
|
CREATE TABLE IF NOT EXISTS websites (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
|
|
|
|
|
organization_id TEXT NOT NULL REFERENCES organizations(id), url TEXT NOT NULL, website_class TEXT NOT NULL DEFAULT 'business_site', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
|
|
|
);
|
|
|
|
|
CREATE TABLE IF NOT EXISTS evidence (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
|
|
|
|
|
organization_id TEXT NOT NULL REFERENCES organizations(id), kind TEXT NOT NULL, url TEXT NOT NULL DEFAULT '', claim TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
|
|
|
);
|
|
|
|
|
CREATE TABLE IF NOT EXISTS pipeline_entries (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
|
|
|
|
|
organization_id TEXT NOT NULL REFERENCES organizations(id), stage TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'active', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
|
|
|
);
|
|
|
|
|
CREATE TABLE IF NOT EXISTS interactions (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
|
|
|
|
|
organization_id TEXT NOT NULL REFERENCES organizations(id), kind TEXT NOT NULL, body TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
|
|
|
);
|
|
|
|
|
CREATE TABLE IF NOT EXISTS notes (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
|
|
|
|
|
organization_id TEXT NOT NULL REFERENCES organizations(id), body TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
2026-09-02 17:38:50 +02:00
|
|
|
);
|
2026-09-02 17:57:17 +02:00
|
|
|
CREATE INDEX IF NOT EXISTS idx_contacts_business ON contacts(business_id);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_contacts_org_business ON contacts(organization_id,business_id);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_identifiers_business ON business_identifiers(business_id,kind);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_domains_business ON domains(business_id);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_websites_business ON websites(business_id);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_evidence_business ON evidence(business_id);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_pipeline_business ON pipeline_entries(business_id,created_at);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_pipeline_stage ON pipeline_entries(organization_id,stage);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_notes_business ON notes(business_id,created_at);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_interactions_business ON interactions(business_id,created_at);
|
2026-09-02 18:12:21 +02:00
|
|
|
|
|
|
|
|
-- Phase 4 durable background jobs (additive-safe for existing databases).
|
|
|
|
|
CREATE TABLE IF NOT EXISTS jobs (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
|
organization_id TEXT NOT NULL REFERENCES organizations(id),
|
|
|
|
|
idempotency_key TEXT NOT NULL,
|
|
|
|
|
type TEXT NOT NULL,
|
|
|
|
|
payload TEXT NOT NULL DEFAULT '{}',
|
|
|
|
|
status TEXT NOT NULL DEFAULT 'queued' CHECK(status IN ('queued','running','succeeded','failed','cancelled')),
|
|
|
|
|
attempts INTEGER NOT NULL DEFAULT 0,
|
|
|
|
|
max_attempts INTEGER NOT NULL DEFAULT 3,
|
|
|
|
|
progress INTEGER NOT NULL DEFAULT 0 CHECK(progress BETWEEN 0 AND 100),
|
|
|
|
|
error_code TEXT,
|
|
|
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
|
started_at TEXT,
|
|
|
|
|
completed_at TEXT,
|
|
|
|
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
|
UNIQUE(organization_id,idempotency_key)
|
|
|
|
|
);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_jobs_org_created ON jobs(organization_id,created_at DESC,id DESC);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_jobs_queue ON jobs(status,created_at,id);
|
|
|
|
|
CREATE TABLE IF NOT EXISTS job_events (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
|
job_id INTEGER NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
|
|
|
|
|
organization_id TEXT NOT NULL REFERENCES organizations(id),
|
|
|
|
|
sequence INTEGER NOT NULL,
|
|
|
|
|
event_type TEXT NOT NULL,
|
|
|
|
|
message TEXT NOT NULL DEFAULT '',
|
|
|
|
|
progress INTEGER NOT NULL DEFAULT 0 CHECK(progress BETWEEN 0 AND 100),
|
|
|
|
|
error_code TEXT,
|
|
|
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
|
UNIQUE(job_id,sequence)
|
|
|
|
|
);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_job_events_job_sequence ON job_events(job_id,sequence);
|
2026-09-02 18:58:10 +02:00
|
|
|
|
|
|
|
|
-- Phase 5 source framework (additive-safe; credentials contain metadata only).
|
|
|
|
|
CREATE TABLE IF NOT EXISTS sources (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, organization_id TEXT NOT NULL REFERENCES organizations(id),
|
|
|
|
|
name TEXT NOT NULL, kind TEXT NOT NULL CHECK(kind IN ('csv','manual')), enabled INTEGER NOT NULL DEFAULT 0,
|
|
|
|
|
config_json TEXT NOT NULL DEFAULT '{}', health_status TEXT NOT NULL DEFAULT 'unknown',
|
|
|
|
|
consecutive_failures INTEGER NOT NULL DEFAULT 0, circuit_open INTEGER NOT NULL DEFAULT 0,
|
|
|
|
|
last_success_at TEXT, last_failure_at TEXT, last_error TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE(organization_id,name)
|
|
|
|
|
);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_sources_org ON sources(organization_id,id);
|
|
|
|
|
CREATE TABLE IF NOT EXISTS source_credentials (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, source_id INTEGER NOT NULL REFERENCES sources(id) ON DELETE CASCADE,
|
|
|
|
|
organization_id TEXT NOT NULL REFERENCES organizations(id), provider TEXT NOT NULL, key_name TEXT NOT NULL,
|
|
|
|
|
secret_ref TEXT NOT NULL DEFAULT '', metadata_json TEXT NOT NULL DEFAULT '{}', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
|
UNIQUE(source_id,provider,key_name)
|
|
|
|
|
);
|
|
|
|
|
CREATE TABLE IF NOT EXISTS discovery_queries (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, organization_id TEXT NOT NULL REFERENCES organizations(id), source_id INTEGER NOT NULL REFERENCES sources(id) ON DELETE CASCADE,
|
|
|
|
|
name TEXT NOT NULL, query_json TEXT NOT NULL DEFAULT '{}', enabled INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE(organization_id,name)
|
|
|
|
|
);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_discovery_queries_org ON discovery_queries(organization_id,id);
|
|
|
|
|
CREATE TABLE IF NOT EXISTS source_records (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, organization_id TEXT NOT NULL REFERENCES organizations(id), source_id INTEGER NOT NULL REFERENCES sources(id) ON DELETE CASCADE,
|
|
|
|
|
discovery_query_id INTEGER REFERENCES discovery_queries(id) ON DELETE SET NULL, content_hash TEXT NOT NULL, raw_json TEXT NOT NULL,
|
|
|
|
|
normalized_json TEXT NOT NULL, source_url TEXT NOT NULL DEFAULT '', query_context_json TEXT NOT NULL DEFAULT '{}', processing_status TEXT NOT NULL DEFAULT 'raw',
|
|
|
|
|
cursor_json TEXT NOT NULL DEFAULT '{}', rate_policy_json TEXT NOT NULL DEFAULT '{}', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
|
UNIQUE(organization_id,source_id,content_hash)
|
|
|
|
|
);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_source_records_org ON source_records(organization_id,id DESC);
|
2026-09-03 10:43:18 +02:00
|
|
|
|
|
|
|
|
-- Phase 7 domain intelligence (additive-safe; DNS results are explicitly stateful).
|
|
|
|
|
CREATE TABLE IF NOT EXISTS domain_checks (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, organization_id TEXT NOT NULL REFERENCES organizations(id),
|
|
|
|
|
business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE, domain TEXT NOT NULL,
|
|
|
|
|
status TEXT NOT NULL, result_json TEXT NOT NULL DEFAULT '{}', cache_key TEXT NOT NULL,
|
|
|
|
|
checked_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, cache_expires_at TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
|
UNIQUE(organization_id, business_id, cache_key)
|
|
|
|
|
);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_domain_checks_org ON domain_checks(organization_id,created_at DESC,id DESC);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_domain_checks_business ON domain_checks(organization_id,business_id,created_at DESC);
|
|
|
|
|
CREATE TABLE IF NOT EXISTS domain_candidates (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, organization_id TEXT NOT NULL REFERENCES organizations(id), business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
|
|
|
|
|
domain TEXT NOT NULL, source TEXT NOT NULL DEFAULT 'generated', rank INTEGER NOT NULL DEFAULT 0, metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
|
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE(organization_id,business_id,domain)
|
|
|
|
|
);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_domain_candidates_business ON domain_candidates(organization_id,business_id,rank,id);
|