630eb30bc3695afcf73a19be6e3bc9a2829b365f chmalee Wed May 13 13:04:37 2026 -0700 Make myVariants items bed12+ rather than bed9+, refs #33808 diff --git src/hg/hgTracks/myVariantsTrack.c src/hg/hgTracks/myVariantsTrack.c index 2f098d1408b..50fea157b19 100644 --- src/hg/hgTracks/myVariantsTrack.c +++ src/hg/hgTracks/myVariantsTrack.c @@ -14,32 +14,33 @@ #include "binRange.h" #include "myVariants.h" #include "myVariantsShare.h" #include "jsonParse.h" #include "sqlNum.h" #include "customFactory.h" #include "hgConfig.h" #include "htmlColor.h" #include "wikiLink.h" #include "hgFind.h" #include "hgHgvs.h" #include "jsonWrite.h" static char *reservedFieldNames[] = { "bin", "chrom", "chromStart", "chromEnd", "name", "score", "strand", - "thickStart", "thickEnd", "itemRgb", "description", "db", "ref", "alt", - "project", "mouseover", "id", NULL + "thickStart", "thickEnd", "itemRgb", "blockCount", "blockSizes", + "chromStarts", "description", "db", "ref", "alt", "project", + "mouseover", "id", NULL }; static char *truncateSeq(char *seq, int maxLen) /* Return seq cloned and truncated to maxLen with "..." appended if longer. * NULL or empty input returns NULL. */ { if (isEmpty(seq)) return NULL; if (strlen(seq) <= (size_t)maxLen) return cloneString(seq); struct dyString *dy = dyStringNew(maxLen + 4); dyStringAppendN(dy, seq, maxLen); dyStringAppend(dy, "..."); return dyStringCannibalize(&dy); } @@ -161,30 +162,96 @@ for (i = 1; name[i] != '\0'; i++) { if (!isalnum(name[i]) && name[i] != '_') return FALSE; } /* Check against reserved names */ int j; for (j = 0; reservedFieldNames[j] != NULL; j++) { if (sameString(name, reservedFieldNames[j])) return FALSE; } return TRUE; } +static boolean hasTabOrNewline(char *s) +/* TRUE if s contains \t, \n, or \r. */ +{ +return s != NULL && (strchr(s, '\t') != NULL || + strchr(s, '\n') != NULL || + strchr(s, '\r') != NULL); +} + +static void validateItemBlocks(struct myVariants *item) +/* Format item as a 12-column BED row and run it through loadAndValidateBed + * (isCt=TRUE) for the canonical block checks. lineFileAbort fires errAbort + * on failure, the same error path used elsewhere in this file. */ +{ +/* Reject embedded tab/newline in chrom and name: lineFileNext would + * truncate the row and chopByChar would leave trailing row[] slots + * uninitialized for loadAndValidateBed to dereference. */ +if (hasTabOrNewline(item->chrom)) + errAbort("chrom contains illegal whitespace"); +if (hasTabOrNewline(item->name)) + errAbort("name contains illegal whitespace"); + +struct dyString *dy = dyStringNew(256); +int i; +dyStringPrintf(dy, "%s\t%u\t%u\t%s\t%u\t%s\t%u\t%u\t%u\t%u\t", + item->chrom, item->chromStart, item->chromEnd, + isEmpty(item->name) ? "x" : item->name, + item->score, item->strand, item->thickStart, item->thickEnd, + item->itemRgb, item->blockCount); +for (i = 0; i < item->blockCount; i++) + dyStringPrintf(dy, "%d,", item->blockSizes[i]); +dyStringAppendC(dy, '\t'); +for (i = 0; i < item->blockCount; i++) + dyStringPrintf(dy, "%d,", item->chromStarts[i]); + +/* lineFileOnString takes the buffer by pointer, but lineFileClose does + * not free it (the fd<0 branch skips the free). Hold the pointer + * separately and free it after lineFileClose. */ +char *buf = dyStringCannibalize(&dy); +struct lineFile *lf = lineFileOnString("myVariantsBlocks", TRUE, buf); +char *row[12]; +char *line = NULL; +lineFileNext(lf, &line, NULL); +int got = chopByChar(line, '\t', row, ArraySize(row)); +if (got != 12) + errAbort("validateItemBlocks: chopped %d fields, expected 12", got); +struct bed tmp; +ZeroVar(&tmp); +loadAndValidateBed(row, 12, 12, lf, &tmp, NULL, TRUE); +lineFileClose(&lf); +freeMem(buf); +freeMem(tmp.blockSizes); +freeMem(tmp.chromStarts); +} + +static void ensureItemBlocks(struct myVariants *item) +/* If item has no blocks, synthesize a single full-span block. */ +{ +if (item->blockCount > 0) + return; +item->blockCount = 1; +AllocArray(item->blockSizes, 1); +AllocArray(item->chromStarts, 1); +item->blockSizes[0] = item->chromEnd - item->chromStart; +item->chromStarts[0] = 0; +} + void myVariantsJsCommand(char *command, struct track *trackList, struct hash *trackHash) /* Execute some command sent to us from the javaScript. All we know for sure is that * the first word of the command is "myVariants." We expect it to be of format: * myVariants * where jsonData is a JSON string containing the item details */ { if (!cfgOptionBooleanDefault("doMyVariants", FALSE)) return; char *userName = getUserName(); if (userName == NULL) { warn("You must be logged in to add an annotation."); return; } @@ -351,30 +418,68 @@ item->name = cloneString(name); item->score = score; item->strand[0] = strand[0]; item->itemRgb = color; item->description = cloneString(description); item->ref = cloneString(ref); item->alt = cloneString(alt); item->db = database; item->project = cloneString(project); item->mouseover = cloneString(mouseover); } if (!item) return; +/* Parse blocks from JSON if present. Empty / missing means single full-span + * block; ensureItemBlocks synthesizes that below. */ +struct jsonElement *blockSizesJson = jsonFindNamedField(json, "jsonData", "blockSizes"); +struct jsonElement *chromStartsJson = jsonFindNamedField(json, "jsonData", "chromStarts"); +if (blockSizesJson && blockSizesJson->type == jsonList && + chromStartsJson && chromStartsJson->type == jsonList) + { + int sizeN = slCount(blockSizesJson->val.jeList); + int startN = slCount(chromStartsJson->val.jeList); + if (sizeN != startN) + errAbort("blockSizes and chromStarts lengths differ (%d vs %d)", sizeN, startN); + if (sizeN > 0) + { + item->blockCount = sizeN; + AllocArray(item->blockSizes, sizeN); + AllocArray(item->chromStarts, sizeN); + struct slRef *el; + int i = 0; + for (el = blockSizesJson->val.jeList; el != NULL; el = el->next, i++) + { + struct jsonElement *child = (struct jsonElement *)el->val; + if (child->type != jsonNumber) + errAbort("blockSizes[%d] is not a number", i); + item->blockSizes[i] = child->val.jeNumber; + } + i = 0; + for (el = chromStartsJson->val.jeList; el != NULL; el = el->next, i++) + { + struct jsonElement *child = (struct jsonElement *)el->val; + if (child->type != jsonNumber) + errAbort("chromStarts[%d] is not a number", i); + item->chromStarts[i] = child->val.jeNumber; + } + } + } +ensureItemBlocks(item); +validateItemBlocks(item); + /* Parse custom fields from JSON payload and ALTER TABLE as needed */ char *tableName = myVariantsCreateTable(userName); struct sqlConnection *conn = hAllocConn(CUSTOM_TRASH); struct jsonElement *extraFieldsJson = jsonFindNamedField(json, "jsonData", "extraFields"); if (extraFieldsJson && extraFieldsJson->type == jsonList) { struct slRef *el; for (el = extraFieldsJson->val.jeList; el != NULL; el = el->next) { struct jsonElement *cfObj = el->val; char *cfName = jsonOptionalStringField(cfObj, "name", NULL); char *cfValue = jsonOptionalStringField(cfObj, "value", ""); if (isEmpty(cfName) || !isValidFieldName(cfName)) continue; @@ -462,30 +567,121 @@ cartRemove(cart, varName); } return; } struct dyString *sql = sqlDyStringCreate("update %s set %s='%s' where id=%d", tableName, fieldName, newVal, id); if (isNotEmpty(scopeDb)) sqlDyStringPrintf(sql, " and db='%s'", scopeDb); if (isNotEmpty(scopeProject) && !sameString(scopeProject, "*")) sqlDyStringPrintf(sql, " and project='%s'", scopeProject); sqlUpdate(conn, sql->string); dyStringFree(&sql); cartRemove(cart, varName); } +static void updateBlocksFields(char *trackName, struct sqlConnection *conn, + char *tableName, int id, char *scopeProject, char *scopeDb) +/* Pull _blockCount, _blockSizes, _chromStarts from the cart, + * validate jointly against the row's current chromStart/chromEnd via + * loadAndValidateBed, and UPDATE all three columns in one statement. + * No-op if any of the three cart vars are missing. */ +{ +char vC[128], vS[128], vT[128]; +safef(vC, sizeof vC, "%s_blockCount", trackName); +safef(vS, sizeof vS, "%s_blockSizes", trackName); +safef(vT, sizeof vT, "%s_chromStarts", trackName); +char *cartCount = cartOptionalString(cart, vC); +char *cartSizes = cartOptionalString(cart, vS); +char *cartStarts = cartOptionalString(cart, vT); +if (cartCount == NULL || cartSizes == NULL || cartStarts == NULL) + return; +/* Clone the cart strings before removing the cart vars: if validation + * errAborts below, the cart vars must not survive to corrupt the next + * edit on a different row. */ +char *blockCountStr = cloneString(cartCount); +char *blockSizesStr = cloneString(cartSizes); +char *chromStartsStr = cloneString(cartStarts); +cartRemove(cart, vC); +cartRemove(cart, vS); +cartRemove(cart, vT); + +struct dyString *q = sqlDyStringCreate( + "select chrom,chromStart,chromEnd,name,score,strand,thickStart,thickEnd,itemRgb" + " from %s where id=%d", tableName, id); +if (isNotEmpty(scopeDb)) + sqlDyStringPrintf(q, " and db='%s'", scopeDb); +if (isNotEmpty(scopeProject) && !sameString(scopeProject, "*")) + sqlDyStringPrintf(q, " and project='%s'", scopeProject); +struct sqlResult *sr = sqlGetResult(conn, q->string); +dyStringFree(&q); +char **row = sqlNextRow(sr); +if (row == NULL) + { + sqlFreeResult(&sr); + freeMem(blockCountStr); + freeMem(blockSizesStr); + freeMem(chromStartsStr); + return; + } +struct myVariants item; +ZeroVar(&item); +item.chrom = cloneString(row[0]); +item.chromStart = sqlUnsigned(row[1]); +item.chromEnd = sqlUnsigned(row[2]); +item.name = cloneString(row[3]); +item.score = sqlUnsigned(row[4]); +safecpy(item.strand, sizeof(item.strand), row[5]); +item.thickStart = sqlUnsigned(row[6]); +item.thickEnd = sqlUnsigned(row[7]); +item.itemRgb = sqlUnsigned(row[8]); +sqlFreeResult(&sr); + +item.blockCount = sqlUnsigned(blockCountStr); +char *sizesDup = cloneString(blockSizesStr); +char *startsDup = cloneString(chromStartsStr); +int n; +sqlSignedDynamicArray(sizesDup, &item.blockSizes, &n); +if (n != item.blockCount) + errAbort("blockSizes count %d != blockCount %u", n, item.blockCount); +sqlSignedDynamicArray(startsDup, &item.chromStarts, &n); +if (n != item.blockCount) + errAbort("chromStarts count %d != blockCount %u", n, item.blockCount); +freeMem(sizesDup); +freeMem(startsDup); + +validateItemBlocks(&item); + +struct dyString *upd = sqlDyStringCreate( + "update %s set blockCount=%u,blockSizes='%s',chromStarts='%s' where id=%d", + tableName, item.blockCount, blockSizesStr, chromStartsStr, id); +if (isNotEmpty(scopeDb)) + sqlDyStringPrintf(upd, " and db='%s'", scopeDb); +if (isNotEmpty(scopeProject) && !sameString(scopeProject, "*")) + sqlDyStringPrintf(upd, " and project='%s'", scopeProject); +sqlUpdate(conn, upd->string); +dyStringFree(&upd); + +freeMem(item.chrom); +freeMem(item.name); +freeMem(item.blockSizes); +freeMem(item.chromStarts); +freeMem(blockCountStr); +freeMem(blockSizesStr); +freeMem(chromStartsStr); +} + static void myVariantsEditOrDelete(char *trackName, struct sqlConnection *conn, char *tableName) /* Troll through cart variables looking for things that indicate user edited item * or deleted it in hgc, and carry out edits. See hgc/myVariantsClick.c. */ { char varName[128]; char sql[256]; safef(varName, sizeof(varName), "%s_%s", trackName, "id"); char *idString = cartOptionalString(cart, varName); if (idString != NULL) { int id = sqlUnsigned(idString); idString = NULL; // Will be no good after cartRemove cartRemove(cart, varName); // Remove so only do edits once. /* Handle cancel. */ @@ -513,30 +709,31 @@ char ctVar[256]; safef(ctVar, sizeof ctVar, CT_FILE_VAR_PREFIX "%s", database); cartSetString(cart, ctVar, ctFile); freeMem(ctFile); } } return; } /* Handle edits. Owner edits their own table, no project/db scope filter. */ updateTextField(trackName, conn, tableName, "name", id, NULL, NULL); updateTextField(trackName, conn, tableName, "description", id, NULL, NULL); updateTextField(trackName, conn, tableName, "itemRgb", id, NULL, NULL); updateTextField(trackName, conn, tableName, "chromStart", id, NULL, NULL); updateTextField(trackName, conn, tableName, "chromEnd", id, NULL, NULL); + updateBlocksFields(trackName, conn, tableName, id, NULL, NULL); updateTextField(trackName, conn, tableName, "ref", id, NULL, NULL); updateTextField(trackName, conn, tableName, "alt", id, NULL, NULL); updateTextField(trackName, conn, tableName, "project", id, NULL, NULL); updateTextField(trackName, conn, tableName, "mouseover", id, NULL, NULL); /* Update any custom fields */ { char *editUserName = getUserName(); struct slName *customCols = myVariantsGetCustomFields(editUserName); struct slName *col; for (col = customCols; col != NULL; col = col->next) updateTextField(trackName, conn, tableName, col->name, id, NULL, NULL); slFreeList(&customCols); } /* Trigger CT refresh after edits */ { @@ -580,31 +777,39 @@ AllocVar(bed); /* bed->name is the item identifier passed to hgc, which parses "id name" * to look up the row by primary key. myVariantsName() strips the "id " * prefix for the displayed label. */ char buf[64]; safef(buf, sizeof(buf), "%u %s", item->id, item->name); bed->chrom = item->chrom; bed->chromStart = item->chromStart; bed->chromEnd = item->chromEnd; bed->name = cloneString(buf); bed->score = item->score; bed->strand[0] = item->strand[0]; bed->thickStart = item->thickStart; bed->thickEnd = item->thickEnd; bed->itemRgb = item->itemRgb; - lf = bedMungToLinkedFeatures(&bed, tdb, 9, 0, 1000, TRUE); + bed->blockCount = item->blockCount; + if (item->blockCount > 0) + { + bed->blockSizes = cloneMem(item->blockSizes, + item->blockCount * sizeof(int)); + bed->chromStarts = cloneMem(item->chromStarts, + item->blockCount * sizeof(int)); + } + lf = bedMungToLinkedFeatures(&bed, tdb, 12, 0, 1000, TRUE); dyStringClear(mouseover); if (item->mouseover && isNotEmpty(item->mouseover)) lf->mouseOver = cloneString(item->mouseover); else { dyStringPrintf(mouseover, "%s", item->name); if (item->ref != NULL && isNotEmpty(item->ref)) dyStringPrintf(mouseover, "
Ref: %s", item->ref); if (item->alt != NULL && isNotEmpty(item->alt)) dyStringPrintf(mouseover, "
Alt: %s", item->alt); lf->mouseOver = cloneString(dyStringContents(mouseover)); } slAddHead(&lfList, lf); } sqlFreeResult(&sr); @@ -1019,30 +1224,31 @@ cartRemove(cart, varName); myVariantsShareFree(&liveShare); continue; } /* Apply field edits, scoped to the share's project/db so a recipient * cannot edit rows in projects/assemblies they were not granted. */ char *sp = liveShare->project; char *sd = liveShare->db; struct sqlConnection *conn = hAllocConn(CUSTOM_TRASH); updateTextField(trackName, conn, tableName, "name", id, sp, sd); updateTextField(trackName, conn, tableName, "description", id, sp, sd); updateTextField(trackName, conn, tableName, "itemRgb", id, sp, sd); updateTextField(trackName, conn, tableName, "chromStart", id, sp, sd); updateTextField(trackName, conn, tableName, "chromEnd", id, sp, sd); + updateBlocksFields(trackName, conn, tableName, id, sp, sd); updateTextField(trackName, conn, tableName, "ref", id, sp, sd); updateTextField(trackName, conn, tableName, "alt", id, sp, sd); updateTextField(trackName, conn, tableName, "mouseover", id, sp, sd); struct slName *customCols = myVariantsGetCustomFields(liveShare->ownerUser); struct slName *col; for (col = customCols; col != NULL; col = col->next) updateTextField(trackName, conn, tableName, col->name, id, sp, sd); slFreeList(&customCols); hFreeConn(&conn); myVariantsShareFree(&liveShare); } hashElFreeList(&shareVars); } boolean myVariantsTrackEnabled()