896eb991fff50f44f18811a05171e36fa111163d
braney
  Thu Sep 24 17:13:20 2026 -0700
quickLift: leave empty blocks out of a protein alignment before lifting it, refs #38249

The UniProt bigPsl files store block sizes in bases, and pslFromBigPsl divides them by
three, so a block shorter than a codon loads with size 0.  pslTransMap rejects such an
alignment and aborts, which replaced the whole lifted SwissProt track with a BUG message.
About one alignment in eight has a block like this.  quickLiftPsl now lifts a copy with
the empty blocks removed.  quickLiftTester has a new case in the shape of Q96ME1-2.

diff --git src/hg/lib/tests/quickLiftTester.c src/hg/lib/tests/quickLiftTester.c
index e29112e585e..a38e4f39598 100644
--- src/hg/lib/tests/quickLiftTester.c
+++ src/hg/lib/tests/quickLiftTester.c
@@ -2,31 +2,31 @@
  *
  * pslTransMap does the mapping in nucleotide space and leaves the result there, so
  * quickLiftPsl has to put the query side back into protein units afterwards.  That fixup
  * has to agree with pslIsProtein(), because every reader downstream asks that question and
  * then multiplies block sizes by three or by one on the answer.  A psl that says "++" while
  * its blocks sit in minus-strand target coordinates passes no check and draws at a third of
  * its length in the wrong place, which is what #38349 found.
  *
  * So each case here runs pslCheck2() over the lifted alignment as well as printing it.
  * PSL_CHECK_IGNORE_INSERT_CNTS is on because pslCheck2's own comment says protein psls do
  * not compute the insert counts consistently; everything else is checked, including the
  * target range, which is where a wrong strand shows up.
  *
  * No database and no files: the chains are built here in the shape quickLiftSourceRanges
  * leaves in the hash, which is the reference on the query side after a chainSwap.
- * refs #38349 */
+ * refs #38349, #38249 */
 
 /* Copyright (C) 2026 The Regents of the University of California
  * See kent/LICENSE or http://genome.ucsc.edu/license/ for licensing information. */
 
 #include "common.h"
 #include "hash.h"
 #include "chain.h"
 #include "psl.h"
 #include "liftOver.h"
 /* for struct sqlConnection, struct cart and struct trackDb, named in prototypes in
  * quickLift.h, which does not declare them itself */
 #include "jksql.h"
 #include "cart.h"
 #include "trackDb.h"
 #include "quickLift.h"
@@ -187,37 +187,61 @@
         pslOnSource("prot", 100, 100, TRUE));
 }
 
 static void mrnaBothStrands()
 /* The same two chains over an mRNA alignment, which the protein fixup must not touch. */
 {
 int refStarts[] = {100}, srcStarts[] = {100}, sizes[] = {300};
 runCase("mRNA, chain on the same strand",
         chainFromBlocks('+', 1, refStarts, srcStarts, sizes),
         pslOnSource("mrna", 100, 100, FALSE));
 runCase("mRNA, chain on the opposite strand",
         chainFromBlocks('-', 1, refStarts, srcStarts, sizes),
         pslOnSource("mrna", 100, 600, FALSE));
 }
 
+static void emptyBlock()
+/* #38249.  The UniProt bigPsl files store block sizes in bases and pslFromBigPsl divides
+ * them by three, so a block shorter than a codon loads with size 0.  pslTransMap rejects
+ * such an alignment and aborts, which took down the whole lifted SwissProt track.  The lift
+ * has to leave the empty block out and map the rest. */
+{
+int refStarts[] = {100}, srcStarts[] = {100}, sizes[] = {600};
+// The shape of Q96ME1-2 in hg19, whose first block is a single base:  query on the minus
+// strand, so the empty block sits at the far end of the query, where pslCheck notices it.
+struct psl *psl = pslNew("prot", 100, 0, 100, srcChrom, seqSize, 100, 400, "-+", 2, 0);
+
+psl->blockCount = 2;
+psl->blockSizes[0] = 0;
+psl->qStarts[0] = 0;
+psl->tStarts[0] = 100;
+psl->blockSizes[1] = 90;
+psl->qStarts[1] = 10;
+psl->tStarts[1] = 130;
+psl->match = 90;
+runCase("protein, a block shorter than a codon",
+        chainFromBlocks('+', 1, refStarts, srcStarts, sizes), psl);
+}
+
 static void noChain()
 /* Nothing covers the alignment, so it is dropped rather than half-lifted. */
 {
 int refStarts[] = {100}, srcStarts[] = {100}, sizes[] = {300};
 runCase("no chain over the alignment",
         chainFromBlocks('+', 1, refStarts, srcStarts, sizes),
         pslOnSource("prot", 100, 700, TRUE));
 }
 
 int main(int argc, char *argv[])
 {
 if (argc != 1)
     usage();
 sameStrandProtein();
 oppositeStrandProtein();
 targetGap();
 splitCodon();
 mrnaBothStrands();
+emptyBlock();
 noChain();
 printf("passed\n");
 return 0;
 }