Repository navigation
Store DOI, raw read and target gene identifiers on the rows that use them #944
Description
Summary
DOI, raw read (SRA), Ensembl, RefSeq and UniProt identifiers each live in a shared table that records link to. They're really attributes of the record. Every column besides the identifier is either a constant (db_name), always null (db_version, reference_html), or derived from the identifier (url). This issue moves each identifier string onto the row that uses it, merges the three gene offset tables into one, and drops the five shared tables.
That makes them ordinary owned rows under RLS, visible exactly when their record is (terms in the glossary on #808).
Scope
-
DOI and raw read link tables. Replace each link's foreign key to the shared row with the identifier string. The primary key becomes
(<parent>_id, identifier). The tables:- DOI:
scoreset_doi_identifiers,experiment_doi_identifiers,experiment_set_doi_identifiers - raw read:
scoreset_sra_identifiers,experiment_sra_identifiers,experiment_set_sra_identifiers
- DOI:
-
One table for target gene identifiers. Replace
ensembl_offsets,refseq_offsetsanduniprot_offsetswithtarget_gene_external_identifiers:target_gene_external_identifiers ( target_gene_id integer NOT NULL REFERENCES target_genes (id), db_name text NOT NULL CHECK (db_name IN ('Ensembl', 'RefSeq', 'UniProt')), identifier text NOT NULL, "offset" integer NOT NULL, PRIMARY KEY (target_gene_id, db_name) )
- The primary key matches the data: each target gene has at most one identifier per database.
- The api already presents these as one list,
external_identifierson target genes, built from three relationships bytransform_external_identifier_offsets_to_list. One relationship replaces them, and the transform goes away.
-
Drop the shared tables:
doi_identifiers,sra_identifiers,ensembl_identifiers,refseq_identifiersanduniprot_identifiers. -
Normalize DOIs to one case before storing them, so a record can't cite the same DOI twice.
-
Compute urls when serializing, not store them. Build raw read urls from the accession type, which fixes Submitted Raw Read Identifiers are only created with SRA URLs, even though validation accepts other database identifiers #445.
-
Write paths. Create and update store the strings directly. Remove
find_or_create_doi_identifier,find_or_create_raw_read_identifierandfind_or_create_external_gene_identifier(lib/identifiers.py, L411-L446) and their callers inrouters/experiments.pyandrouters/score_sets.py. -
Search routes. These return distinct identifiers from the link rows and from
target_gene_external_identifiers:POST /doi-identifiers/searchPOST /raw-read-identifiers/searchPOST /target-gene-identifiers/search, which already takesdb_name
Until RLS is enforcing, they filter to rows whose parent the caller can read. Once the link tables carry policies, the database does that filtering.
-
Statistics. The identifier counts in
routers/statistics.pygroup these rows by identifier. -
Api schema. Changes are limited to removing fields:
- the DOI and raw read response models lose
id; - the external gene identifier model loses
idandreference_html.
Target gene responses keep the same
external_identifiersshape. A ui PR regeneratessrc/schema/openapi.d.ts. - the DOI and raw read response models lose
Out of scope: converting DOI identifiers that resolve to publications into publication citations (#217). Publication identifiers keep their shared table (#945).
Acceptance criteria
- The five shared identifier tables and the three offset tables are gone. The DOI and raw read link tables hold the identifier string, and
target_gene_external_identifiersholds the gene identifiers, under the keys above. - The migration replays from an empty database (
alembic/README.md), and its downgrade rebuilds the old tables from the new rows. - Creating and updating a score set or experiment with DOIs, raw read identifiers and target gene identifiers stores the strings without reading any shared table.
- Target gene responses return the same
external_identifierslist as before, minus the removed fields. - The three search routes return only identifiers attached to records the caller can read.
- Raw read urls match their accession type: SRA, BioProject, GEO and ArrayExpress (Submitted Raw Read Identifiers are only created with SRA URLs, even though validation accepts other database identifiers #445).
- A record can't hold the same DOI twice in different cases.
- Router tests cover create, update and search for each identifier type, including a private record's identifier staying out of another user's search.
- The ui PR that regenerates the API types is linked.
Background
On the 2026.3.0 dump the tables are small:
- 79 DOI rows;
- 141 raw read rows;
- gene identifiers: 208 Ensembl, 44 RefSeq and 668 UniProt rows;
- offsets: 635 Ensembl, 289 RefSeq and 2,132 UniProt rows across 2,524 target genes.
Other facts from the same dump:
- Every identifier row has a constant
db_nameand a nulldb_version.reference_htmlis null on every RefSeq and UniProt row, and most gene identifiers have nourl. - No target gene has more than one offset per database.
- None of the identifier tables has a unique constraint, and there are no duplicates.
Kept as shared tables, these would need RLS policies of the "visible if a visible record links to me" kind. Their find-or-create helpers would then break, for the reasons in #945. They aren't rendered as entities anywhere in the product, so folding them in removes the problem instead.
Activity
refactor(models): move ValidTime into models/mixins