Class AnnotationRelationDaoTest
ANNOTATION_RELATION.
The interesting behaviour is not "does the query return rows" - it is the three places this can be quietly wrong: an ACL clause that eats the asserted rows it should never have touched, a ranking that lets co-occurrence outrank a curator, and an expansion that resolves ambiguity it was supposed to preserve. Each has a test.
-
Nested Class Summary
Nested classes/interfaces inherited from class BaseDatabaseTest5
BaseDatabaseTest5.BaseDatabaseTestContextConfiguration -
Field Summary
Fields inherited from class BaseDatabaseTest5
sessionFactory -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionvoidsetUp()voidA cell line or strain's genetic background is a fact about that line, not about the experiment that used it, and the implication runs one way: a knockout line implies C57BL/6, while C57BL/6 implies nothing about which of the thousands of lines on it is in hand.void🛑 An object that relates to many subjects identifies none of them.voidA conclusion the dataset already carries is not an inference, so it is not offered back to it.voidThe shape the two rules must NOT cost us, measured on GSE28044.void🛑 A vocabulary label spelled two ways is one relation, not two.voidThe bar, on the same fixture.voidAn assertion and a refutation of the same triple are two things a source said, so the grain must keep them apart.void🛑 An experiment seed may only walk FORWARDS: the dataset has to supply the end that implies.void🛑 An inducer is never handed the disease, whatever the taxon.voidRefuse to enumerate the table.voidA predicate whose object is a quantity does not relate two concepts, so the harvest does not store it at all — 9,606 of 36,073 curated rows, whose objects are10 uMand10 mg/kg.void🛑 A refutation is not weak support — it is the opposite — so it stays out of ordinary reads and out of every inference.voidAn ontology's claim is about a term, not about anything Gemma holds, so no dataset permission can be relevant to it.voidA curator's statement outranks any amount of co-occurrence.void🛑 AnOBJECT_IMPLIES_SUBJECTlicence resolves tohas role in modelingorhas disease— a claim ABOUT DISEASE — so it may only run where the subject is one.void🛑 The curated CATEGORY is the unreliable half of the row; the subject's VOCABULARY is the reliable half.voidAn attested row names a dataset, so a private one must not contribute a count or an example.voidBreadth counts subjects, not rows.void🛑RO_0001000 derives fromlicenses nothing until some source has said what its object IS.voidThe complement ofsubjectCategoryUris: everything except one kind of implying term.voidExcluding a dataset removes its contribution, which is what makes "this tag is inferable, so it can be dropped" a claim about the rest of the corpus rather than about the dataset itself.voidExpansion returns the whole candidate set and resolves nothing.void🛑 uib, 2026-08-18: a curator's term card carriedinduced pluripotent stem cell line cell --has disease--> lower motor neuron.voidWith no exclusion the same three rows all come back, so the filter is what removed the row above.voidA rebuild clears the basis it is rebuilding, so a relation whose source annotation was deleted does not outlive it.void🛑 Subject breadth counts objects under ONE predicate, and the unscoped number it used to report cannot do the job the field was added for.void🛑 Within a basis, support has to order — the score used to be a constant.void🛑 The breadth cut runs BEFORE the row cap, or a threshold thins the answer instead of improving it.voidThe derived claim is its own triple, and taxon picks its verb.voidvoidThe same rows read from either end.void🛑 An asserted basis carries no support, so every row of it scores the same and the sort used to fall through to alphabetical.voidTwo bases naming different terms are reported side by side, not merged and not treated as a disagreement.void🛑 Two different stored relations converging on one claim must key the same.Methods inherited from class BaseDatabaseTest5
flushAndClearSession
-
Constructor Details
-
AnnotationRelationDaoTest
public AnnotationRelationDaoTest()
-
-
Method Details
-
setUp
@BeforeEach public void setUp() -
testAssertedRelationIsNotFilteredByDatasetPermissions
@Test public void testAssertedRelationIsNotFilteredByDatasetPermissions()An ontology's claim is about a term, not about anything Gemma holds, so no dataset permission can be relevant to it.This is the failure the ACL composition exists to prevent: the clause is a bitmask test on a column that asserted rows have no value for, so writing it the obvious way silently deletes every ontology-asserted relation from every anonymous response - and looks like "the ontology producer never ran".
-
testAttestedRelationOnAPrivateDatasetIsHidden
@Test public void testAttestedRelationOnAPrivateDatasetIsHidden()An attested row names a dataset, so a private one must not contribute a count or an example. -
testTheRelationReadsFromEitherEnd
@Test public void testTheRelationReadsFromEitherEnd()The same rows read from either end. A relation is not a direction, and the browse selector asks it one way round while the experiment page asks it the other. -
testAssertionOutranksAttestation
@Test public void testAssertionOutranksAttestation()A curator's statement outranks any amount of co-occurrence.Ranked on support alone the co-occurrence wins here by five experiments to one, which is the whole reason the basis dominates the score: the corpus signal exists because a redundant tag was written beside a genotype that already implied it, and quantity of that is not quality.
-
testTwoFramingsOfTheSameFactBothSurvive
@Test public void testTwoFramingsOfTheSameFactBothSurvive()Two bases naming different terms are reported side by side, not merged and not treated as a disagreement.This is the SURF1 case exactly: MONDO's germline axiom points at
MONDO:0700250 mitochondrial complex IV deficiency, nuclear type 1where the curator wroteMONDO:0009723 Leigh syndrome. They share no xref, sit in different branches, and neither subsumes the other, because the ontology is modelling the molecular diagnosis and the curator the clinical syndrome. Collapsing them on term identity would drop one of two correct answers. -
testExpansionKeepsAmbiguityInsteadOfResolvingIt
@Test public void testExpansionKeepsAmbiguityInsteadOfResolvingIt()Expansion returns the whole candidate set and resolves nothing.Ambiguity is fatal to generation and harmless to membership. A caller asking "is the disease I am about to tag among those this genotype is associated with?" gets the right answer whichever of the three is meant; a caller that had been handed one confident answer would ship a wrong tag two times in three.
-
testABroadObjectIsDroppedWhenACallerSetsABar
@Test public void testABroadObjectIsDroppedWhenACallerSetsABar()🛑 An object that relates to many subjects identifies none of them.Measured on the corpus:
Homozygous negativerelates to 2,898 distinct subjects,Overexpressionto 1,839,24 hto 448,induced pluripotent stem cell line cellto 81 — whileMPTPand5xFADsit in the low single digits. A gate seeded with a broad object implies every one of those subjects, which is how a suppression rule ends up dropping a tag because the experiment mentioned a dose.Not a quality judgement and not a list of bad terms: a dose is a perfectly good curated statement, and one measured number covers zygosity, perturbation direction, dose, duration and generic ontology classes without anyone maintaining it.
-
testBreadthCountsDistinctSubjects
@Test public void testBreadthCountsDistinctSubjects()Breadth counts subjects, not rows.🛑 What this test CANNOT cover: the production failure was a case variant, and H2 in
MODE=MYSQLis case-sensitive where production MySQL is not — so a test asserting that behaviour here passes against code that is wrong in production. The fix is therefore in SQL (group by lower(trim(...))), where both engines agree, rather than in a Java-side comparison a test could bless. Seen live as breadth 0 onfamilial Alzheimer's diseaseandintermediate, where 0 means "maximally specific" and so fails OPEN. -
testSupportOrdersWithinABasis
@Test public void testSupportOrdersWithinABasis()🛑 Within a basis, support has to order — the score used to be a constant.getScore()returnedbasisRank * 1000 + 1for every self-sufficient basis, so everyCURATEDrow scored identically and the sort became a no-op: results fell through to the alphabetical tiebreakers and a 2-dataset relation could be served above a 10-dataset one. uib saw the strongest Alzheimer row arrive tenth. -
testALabelSpelledTwoWaysIsOneRelation
@Test public void testALabelSpelledTwoWaysIsOneRelation()🛑 A vocabulary label spelled two ways is one relation, not two.Disease modelanddisease modelshareTGEMO_00101;towardandtowardsshareRO_0002503. Grouping on the spelling splits one relation across two rows and fragments its support, so every ranking built on per-row support ranks fragments and whichever row a client renders understates the evidence.Normalized in SQL rather than by collation, so H2 and MySQL agree and this is actually testable — unlike
objectBreadth's case bug, which is not. -
testABackgroundIsAFactAboutTheLineAndImpliesOnlyDownwards
@Test public void testABackgroundIsAFactAboutTheLineAndImpliesOnlyDownwards()A cell line or strain's genetic background is a fact about that line, not about the experiment that used it, and the implication runs one way: a knockout line implies C57BL/6, while C57BL/6 implies nothing about which of the thousands of lines on it is in hand.Minted as TGEMO_00216 on 2026-08-29 so that a background the submitter reported as a constant characteristic stops being recorded as the experimental strain itself.
-
testTheImplicationRunsOnlyOneWayAndTheWayDependsOnThePredicate
@Test public void testTheImplicationRunsOnlyOneWayAndTheWayDependsOnThePredicate() -
testDerivesFromNeedsSomebodyToHaveSaidWhatTheObjectIs
@Test public void testDerivesFromNeedsSomebodyToHaveSaidWhatTheObjectIs()🛑RO_0001000 derives fromlicenses nothing until some source has said what its object IS.It is the one predicate that carries both directions under a single subject category, so no subject-side rule can classify it. Both of these are curated and both are filed under
disease:refractory anemia with excess blasts -- derives from --> myelodysplastic syndrome subject is specific influenza -- derives from --> H3N2 object is specific
Over the 666 curated rows measured 2026-08-18, topicality admits 250 and they are the wrong ones —
Cachexia -> melanoma,infectious disease -> Borrellia burgdorferi,partial duplication of chromosome 7 -> maternal duplicationbeside the same subject's-> paternal duplication. An object category is set only by a producer and never by the curated harvest, so requiring one admits CLO's flat rows and refuses every curated row at once. -
testTheDerivedClaimIsItsOwnTripleWithATaxonChosenVerb
@Test public void testTheDerivedClaimIsItsOwnTripleWithATaxonChosenVerb()The derived claim is its own triple, and taxon picks its verb.The store holds
Alzheimer disease --has_genotype--> APP/PS1; what follows is "APP/PS1 is a disease model of Alzheimer's" — different ends AND a different verb. Handing a client only the stored row makes it invert and choose, and three clients will choose three ways.A mouse carrying the genotype models the disease; a human line carrying a variant has it. Unknown taxon takes the weaker claim rather than a guess.
-
testAnUnseededQueryReturnsNothing
@Test public void testAnUnseededQueryReturnsNothing()Refuse to enumerate the table. Every caller knows one end of the relation, and a query that names neither is a mistake rather than a request for everything. -
testExcludingADatasetRemovesItsOwnEvidence
@Test public void testExcludingADatasetRemovesItsOwnEvidence()Excluding a dataset removes its contribution, which is what makes "this tag is inferable, so it can be dropped" a claim about the rest of the corpus rather than about the dataset itself. -
testRemoveByBasisClearsOnlyThatBasis
@Test public void testRemoveByBasisClearsOnlyThatBasis()A rebuild clears the basis it is rebuilding, so a relation whose source annotation was deleted does not outlive it. -
testExcludedSubjectCategoryDropsOnlyThatCategory
@Test public void testExcludedSubjectCategoryDropsOnlyThatCategory()The complement ofsubjectCategoryUris: everything except one kind of implying term.Naming the categories to KEEP cannot express this — it would mean enumerating every category the store holds and revisiting that list whenever one is added — which is why the exclusion exists rather than the caller inverting the include.
🛑 The uncategorised row is the one that matters.
NOT INagainst a NULL column evaluates to NULL, never true, so the obvious clause drops every subject with no category alongside the excluded one. An unknown category is not the excluded category.termLevelOnly(false)because topicality is decided downstream of this clause and would remove the uncategorised row for a different reason, which is the one thing that would make a green result here meaningless. -
testNoExclusionKeepsEveryCategory
@Test public void testNoExclusionKeepsEveryCategory()With no exclusion the same three rows all come back, so the filter is what removed the row above. -
testTheBreadthCutRunsBeforeTheRowCap
@Test public void testTheBreadthCutRunsBeforeTheRowCap()🛑 The breadth cut runs BEFORE the row cap, or a threshold thins the answer instead of improving it.The cap used to be applied in SQL — with no
ORDER BY, so an arbitrary N — and the breadth bar in Java afterwards. A caller asking for "the best N that clear the bar" got "an arbitrary N, minus the failures", which for a gate means missing a term that qualifies because something unqualified was fetched ahead of it. oganm found the identical shape in #1685's disease-model endpoint, where the specificity cut ran after the row cap.The fixture makes the two orders disagree: three broad objects and one specific one, with a cap of two. Cut-then-cap returns the specific one; cap-then-cut can lose it entirely.
-
testARefutedRelationIsExcludedUnlessAskedForAndNeverInfers
@Test public void testARefutedRelationIsExcludedUnlessAskedForAndNeverInfers()🛑 A refutation is not weak support — it is the opposite — so it stays out of ordinary reads and out of every inference.MGI publishes 1,211 of these (
MGI_Geno_NotDiseaseDO.rpt): curated, cited rows saying a genotype does NOT model a disease. Worth holding, and dangerous to hold carelessly — read by anything unaware of the column it states the reverse of its source. Hence excluded by default, reachable only by asking, and never able to license a claim. -
testAnAssertionAndARefutationOfOneTripleDoNotCollapse
@Test public void testAnAssertionAndARefutationOfOneTripleDoNotCollapse()An assertion and a refutation of the same triple are two things a source said, so the grain must keep them apart. Collapsed into one row, whichever won would hide the other. -
testTiedAssertedRowsAreOrderedSpecificFirst
@Test public void testTiedAssertedRowsAreOrderedSpecificFirst()🛑 An asserted basis carries no support, so every row of it scores the same and the sort used to fall through to alphabetical.uib measured it on
imatinibonce CHEBI's roles came back whole: ten roles, all support 0, ordered a–z, soantihypertensive agent(borne by 487 chemicals) led the card andtyrosine kinase inhibitor(44) — the only role that identifies the compound — sat tenth behind a "+5 more". Breadth ascending puts the specific end first, which is the advice we gave for reading roles at all, applied at the one place a client cannot apply it: inside a?limit=.The fixture is built so the two orderings disagree — the specific object sorts LAST alphabetically — or it would pass without the comparator.
-
testASubjectsVocabularyOutranksAMiscategorizedRow
@Test public void testASubjectsVocabularyOutranksAMiscategorizedRow()🛑 The curated CATEGORY is the unreliable half of the row; the subject's VOCABULARY is the reliable half.uib measured the same fact twice in one corpus, 2026-08-18:
seizures MP_0002064 --induced by--> kainic acidfiled once asDisease modeland once astreatment. Licensing on the category alone made one a disease model and the other nothing — one fact wearing two spellings of its metadata, which is the same class of defect as label-spelling fragmentation.media EFO_0000579is the control that stops this becoming "license everything filed as treatment": a growth condition really is a treatment condition, and LPS added to media models nothing. EFO is deliberately absent from the namespace list for exactly that reason — EFO diseases come in by their category, which is correct on those rows. -
testAStrainOrGenotypeSubjectLicensesNoDiseaseClaim
@Test public void testAStrainOrGenotypeSubjectLicensesNoDiseaseClaim()🛑 AnOBJECT_IMPLIES_SUBJECTlicence resolves tohas role in modelingorhas disease— a claim ABOUT DISEASE — so it may only run where the subject is one.uib, 2026-08-18, from the
C57BL/6chip on GSE99114:strain: C10 Congenic (A.B6chr10) --has_genotype--> C57BL/6 => C57BL/6 has role in modeling C10 Congenic backwards: C57BL/6 is its BACKGROUND genotype: Myrf [mouse] --has_genotype--> C57BL/6 => C57BL/6 has role in modeling Myrf a strain is not a model of a gene
The licence assumed the pattern
disease model: X --has_genotype--> <genotype>, where the specific end is the object. With a strain or a genotype as the subject the arrow simply runs the other way, and inverting it asserts something nobody said.These rows stay TERM_LEVEL and stay on the card — they are facts about the term. Only the claim is withdrawn. Two orthogonal filters, as designed.
-
testInducedByOnACellTypeSubjectLicensesNothing
@Test public void testInducedByOnACellTypeSubjectLicensesNothing()🛑 uib, 2026-08-18: a curator's term card carriedinduced pluripotent stem cell line cell --has disease--> lower motor neuron. A neuron is not a disease and a cell line does not have one.The stored row was right —
lower motor neuron --induced by--> iPSC lineis a differentiation protocol, one of the commonest things curated here. What was wrong was readinginduced byas though it always meant the disease-model sense it carries onParkinson disease --induced by--> MPTP. The subject's category is what separates them, and the inference now consults it.uib could not filter this out themselves:
objectCategoryis null on CURATED rows by construction, sohas disease -> neuronandhas disease -> glioblastomaare indistinguishable to a client. It had to be fixed here or it stayed on screen. -
testAnInducerIsNeverHandedTheDiseaseEvenForAHumanExperiment
@Test public void testAnInducerIsNeverHandedTheDiseaseEvenForAHumanExperiment()🛑 An inducer is never handed the disease, whatever the taxon.uib measured four
induced byrows on one subject:MPTP,alpha-synuclein inclusion bodyandmethamphetaminecame back has role in modeling Parkinson disease andoxidopaminecame back has disease Parkinson disease. The only thing that differed was which taxon the attesting experiment carried."A mouse carrying APP/PS1 models the disease; a human line carrying LRRK2 G2019S has it" is the right rule for an organism or a line and unsatisfiable for a compound. The fixture taxon here is human, so the taxon rule alone would say
has disease. -
testAQuantityValuedPredicateIsNotARelationBetweenConcepts
@Test public void testAQuantityValuedPredicateIsNotARelationBetweenConcepts()A predicate whose object is a quantity does not relate two concepts, so the harvest does not store it at all — 9,606 of 36,073 curated rows, whose objects are10 uMand10 mg/kg. Caught by URI, and by label for the rows nobody grounded. -
testAnExperimentSeedMustSupplyTheEndThatImplies
@Test public void testAnExperimentSeedMustSupplyTheEndThatImplies()🛑 An experiment seed may only walk FORWARDS: the dataset has to supply the end that implies.Paul on GSE315959, 2026-08-27. Its one grounded annotation is
organism part: prostate gland, which is the CONCLUSION ofCLO_0037208 derives from anatomic part, not its premise. Seeded from the object side the card read 169 Cellosaurus prostate lines back out of that one term and offered them as the dataset's inferred concepts. A cell line implies the organ it was taken from; an organ implies no cell line.The control is the same stored row seeded from the other end, so an empty answer here is the rule and not a fixture that never matched anything.
-
testAConclusionTheDatasetAlreadyCarriesIsNotOfferedBackToIt
@Test public void testAConclusionTheDatasetAlreadyCarriesIsNotOfferedBackToIt()A conclusion the dataset already carries is not an inference, so it is not offered back to it.Both legs seed from the SUBJECT side, so both clear the forward-walk rule and only this one separates them: the experiment that carries the line alone is told what it came from, and the experiment that already says
prostate glandis told nothing. -
testAGenotypeSeedStillReachesTheDiseaseItStandsFor
@Test public void testAGenotypeSeedStillReachesTheDiseaseItStandsFor()The shape the two rules must NOT cost us, measured on GSE28044.breast cancer --has_genotype--> BRCA1is stored with the disease as the subject, so the gene is the premise and the disease is what follows. A dataset carrying the gene supplies the implying end and does not carry the disease, which is exactly the case the experiment page exists to show. -
testSubjectBreadthCountsObjectsUnderOnePredicateOnly
@Test public void testSubjectBreadthCountsObjectsUnderOnePredicateOnly()🛑 Subject breadth counts objects under ONE predicate, and the unscoped number it used to report cannot do the job the field was added for.Measured on gemma2 2026-08-27, unscoped:
dimethyl sulfoxide9,BRCA113,biotin15,epithelial cell20. The one row a reader wanted (BRCA1 --has disease--> breast cancer) sits between two ontology closures nobody wanted, so no bar on that number separates them. Asking the endpoint one predicate at a time returned 8, 1, 15 and 3 objects for those four.The fixture is that shape at small scale: one subject bearing three roles and one disease. Unscoped every row of it reads 4 and the disease row is indistinguishable from a role.
-
testAListEnumeratingSubjectIsDroppedWhenACallerSetsASubjectBreadthBar
@Test public void testAListEnumeratingSubjectIsDroppedWhenACallerSetsASubjectBreadthBar()The bar, on the same fixture. A subject enumerating a list under one predicate goes; the row it makes one statement with stays.
-