630eb30bc3695afcf73a19be6e3bc9a2829b365f
chmalee
  Wed May 13 13:04:37 2026 -0700
Make myVariants items bed12+ rather than bed9+, refs #33808

diff --git src/hg/lib/myVariants.c src/hg/lib/myVariants.c
index cf1785a1593..85a7c750a60 100644
--- src/hg/lib/myVariants.c
+++ src/hg/lib/myVariants.c
@@ -3,202 +3,251 @@
 #include "dystring.h"
 #include "jksql.h"
 #include "myVariants.h"
 #include "myVariantsShare.h"
 #include "customTrack.h"
 #include "hdb.h"
 #include "hgConfig.h"
 #include "cheapcgi.h"
 #include "trashDir.h"
 #include "obscure.h"
 #include "wikiLink.h"
 
 void myVariantsStaticLoad(char **row, struct myVariants *ret)
 /* Load a row from myVariants table into ret. The contents of ret will be replaced at the next call to this function. */
 {
+int sizeOne;
 ret->bin = sqlUnsigned(row[0]);
 ret->chrom = row[1];
 ret->chromStart = sqlUnsigned(row[2]);
 ret->chromEnd = sqlUnsigned(row[3]);
 ret->name = row[4];
 ret->score = sqlUnsigned(row[5]);
 safecpy(ret->strand, sizeof(ret->strand), row[6]);
 ret->thickStart = sqlUnsigned(row[7]);
 ret->thickEnd = sqlUnsigned(row[8]);
 ret->itemRgb = sqlUnsigned(row[9]);
-ret->description = row[10];
-ret->db = row[11];
-ret->ref = row[12];
-ret->alt = row[13];
-ret->project = row[14];
-ret->mouseover = row[15];
-ret->id = sqlUnsigned(row[16]);
+ret->blockCount = sqlUnsigned(row[10]);
+sqlSignedDynamicArray(row[11], &ret->blockSizes, &sizeOne);
+assert(sizeOne == ret->blockCount);
+sqlSignedDynamicArray(row[12], &ret->chromStarts, &sizeOne);
+assert(sizeOne == ret->blockCount);
+ret->description = row[13];
+ret->db = row[14];
+ret->ref = row[15];
+ret->alt = row[16];
+ret->project = row[17];
+ret->mouseover = row[18];
+ret->id = sqlUnsigned(row[19]);
 }
 
 struct myVariants *myVariantsLoadByQuery(struct sqlConnection *conn, char *query)
 /* Load all myVariants from table that satisfy the query given. Dispose of this with myVariantsFreeList(). */
 {
 struct myVariants *list = NULL, *el;
 struct sqlResult *sr;
 char **row;
 sr = sqlGetResult(conn, query);
 while ((row = sqlNextRow(sr)) != NULL)
     {
     el = myVariantsLoad(row);
     slAddHead(&list, el);
     }
 slReverse(&list);
 sqlFreeResult(&sr);
 return list;
 }
 
+static char *commaIntList(int *arr, int n)
+/* Build a "n1,n2,...,nN," string from an int array.  Caller frees. */
+{
+struct dyString *dy = dyStringNew(n * 8);
+int i;
+for (i = 0; i < n; i++)
+    dyStringPrintf(dy, "%d,", arr[i]);
+return dyStringCannibalize(&dy);
+}
+
 void myVariantsSaveToDb(struct sqlConnection *conn, struct myVariants *el, char *tableName, int updateSize)
 /* Save myVariants as a row to the table specified by tableName.
  * Uses explicit column names so custom fields in el->customFields are included.
  * If el->name is NULL or empty, fills it in post-INSERT as "Variant N" using
  * the row's auto-increment id; sqlLastAutoId wraps MariaDB's mysql_insert_id,
  * which is per-connection and unaffected by concurrent INSERTs on other
  * connections. */
 {
 struct dyString *update = dyStringNew(updateSize);
-sqlDyStringPrintf(update, "insert into %s (bin,chrom,chromStart,chromEnd,name,score,strand,thickStart,thickEnd,itemRgb,description,db,ref,alt,project,mouseover", tableName);
+sqlDyStringPrintf(update, "insert into %s (bin,chrom,chromStart,chromEnd,name,score,strand,thickStart,thickEnd,itemRgb,blockCount,blockSizes,chromStarts,description,db,ref,alt,project,mouseover", tableName);
 
 /* Append custom field column names */
 struct slPair *cf;
 for (cf = el->customFields; cf != NULL; cf = cf->next)
     sqlDyStringPrintf(update, ",%s", cf->name);
 
 char *insertName = isEmpty(el->name) ? "" : el->name;
-sqlDyStringPrintf(update, ") values (%u,'%s',%u,%u,'%s',%u,'%s',%u,%u,%u,'%s','%s','%s','%s','%s','%s'",
+char *blockSizesStr = commaIntList(el->blockSizes, el->blockCount);
+char *chromStartsStr = commaIntList(el->chromStarts, el->blockCount);
+sqlDyStringPrintf(update, ") values (%u,'%s',%u,%u,'%s',%u,'%s',%u,%u,%u,%u,'%s','%s','%s','%s','%s','%s','%s','%s'",
     el->bin, el->chrom, el->chromStart, el->chromEnd, insertName, el->score, el->strand,
-    el->thickStart, el->thickEnd, el->itemRgb, el->description, el->db, el->ref, el->alt,
-    el->project, el->mouseover);
+    el->thickStart, el->thickEnd, el->itemRgb, el->blockCount, blockSizesStr, chromStartsStr,
+    el->description, el->db, el->ref, el->alt, el->project, el->mouseover);
+freeMem(blockSizesStr);
+freeMem(chromStartsStr);
 
 /* Append custom field values */
 for (cf = el->customFields; cf != NULL; cf = cf->next)
     sqlDyStringPrintf(update, ",'%s'", (char *)cf->val);
 
 sqlDyStringPrintf(update, ")");
 sqlUpdate(conn, update->string);
 dyStringFree(&update);
 
 if (isEmpty(el->name))
     {
     unsigned int newId = sqlLastAutoId(conn);
     struct dyString *nameUpdate = sqlDyStringCreate(
         "update %s set name = 'Variant %u' where id = %u",
         tableName, newId, newId);
     sqlUpdate(conn, dyStringCannibalize(&nameUpdate));
     el->id = newId;
     freez(&el->name);
     struct dyString *dy = dyStringNew(0);
     dyStringPrintf(dy, "Variant %u", newId);
     el->name = dyStringCannibalize(&dy);
     }
 }
 
 struct myVariants *myVariantsLoad(char **row)
 /* Load a myVariants from row fetched with select * from myVariants from database. Dispose of this with myVariantsFree(). */
 {
 struct myVariants *ret;
 AllocVar(ret);
+int sizeOne;
 ret->bin = sqlUnsigned(row[0]);
 ret->chrom = cloneString(row[1]);
 ret->chromStart = sqlUnsigned(row[2]);
 ret->chromEnd = sqlUnsigned(row[3]);
 ret->name = cloneString(row[4]);
 ret->score = sqlUnsigned(row[5]);
 safecpy(ret->strand, sizeof(ret->strand), row[6]);
 ret->thickStart = sqlUnsigned(row[7]);
 ret->thickEnd = sqlUnsigned(row[8]);
 ret->itemRgb = sqlUnsigned(row[9]);
-ret->description = cloneString(row[10]);
-ret->db = cloneString(row[11]);
-ret->ref = cloneString(row[12]);
-ret->alt = cloneString(row[13]);
-ret->project = cloneString(row[14]);
-ret->mouseover = cloneString(row[15]);
-ret->id = sqlUnsigned(row[16]);
+ret->blockCount = sqlUnsigned(row[10]);
+sqlSignedDynamicArray(row[11], &ret->blockSizes, &sizeOne);
+assert(sizeOne == ret->blockCount);
+sqlSignedDynamicArray(row[12], &ret->chromStarts, &sizeOne);
+assert(sizeOne == ret->blockCount);
+ret->description = cloneString(row[13]);
+ret->db = cloneString(row[14]);
+ret->ref = cloneString(row[15]);
+ret->alt = cloneString(row[16]);
+ret->project = cloneString(row[17]);
+ret->mouseover = cloneString(row[18]);
+ret->id = sqlUnsigned(row[19]);
 return ret;
 }
 
 struct myVariants *myVariantsLoadAll(char *fileName)
 /* Load all myVariants from a whitespace-separated file. Dispose of this with myVariantsFreeList(). */
 {
 struct myVariants *list = NULL, *el;
 struct lineFile *lf = lineFileOpen(fileName, TRUE);
-char *row[17];
+char *row[MYVARIANTS_NUM_COLS];
 while (lineFileRow(lf, row))
     {
     el = myVariantsLoad(row);
     slAddHead(&list, el);
     }
 lineFileClose(&lf);
 slReverse(&list);
 return list;
 }
 
 struct myVariants *myVariantsLoadAllByChar(char *fileName, char chopper)
 /* Load all myVariants from a chopper separated file. Dispose of this with myVariantsFreeList(). */
 {
 struct myVariants *list = NULL, *el;
 struct lineFile *lf = lineFileOpen(fileName, TRUE);
-char *row[17];
+char *row[MYVARIANTS_NUM_COLS];
 while (lineFileNextCharRow(lf, chopper, row, ArraySize(row)))
     {
     el = myVariantsLoad(row);
     slAddHead(&list, el);
     }
 lineFileClose(&lf);
 slReverse(&list);
 return list;
 }
 
 struct myVariants *myVariantsCommaIn(char **pS, struct myVariants *ret)
 /* Create a myVariants out of a comma separated string. This will fill in ret if non-null, otherwise will return a new myVariants */
 {
 char *s = *pS;
 if (ret == NULL)
     AllocVar(ret);
 ret->bin = sqlUnsignedComma(&s);
 ret->chrom = sqlStringComma(&s);
 ret->chromStart = sqlUnsignedComma(&s);
 ret->chromEnd = sqlUnsignedComma(&s);
 ret->name = sqlStringComma(&s);
 ret->score = sqlUnsignedComma(&s);
 sqlFixedStringComma(&s, ret->strand, sizeof(ret->strand));
 ret->thickStart = sqlUnsignedComma(&s);
 ret->thickEnd = sqlUnsignedComma(&s);
 ret->itemRgb = sqlUnsignedComma(&s);
+ret->blockCount = sqlUnsignedComma(&s);
+    {
+    int i;
+    s = sqlEatChar(s, '{');
+    if (ret->blockCount > 0)
+        AllocArray(ret->blockSizes, ret->blockCount);
+    for (i=0; i<ret->blockCount; ++i)
+        ret->blockSizes[i] = sqlSignedComma(&s);
+    s = sqlEatChar(s, '}');
+    s = sqlEatChar(s, ',');
+    }
+    {
+    int i;
+    s = sqlEatChar(s, '{');
+    if (ret->blockCount > 0)
+        AllocArray(ret->chromStarts, ret->blockCount);
+    for (i=0; i<ret->blockCount; ++i)
+        ret->chromStarts[i] = sqlSignedComma(&s);
+    s = sqlEatChar(s, '}');
+    s = sqlEatChar(s, ',');
+    }
 ret->description = sqlStringComma(&s);
 ret->db = sqlStringComma(&s);
 ret->ref = sqlStringComma(&s);
 ret->alt = sqlStringComma(&s);
 ret->project = sqlStringComma(&s);
 ret->mouseover = sqlStringComma(&s);
 ret->id = sqlUnsignedComma(&s);
 *pS = s;
 return ret;
 }
 
 void myVariantsFree(struct myVariants **pEl)
 /* Free a single dynamically allocated myVariants such as created with myVariantsLoad(). */
 {
 struct myVariants *el;
 if ((el = *pEl) == NULL) return;
 freeMem(el->chrom);
 freeMem(el->name);
+freeMem(el->blockSizes);
+freeMem(el->chromStarts);
 freeMem(el->description);
 freeMem(el->db);
 freeMem(el->ref);
 freeMem(el->alt);
 freeMem(el->project);
 freeMem(el->mouseover);
 slPairFreeValsAndList(&el->customFields);
 freez(pEl);
 }
 
 void myVariantsFreeList(struct myVariants **pList)
 /* Free a list of dynamically allocated myVariants's */
 {
 struct myVariants *el, *next;
 for (el = *pList; el != NULL; el = next)
@@ -226,30 +275,54 @@
 fprintf(f, "%s", el->name);
 if (sep == ',') fputc('"',f);
 fputc(sep,f);
 fprintf(f, "%u", el->score);
 fputc(sep,f);
 if (sep == ',') fputc('"',f);
 fprintf(f, "%s", el->strand);
 if (sep == ',') fputc('"',f);
 fputc(sep,f);
 fprintf(f, "%u", el->thickStart);
 fputc(sep,f);
 fprintf(f, "%u", el->thickEnd);
 fputc(sep,f);
 fprintf(f, "%u", el->itemRgb);
 fputc(sep,f);
+fprintf(f, "%u", el->blockCount);
+fputc(sep,f);
+    {
+    int i;
+    if (sep == ',') fputc('{',f);
+    for (i=0; i<el->blockCount; ++i)
+        {
+        fprintf(f, "%d", el->blockSizes[i]);
+        fputc(',', f);
+        }
+    if (sep == ',') fputc('}',f);
+    }
+fputc(sep,f);
+    {
+    int i;
+    if (sep == ',') fputc('{',f);
+    for (i=0; i<el->blockCount; ++i)
+        {
+        fprintf(f, "%d", el->chromStarts[i]);
+        fputc(',', f);
+        }
+    if (sep == ',') fputc('}',f);
+    }
+fputc(sep,f);
 if (sep == ',') fputc('"',f);
 fprintf(f, "%s", el->description);
 if (sep == ',') fputc('"',f);
 fputc(sep,f);
 if (sep == ',') fputc('"',f);
 fprintf(f, "%s", el->db);
 if (sep == ',') fputc('"',f);
 fputc(sep,f);
 if (sep == ',') fputc('"',f);
 fprintf(f, "%s", el->ref);
 if (sep == ',') fputc('"',f);
 fputc(sep,f);
 if (sep == ',') fputc('"',f);
 fprintf(f, "%s", el->alt);
 if (sep == ',') fputc('"',f);
@@ -268,30 +341,33 @@
 
 static char *myVariantsAutoSqlString =
 "table myVariants\n"
 "\"An item in a myVariants type track.\"\n"
 "    (\n"
 "    uint bin;         \"Bin for range index\"\n"
 "    string chrom;     \"Reference sequence chromosome or scaffold\"\n"
 "    uint   chromStart;\"Start position in chromosome\"\n"
 "    uint   chromEnd;  \"End position in chromosome\"\n"
 "    string name;      \"Name of item - up to 16 chars\"\n"
 "    uint  score;      \"0-1000.  Higher numbers are darker.\"\n"
 "    char[1] strand;   \"+ or - for strand\"\n"
 "    uint   thickStart;\"Start of thick part\"\n"
 "    uint   thickEnd;  \"End position of thick part\"\n"
 "    uint itemRgb;     \"RGB 8 bits each as in bed\"\n"
+"    uint blockCount;  \"Number of blocks\"\n"
+"    int[blockCount] blockSizes; \"Comma separated list of block sizes\"\n"
+"    int[blockCount] chromStarts; \"Start positions relative to chromStart\"\n"
 "    lstring description; \"Longer item description\"\n"
 "    string db;        \"database name of this annotation\"\n"
 "    string ref;       \"reference allele\"\n"
 "    string alt;       \"alternate allele\"\n"
 "    string project;    \"project name for grouping variants\"\n"
 "    string mouseover;  \"short mouseover text for hover display\"\n"
 "    uint id;          \"Unique ID for item\"\n"
 "    )\n"
 ;
 
 struct asObject *myVariantsAsObj()
 /* Return asObject describing fields of myVariants */
 {
 return asParseText(myVariantsAutoSqlString);
 }
@@ -543,30 +619,33 @@
 else
     {
     struct sqlConnection *conn = hAllocConn(CUSTOM_TRASH);
     struct dyString *createTable = sqlDyStringCreate(
             "CREATE TABLE %s.%s (\n"
             "    bin int unsigned not null,\n"
             "    chrom varchar(255) not null,\n"
             "    chromStart int unsigned not null,\n"
             "    chromEnd int unsigned not null,\n"
             "    name varchar(255) not null,\n"
             "    score int unsigned not null,\n"
             "    strand char(1) not null,\n"
             "    thickStart int unsigned not null,\n"
             "    thickEnd int unsigned not null,\n"
             "    itemRgb int unsigned not null,\n"
+            "    blockCount int unsigned not null,\n"
+            "    blockSizes longblob not null,\n"
+            "    chromStarts longblob not null,\n"
             "    description longblob not null,\n"
             "    db varchar(255) not null,\n"
             "    ref varchar(255) not null,\n"
             "    alt varchar(255) not null,\n"
             "    project varchar(255) not null,\n"
             "    mouseover varchar(255) not null,\n"
             "    id int auto_increment,\n"
             "    PRIMARY KEY(id),\n"
             "    INDEX(chrom(16),bin),\n"
             "    INDEX(db),\n"
             "    INDEX(project)\n"
             ") ENGINE=InnoDB;", db, tableName);
     sqlUpdate(conn, dyStringCannibalize(&createTable));
     return myVariantsGetDbTable(userName);
     }
@@ -907,32 +986,33 @@
 struct slName *projects = NULL;
 char query[512];
 sqlSafef(query, sizeof(query),
     "SELECT DISTINCT project FROM %s WHERE project IS NOT NULL AND project != '' ORDER BY project",
     dbTable);
 projects = sqlQuickList(conn, query);
 hFreeConn(&conn);
 return projects;
 }
 
 /* Built-in column names from myVariants.as - any column NOT in this list is a
  * user-added custom column.  Filtering by name (rather than index) is robust
  * against column reordering or future schema changes. */
 static const char *builtInColumns[] = {
     "bin", "chrom", "chromStart", "chromEnd", "name", "score", "strand",
-    "thickStart", "thickEnd", "itemRgb", "description", "db", "ref", "alt",
-    "project", "mouseover", "id",
+    "thickStart", "thickEnd", "itemRgb", "blockCount", "blockSizes",
+    "chromStarts", "description", "db", "ref", "alt", "project",
+    "mouseover", "id",
 };
 
 static boolean isBuiltInColumn(char *name)
 {
 int i;
 for (i = 0;  i < ArraySize(builtInColumns);  i++)
     if (sameString(name, builtInColumns[i]))
         return TRUE;
 return FALSE;
 }
 
 struct slName *myVariantsGetCustomFields(char *userName)
 /* Return list of user-added custom column names for this user's myVariants table.
  * Excludes built-in columns and _hidden_ prefixed columns.
  * Caller must slFreeList the result. Returns NULL if no custom fields or table doesn't exist. */