# CancerIndex — Data model Source of truth: `packages/database/src/schema/*.ts` (Drizzle, `casing: 'snake_case'`). This document explains the purpose of each table, its key columns and the modelling rules behind it. Column names below are the database names (snake_case); the API returns camelCase. ## 1. Identifier namespaces Public identifiers are `CI--00000001` (8+ digits, zero-padded), minted from per-namespace counters in `id_sequences` by `mintId()` — never reused, never database integers (CLAUDE.md §6). | Namespace | Entity | Table | |---|---|---| | `CI-CAN` | cancer entity | `cancers` | | `CI-GENE` | gene | `genes` | | `CI-VAR` | variant | `variants` | | `CI-DRUG` | drug | `drugs` | | `CI-TRIAL` | clinical trial | `clinical_trials` | | `CI-PUB` | publication | `publications` | | `CI-BIO` | biomarker | `biomarkers` | | `CI-STUDY` | genomic cohort / study | `genomic_cohorts` | | `CI-METRIC` | metric definition | `metric_definitions` | | `CI-SOURCE` | source | `sources` | | `CI-TRT` | treatment / regimen | `treatment_regimens` | | `CI-ANAT` | anatomical site | `anatomical_sites` | | `CI-GEO` | geography | `geographies` | | `CI-PROV` | provenance (minted lazily when exposed) | `provenance.public_id` | | `CI-ORG`, `CI-EDGE`, `CI-RANK` | reserved | — | Other public references: slugs (`cancers.slug`, `drugs.slug`, `variants.slug`), HGNC symbols, `NCT` ids and PMIDs. Ingest runs are `ING--YYYYMMDD-000001`. ## 2. Registry and operations | Table | Purpose | Key columns | |---|---|---| | `sources` | Source + license registry, seeded from connector manifests (`pnpm cix sources:sync`). Drives `/sources`. | `slug` (= connector id), `category`, `access_type`, `access_auth`, `license`, `license_status` (review/approved/restricted/blocked), `commercial_use`, `redistribution`, `attribution`, `approved_for_production`, `status` (planned/active/paused/degraded/awaiting_credentials/review/retired), `tier`, `manifest` (jsonb copy) | | `ingest_runs` | One row per connector execution with counters, HTTP stats, schema drift, cursors, log and anomaly flag. | `id`, `connector_id`, `mode` (full/incremental/backfill/dry_run/probe), `status` (running/succeeded/failed/partial/aborted), `records_*`, `http_*`, `schema_drift`, `cursor_before/after`, `log`, `dataset_version`, `anomaly` | | `connector_cursors` | Restart cursor and health per connector. | `connector_id`, `cursor` (jsonb), `last_success_at`, `paused`, `health` (healthy/degraded/failing/review/awaiting_credentials/unknown), `health_detail` | | `connector_field_stats` | Observed field names/types per entity for schema-drift detection. | `connector_id`, `entity`, `field`, `types[]`, `seen_count`, `null_count` | | `source_records` | RAW layer index: one row per source-native record, idempotency key `(source_id, entity_kind, source_record_id)`. | `payload_hash` (sha256), `raw_path` (lake file), `status` (active/deprecated/retracted/withdrawn/source_missing), `first_seen_run`, `last_seen_run`, `canonical_type`, `canonical_id` | | `provenance` | The provenance record every scientific value points to. | `source_id`, `source_record_id`, `source_url`, `dataset`, `dataset_version`, `pmid`, `doi`, `retrieved_at`, `published_at`, `geography`, `population`, `cohort_size`, `methodology`, `evidence_type` (registry/clinical_trial/…/computed), `access_level`, `confidence`, `license`, `ingest_run_id` | | `unresolved_labels` | Curation queue: labels no connector could reconcile (never dropped). | `source_id`, `entity_kind`, `source_text`, `normalized`, `count`, `status` (open/mapped/rejected/ignored), `suggested_id`, `suggested_match_type`, `suggested_score`, `resolved_id`, `resolved_by` | | `change_events` | Entity change history feeding "What changed". | `entity_type`, `entity_id`, `kind` (created/updated/trial_added/approval_added/ranking_changed/merged/deprecated/alias_added), `summary`, `before`, `after`, `ingest_run_id` | | `audit_log` | Every admin/curator mutation. | `actor`, `action`, `entity_type`, `entity_id`, `before`, `after`, `reason` | | `entity_merges` | Reversible merge queue. | `entity_type`, `keep_id`, `merge_id`, `evidence`, `status` (proposed/merged/kept_separate/reverted) | | `api_keys` | Public API keys: hash only. | `key_hash` (sha256), `prefix`, `tier`, `rate_limit_per_minute`, `active`, `last_used_at` | | `id_sequences` | Per-namespace counters for `CI-*` ids. | `namespace`, `next` | ## 3. Cancer ontology (canonical) | Table | Purpose | Key columns | |---|---|---| | `cancers` | One row per recognised disease concept (never per alias). | `id`, `slug`, `canonical_name`, `short_name`, `entity_type` (cancer/cancer_family/histology/subtype/molecular_subtype/hematologic_malignancy/precursor_condition/other), `malignant`, `solid_tumor`, `hematologic`, `pediatric_relevant`, `rare_cancer` (null = unknown), `top_level` (member of the mutually exclusive ranking set), `description` + `description_provenance_id`, `primary_ncit_code`, `primary_oncotree_code`, `depth`, `status` (active/deprecated/merged), `merged_into`, `classification_version`, `semantic_types[]` | | `cancer_aliases` | Synonyms with normalised form used for reconciliation. | `cancer_id`, `alias`, `normalized`, `alias_type` (preferred/synonym/abbreviation/historical/deprecated/display), `source_id`, `source_terminology`, `language` | | `cancer_hierarchy` | Multi-dimensional hierarchy: several trees coexist. | `parent_id`, `child_id`, `hierarchy_type` (ncit/oncotree/anatomical/histological/molecular/who/icd/seer), `source_id` | | `cancer_codes` | Cross-reference codes, searchable (never buried in JSON). | `cancer_id`, `system` (ncit/icd10/icd10cm/icdo_topography/icdo_morphology/doid/oncotree/umls/mesh/mondo/seer_site/efo/orphanet/gdc_project), `code`, `match_type`, `source_id`, `valid_from`, `valid_to` | | `anatomical_sites` / `cancer_anatomy` | Anatomy dimension (NCIt/UBERON) and cancer→site relations (`primary`/`metastatic`). | `ncit_code`, `uberon_id`, `parent_id`, `system`; `relation` | | `geographies` | Canonical geography for observations and ranking scopes. | `slug`, `name`, `kind` (world/region/who_region/country/subdivision), `iso2`, `iso3`, `parent_id`, `who_region`, `population`, `population_year` | | `cohort_definitions` | Attribute combinations that are not taxonomy nodes (stage, biomarkers). | `cancer_id`, `biomarker_ids[]`, `variant_ids[]`, `stage`, `attributes` | Qualified disease states (stage, recurrent, metastatic, laterality…) are not canonical cancers; the rules are in `packages/ontology/src/qualifiers.ts`. The top-level ranking set (`packages/ontology/src/top-level.ts`) maps GLOBOCAN/ICD-10 site groups to NCIt anchors. ## 4. Genes, variants, biomarkers, cohorts | Table | Purpose | Key columns | |---|---|---| | `genes` | HGNC is authoritative for symbols. | `hgnc_id`, `symbol`, `name`, `locus_type`, `location`, `chromosome`, `ensembl_gene_id`, `ncbi_gene_id`, `omim_ids[]`, `uniprot_ids[]`, `prev_symbols[]`, `alias_symbols[]`, `status`, `is_cancer_gene` (derived), `civic_gene_id` | | `gene_aliases` | Previous/alias symbols and names. | `alias`, `alias_type` (prev_symbol/alias_symbol/prev_name/alias_name) | | `variants` | Coordinates always carry an assembly; original + normalised kept. | `slug`, `gene_id`, `gene_symbol`, `name`, `variant_type` (SO-style), `hgvs_g/c/p`, `assembly` (GRCh37/GRCh38), `chromosome`, `start`, `end`, `reference_bases`, `alternate_bases`, `coordinates` (per-assembly list), `clinvar_variation_id`, `civic_variant_id`, `dbsnp_ids[]`, `fusion_partners[]` | | `variant_aliases` | Alternative names. | `alias`, `source_id` | | `variant_clinical_significance` | ClinVar interpretations kept structured (never flattened). | `clinvar_variation_id`, `clinical_significance`, `review_status`, `star_rating`, `last_evaluated`, `conditions[]`, `condition_cancer_ids[]`, `origin_simple`, `number_submitters`, `provenance_id` | | `biomarkers` | First-class biomarkers with assay/threshold metadata. | `kind` (gene_mutation/protein_expression/hormone_receptor/immune_marker/msi/tmb/hrd/ctdna/…), `gene_id`, `ncit_code`, `measurement` (jsonb) | | `genomic_cohorts` | Studies (GDC projects…), original study ids preserved. | `source_id`, `study_id`, `name`, `program`, `primary_sites[]`, `disease_types[]`, `cancer_id`, `cancer_match_type`, `case_count`, `cases_with_ssm` (denominator), `data_release`, `access_level`, `provenance_id` | | `cancer_gene_frequencies` | Alteration frequency per cohort — denominator mandatory. | `cohort_id`, `cancer_id`, `gene_id`, `gene_symbol`, `alteration_type` (ssm/cnv_gain/cnv_loss/fusion), `cases_affected`, `cases_profiled`, `frequency`, `rank`, `data_release`, `provenance_id` | | `entity_embeddings` | Vectors with model + dimensions recorded (pgvector). | `entity_type`, `entity_id`, `model`, `dimensions`, `text_hash`, `embedding` | ## 5. Therapies and regulatory status | Table | Purpose | Key columns | |---|---|---| | `drugs` | Generic/INN preferred; brands are aliases, never separate molecules. | `slug`, `name`, `kind`, `ncit_code`, `chembl_id`, `civic_therapy_id`, `drugbank_id`, `pubchem_cid`, `unii`, `mechanism`, `target_gene_ids[]`, `development_status` | | `drug_aliases` | Generic/brand/development code/salt/synonym. | `alias`, `normalized`, `alias_type`, `source_id` | | `treatment_regimens` | Combinations and modalities. | `component_drug_ids[]`, `modality` | | `drug_approvals` | Country-aware regulatory status — never a bare `approved=true`. | `drug_id`, `cancer_id`, `biomarker_ids[]`, `tumor_agnostic`, `jurisdiction` (US/CA/EU/UK/AU/JP/CH/OTHER), `authority` (FDA/Health Canada/EMA/…), `indication`, `line_of_therapy`, `disease_stage`, `approval_type`, `accelerated`, `conditional`, `approval_date`, `withdrawal_date`, `status` (approved/conditional/accelerated/withdrawn/superseded), `application_number`, `source_id`, `provenance_id`, `raw` | ## 6. Clinical trials | Table | Purpose | Key columns | |---|---|---| | `clinical_trials` | ClinicalTrials.gov studies (v2 API fields). | `nct_id`, `brief_title`, `official_title`, `acronym`, `study_type`, `phases[]`, `overall_status`, `why_stopped`, dates (`start_date`, `primary_completion_date`, `completion_date`, `first_posted_date`, `last_update_posted_date`, `results_first_posted_date`), `has_results`, `enrollment_count/type`, `lead_sponsor`, `lead_sponsor_class`, `collaborators[]`, `conditions[]` (raw), `keywords[]`, `interventions` (jsonb), `arms`, `primary/secondary_outcomes`, `eligibility`, `sex`, `minimum_age`, `maximum_age`, `countries[]`, `locations_count`, `references` (jsonb with PMIDs), `brief_summary`, `is_oncology`, `source_record_id`, `ingest_run_id` | | `trial_conditions` | Free-text condition → cancer mapping with match type and confidence. | `trial_id`, `condition_text`, `normalized`, `cancer_id` (nullable), `match_type` (default `UNRESOLVED`), `confidence` | | `trial_interventions` | Intervention → drug mapping. | `trial_id`, `name`, `normalized`, `intervention_type`, `drug_id`, `match_type` | | `trial_locations` | Sites (facility, city, state, country, status, lat/lng). | `trial_id`, `country`, … | | `trial_pulse` | Derived daily new-trial counts per cancer/phase. | `day`, `cancer_id`, `phase`, `new_trials` | Trials count for a cancer **and all its ancestors**: a trial mapped to "Lung Adenocarcinoma" also counts for "Lung Cancer". Descendant traversal is a recursive CTE over `cancer_hierarchy`, depth ≤ 12, identical in the API (`apps/api/src/lib/descendants.ts`) and the counters (`packages/ranking`). ## 7. Literature | Table | Purpose | Key columns | |---|---|---| | `publications` | One entity per paper across PubMed/DOI/Europe PMC. | `pmid`, `doi`, `pmcid`, `title`, `abstract` (indexed, displayed truncated), `journal`, `journal_iso`, `pub_date`, `pub_year`, `publication_types[]`, `mesh_terms` (jsonb with major flag + qualifiers), `authors` (jsonb), `language`, `is_preprint`, `retracted`, `retraction_notice`, `nct_ids[]`, `cited_by_count` | | `publication_entity_edges` | Publication → entity links with extraction method and status. | `publication_id`, `entity_type` (cancer/gene/variant/drug/biomarker/trial), `entity_id`, `method` (mesh/dictionary/registry_reference/civic_curation/ner/llm/curator), `confidence`, `status` (candidate/validated/rejected), `source_id` | | `literature_counts` | Research activity per cancer per window — the exact PubMed query is stored with the count. | `cancer_id`, `window_key` (all/12m/5y/5y_prior/10y/y2015…), `window_start`, `window_end`, `query`, `count`, `provenance_id` | ## 8. Evidence and knowledge graph | Table | Purpose | Key columns | |---|---|---| | `civic_evidence_items` | CIViC evidence items kept in their native structure. | `civic_id`, `molecular_profile_id/name`, `gene_symbols[]`, `gene_ids[]`, `variant_ids[]`, `civic_variant_ids[]`, `disease_name`, `doid`, `cancer_id`, `cancer_match_type`, `therapy_names[]`, `therapy_ids[]`, `therapy_interaction_type`, `evidence_type` (PREDICTIVE/PROGNOSTIC/DIAGNOSTIC/PREDISPOSING/ONCOGENIC/FUNCTIONAL), `evidence_level` (A–E), `evidence_direction` (SUPPORTS/DOES_NOT_SUPPORT), `significance`, `evidence_rating`, `status` (ACCEPTED/SUBMITTED/REJECTED), `pmid`, `source_citation`, `phenotypes[]`, `provenance_id` | | `knowledge_edges` | Typed edges with mandatory context. | `source_entity_type/id`, `target_entity_type/id`, `relationship_type` (HAS_SUBTYPE/OCCURS_IN/ASSOCIATED_WITH/HAS_VARIANT/HAS_BIOMARKER/TREATED_BY/STUDIED_IN/DESCRIBED_BY/TARGETS/APPROVED_FOR/INVESTIGATED_FOR/PREDICTS_RESPONSE_TO/CONFERS_RESISTANCE_TO/PREDISPOSES_TO/PROGNOSTIC_IN/DIAGNOSTIC_OF), `cancer_context_ids[]`, `predictive/prognostic/diagnostic/predisposing`, `direction`, `evidence_level` (source-native, never re-scaled), `evidence_score`, `evidence_category` (observed_data/published_evidence/curated_evidence/regulatory_status/clinical_guideline/computed_metric), `status`, `source_id`, `provenance_ids[]`, `support_count`, `first/last_seen_at` | | `risk_factors` | Risk factors and carcinogens as first-class entities. | `slug`, `name`, `kind`, `classification_authority`, `classification` | Only accepted CIViC items (`status = 'ACCEPTED'`) feed counters and rankings; the API separates evidence by cancer context and never pools levels across diseases. ## 9. Time-aware observations | Table | Purpose | Key columns | |---|---|---| | `epidemiology_observations` | Incidence/mortality/prevalence per cancer × geography × year × sex × age group × metric × source × site definition. Unique on that key — a new year is a new row, never an overwrite. | `year`, `year_end` (multi-year aggregates), `sex` (all/male/female), `age_group`, `metric` (incidence_count/incidence_rate/as_incidence_rate/mortality_count/mortality_rate/as_mortality_rate/prevalence/prevalence_5y), `value`, `unit` (count/per_100k), `lower_ci`, `upper_ci`, `standard_population`, `estimate_type` (observed/estimated/projected), `site_definition` (source's own site label / ICD range), `source_id`, `provenance_id` | | `survival_observations` | Survival with mandatory context. | `geography_id`, `stage`, `staging_system`, `sex`, `age_group`, `diagnosis_period`, `survival_type` (overall/relative/cause_specific/progression_free/disease_free/net), `duration_months`, `probability` (0..1), `median_months`, `cohort_size`, `lower_ci`, `upper_ci`, `method`, `source_id`, `provenance_id` | Rates standardised to different standard populations are never compared in one ranking; the `standard_population` column is part of the scope. ## 10. Derived counters and rankings | Table | Purpose | Key columns | |---|---|---| | `metric_definitions` | Versioned metric catalog (seeded from `packages/database/src/seed-data/metrics.ts`). | `slug`, `name`, `description`, `formula`, `formula_version`, `unit`, `higher_is_worse`, `aggregation`, `valid_dimensions[]`, `source_slugs[]`, `category`, `eligibility` (jsonb), `experimental` | | `entity_counters` | Deterministic counters per entity, fully rebuilt by `refreshCounters()`. | `entity_type`, `entity_id`, `trial_count`, `active_trial_count`, `recruiting_trial_count`, `phase3_trial_count`, `publication_count(_5y/_12m)`, `gene_count`, `variant_count`, `drug_count`, `approved_drug_count`, `evidence_count`, `cohort_count`, `subtype_count`, `descendant_count`, `epidemiology_obs_count`, `survival_obs_count`, `completeness` (jsonb 0/1 per section), `updated_at` | | `ranking_snapshots` | One metric × one scope × one formula version at one time. | `metric_id`, `metric_slug`, `scope_key` (`geo=…|sex=…|age=…|year=…|level=…`), `geography`, `sex`, `age_group`, `year`, `entity_level` (top/all), `formula_version`, `eligible_entities`, `inputs_hash`, `source_ids[]`, `is_current`, `generated_at` | | `rankings` | Rows of a snapshot with lineage. | `snapshot_id`, `cancer_id`, `rank` (competition ranking 1,2,2,4), `eligible_entities`, `percentile`, `value`, `unit`, `confidence` (HIGH/MEDIUM/LOW/INSUFFICIENT_DATA), `inputs` (observation ids, counters, formula inputs), `breakdown` (composite components, unused in Phase 1), `previous_rank` | | `ai_answers` | Cached AI syntheses (Phase 5). | `kind`, `subject_id`, `question_hash`, `answer`, `model`, `prompt_version`, `source_snapshot`, `data_as_of` | Note on column naming: `entity_counters.computed_at`, `literature_counts.computed_at` and `trial_pulse.computed_at` are declared with the shared `updatedAt()` helper, so their **database column is `updated_at`**. Raw SQL must use `updated_at`; the API exposes it as `computedAt`. ## 11. Provenance model in one picture ``` rankings.inputs.observationId ─▶ epidemiology_observations.provenance_id ─▶ provenance ─▶ source_records (payload_hash, raw_path) ─▶ data/raw/*.jsonl.gz └▶ sources (license, attribution) ``` `traceValue(db, table, id)` (`packages/ranking/src/trace.ts`, exposed at `GET /v1/admin/trace/:table/:id` and inside `GET /v1/rankings/:metric/:cancerId/explain`) returns that chain for `rankings`, `epidemiology_observations`, `survival_observations`, `cancer_gene_frequencies` and `literature_counts`.