47ea57080b515e5dad5f658c58feb8944a7e7d61 chmalee Thu Jan 29 15:30:26 2026 -0800 Replace clade/assembly dropdowns with a search bar on most CGIs. Add a recents list to hgGateway and to the species bar and to the 'Genomes' dropdown menu. Track recently selected species in localStorage. Add toGenome and fromGenome arguemnts to hubApi/liftOver in order to find appropriate liftover assemblies, refs #36232 diff --git src/hg/hubApi/findGenome.c src/hg/hubApi/findGenome.c index 0e068e8467a..c7ff4b9935c 100644 --- src/hg/hubApi/findGenome.c +++ src/hg/hubApi/findGenome.c @@ -1,40 +1,42 @@ /* findGenome search functions */ #include "dataApi.h" #include "hgFind.h" #include "cartTrackDb.h" #include "cartJson.h" #include "genark.h" #include "asmAlias.h" #include "assemblyList.h" +#include "liftOver.h" /* will be initialized as this function begins */ static char *genarkTable = NULL; static char *asmListTable = NULL; static boolean statsOnly = FALSE; /* these three are radio button states, only one of these three can be TRUE */ static boolean browserMustExist = TRUE; /* default: browser must exist */ static boolean browserMayExist = FALSE; static boolean browserNotExist = FALSE; static unsigned specificYear = 0; /* from year=1234 argument */ /* from category= reference or representative */ static char *refSeqCategory = NULL; /* from status= one of: latest, replaced or suppressed */ static char *versionStatus = NULL; /* from level= one of complete, chromosome, scaffold or contig */ static char *assemblyLevel = NULL; +static boolean liftable = FALSE; /* hgsql -e 'desc assemblyList;' hgcentraltest +----------------+---------------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +----------------+---------------------+------+-----+---------+-------+ | name | varchar(255) | NO | PRI | NULL | | | priority | int(10) unsigned | YES | | NULL | | | commonName | varchar(511) | YES | | NULL | | | scientificName | varchar(511) | YES | | NULL | | | taxId | int(10) unsigned | YES | | NULL | | | clade | varchar(255) | YES | | NULL | | | description | varchar(1023) | YES | | NULL | | | browserExists | tinyint(3) unsigned | YES | | NULL | | | hubUrl | varchar(511) | YES | | NULL | | @@ -174,122 +176,130 @@ static void addStatus(struct dyString *query) /* versionStatus = latest, replaced or suppressed */ { if (isNotEmpty(versionStatus)) sqlDyStringPrintf(query, " AND versionStatus='%s'", versionStatus); } static void addLevel(struct dyString *query) /* assemblyLevel = complete, chromosome, scaffold or contig */ { if (isNotEmpty(assemblyLevel)) sqlDyStringPrintf(query, " AND assemblyLevel='%s'", assemblyLevel); } +static void addLiftover(struct dyString *query) +/* liftable = are there liftover chains for this assembly */ +{ +if (liftable) + sqlDyStringPrintf(query, " AND exists (select 1 from %s where %s.name = %s.fromDb) ", liftOverChainTable(), asmListTable, liftOverChainTable()); +} + static void addConditions(struct dyString *query) /* add any of the optional conditions */ { addBrowserExists(query); addCategory(query); addStatus(query); addLevel(query); +addLiftover(query); if (specificYear > 0) sqlDyStringPrintf(query, " AND year='%u'", specificYear); } static long long multipleWordSearch(struct sqlConnection *conn, char **words, int wordCount, struct jsonWrite *jw, long long *totalMatchCount) /* perform search on multiple words, prepare json and return number of matches */ { long long itemCount = 0; *totalMatchCount = 0; if (wordCount < 0) return itemCount; /* get the words[] into a single string */ struct dyString *queryDy = dyStringNew(128); dyStringPrintf(queryDy, "%s", words[0]); for (int i = 1; i < wordCount; ++i) dyStringPrintf(queryDy, " %s", words[i]); /* initial SELECT allows any browser exist status, existing or not */ struct dyString *query = dyStringNew(64); -sqlDyStringPrintf(query, "SELECT COUNT(*) FROM %s WHERE MATCH(name, commonName, scientificName, clade, description) AGAINST ('%s' IN BOOLEAN MODE)", asmListTable, queryDy->string); +sqlDyStringPrintf(query, "SELECT COUNT(*) FROM %s ", asmListTable); +sqlDyStringPrintf(query, "WHERE MATCH(name, commonName, scientificName, clade, description) AGAINST ('%s' IN BOOLEAN MODE)", queryDy->string); addConditions(query); /* add optional SELECT options */ long long matchCount = sqlQuickLongLong(conn, query->string); if (matchCount > 0) { *totalMatchCount = matchCount; if (statsOnly) // only counting, nothing returned { // the LIMIT would limit results to maxItemsOutput itemCount = min(maxItemsOutput, matchCount); } // when less than totalMatchCount else { dyStringFree(&query); - query = dyStringNew(64); - sqlDyStringPrintf(query, "SELECT * FROM %s WHERE MATCH(name, commonName, scientificName, clade, description, refSeqCategory, versionStatus, assemblyLevel) AGAINST ('%s' IN BOOLEAN MODE)", asmListTable, queryDy->string); + sqlDyStringPrintf(query, "SELECT * FROM %s ", asmListTable); + sqlDyStringPrintf(query, "WHERE MATCH(name, commonName, scientificName, clade, description, refSeqCategory, versionStatus, assemblyLevel) AGAINST ('%s' IN BOOLEAN MODE)", queryDy->string); addConditions(query); /* add optional SELECT options */ sqlDyStringPrintf(query, " ORDER BY priority LIMIT %d;", maxItemsOutput); struct sqlResult *sr = sqlGetResult(conn, query->string); itemCount = sqlJsonOut(jw, sr); sqlFreeResult(&sr); dyStringFree(&query); } } return itemCount; } static long long oneWordSearch(struct sqlConnection *conn, char *searchWord, struct jsonWrite *jw, long long *totalMatchCount, boolean *prefixSearch) /* perform search on a single word, prepare json and return number of matches * and number of potential matches totalMatchCount */ { long long itemCount = 0; *totalMatchCount = 0; -struct dyString *query = dyStringNew(64); -sqlDyStringPrintf(query, "SELECT COUNT(*) FROM %s WHERE MATCH(name, commonName, scientificName, clade, description, refSeqCategory, versionStatus, assemblyLevel) AGAINST ('%s' IN BOOLEAN MODE)", asmListTable, searchWord); +struct dyString *query = sqlDyStringCreate("SELECT COUNT(*) FROM %s ", asmListTable); +sqlDyStringPrintf(query, "WHERE MATCH(name, commonName, scientificName, clade, description, refSeqCategory, versionStatus, assemblyLevel) AGAINST ('%s' IN BOOLEAN MODE)", searchWord); addConditions(query); /* add optional SELECT options */ long long matchCount = sqlQuickLongLong(conn, query->string); *prefixSearch = FALSE; /* assume not */ if (matchCount < 1) /* no match, add the * wild card match to make a prefix match */ { - dyStringFree(&query); - query = dyStringNew(64); - sqlDyStringPrintf(query, "SELECT COUNT(*) FROM %s WHERE MATCH(name, commonName, scientificName, clade, description, refSeqCategory, versionStatus, assemblyLevel) AGAINST ('%s*' IN BOOLEAN MODE)", asmListTable, searchWord); + dyStringClear(query); + sqlDyStringPrintf(query, "SELECT COUNT(*) FROM %s ", asmListTable); + sqlDyStringPrintf(query, "WHERE MATCH(name, commonName, scientificName, clade, description, refSeqCategory, versionStatus, assemblyLevel) AGAINST ('%s*' IN BOOLEAN MODE)", searchWord); addConditions(query); /* add optional SELECT options */ matchCount = sqlQuickLongLong(conn, query->string); if (matchCount > 0) *prefixSearch = TRUE; } if (matchCount < 1) // nothing found, returning zero return itemCount; *totalMatchCount = matchCount; if (statsOnly) // only counting, nothing returned { // the LIMIT would limit results to maxItemsOutput itemCount = min(maxItemsOutput, matchCount); } // when less than totalMatchCount else { - dyStringFree(&query); - query = dyStringNew(64); - - sqlDyStringPrintf(query, "SELECT * FROM %s WHERE MATCH(name, commonName, scientificName, clade, description, refSeqCategory, versionStatus, assemblyLevel) AGAINST ('%s%s' IN BOOLEAN MODE)", asmListTable, searchWord, *prefixSearch ? "*" : ""); + dyStringClear(query); + sqlDyStringPrintf(query, "SELECT * FROM %s ", asmListTable); + sqlDyStringPrintf(query, "WHERE MATCH(name, commonName, scientificName, clade, description, refSeqCategory, versionStatus, assemblyLevel) AGAINST ('%s%s' IN BOOLEAN MODE)", searchWord, *prefixSearch ? "*" : ""); addConditions(query); /* add optional SELECT options */ sqlDyStringPrintf(query, " ORDER BY priority LIMIT %d;", maxItemsOutput); struct sqlResult *sr = sqlGetResult(conn, query->string); itemCount = sqlJsonOut(jw, sr); sqlFreeResult(&sr); dyStringFree(&query); } return itemCount; } /* static long long oneWordSearch(struct sqlConnection *conn, char *searchWord, struct jsonWrite *jw, boolean *prefixSearch) */ #ifdef NOT // disabled 2025-10-22 static long elapsedTime(struct jsonWrite *jw) { @@ -315,30 +325,31 @@ if (extraArgs) apiErrAbort(err400, err400Msg, "extraneous arguments found for function /findGenome'%s'", extraArgs); boolean asmListExists = sqlTableExists(conn, asmListTable); if (!asmListExists) apiErrAbort(err400, err400Msg, "table central.assemblyList does not exist for /findGenome"); boolean genArkExists = sqlTableExists(conn, genarkTable); if (!genArkExists) apiErrAbort(err400, err400Msg, "table central.%s does not exist for /findGenome", genarkTable); char *yearString = cgiOptionalString(argYear); char *categoryString = cgiOptionalString(argCategory); char *statusString = cgiOptionalString(argStatus); char *levelString = cgiOptionalString(argLevel); +char *liftableStr = cgiOptionalString(argLiftable); /* protect sqlUnsigned from errors */ if (isNotEmpty(yearString)) { struct errCatch *errCatch = errCatchNew(); if (errCatchStart(errCatch)) { specificYear = sqlUnsigned(yearString); if ((specificYear < 1800) || (specificYear > 2100)) apiErrAbort(err400, err400Msg, "year specified '%s' must be >= 1800 and <= 2100", yearString); } errCatchEnd(errCatch); if (errCatch->gotError) apiErrAbort(err400, err400Msg, "can not recognize year '%s' as a number", yearString); } /* probably be better to place this arg checking business into a function @@ -366,30 +377,42 @@ } } if (isNotEmpty(levelString)) { assemblyLevel = cloneString(levelString); toLowerN(assemblyLevel, strlen(assemblyLevel)); if (differentWord(assemblyLevel, "complete")) { if (differentWord(assemblyLevel, "chromosome")) if (differentWord(assemblyLevel, "scaffold")) if (differentWord(assemblyLevel, "contig")) apiErrAbort(err400, err400Msg, "values for argument %s=%s must be one of: 'complete', 'chromosome', 'scaffold' or 'contig'", argLevel, levelString); } } +if (isNotEmpty(liftableStr)) + { + char *lower = cloneString(liftableStr); + tolowers(lower); + if (sameWord(lower, "liftable") || sameWord(lower, "true") || sameWord(lower, "yes") ||sameWord(lower, "on")) + liftable = TRUE; + else if (sameWord(lower, "false") || sameWord(lower, "no") || sameWord(lower, "off")) + liftable = FALSE; + else + apiErrAbort(err400, err400Msg, "unrecognized '%s=%s' argument, must be either 'liftable' or 'true', or completely missing", argLiftable, liftableStr); + } + char *browserExistString = cgiOptionalString(argBrowser); if (NULL == browserExistString) /* set default if none given */ browserExistString = cloneString("mustExist"); if (isNotEmpty(browserExistString)) { /* from radio buttons, only one can be on */ if (sameWord(browserExistString, "mustExist")) { browserMustExist = TRUE; /* default: browser must exist */ browserMayExist = FALSE; browserNotExist = FALSE; } else if (sameWord(browserExistString, "mayExist")) { browserMustExist = FALSE; @@ -432,30 +455,31 @@ apiErrAbort(err400, err400Msg, "search term '%s=%s' should not have more than 5 words for function /findGenome", argQ, searchString); struct jsonWrite *jw = apiStartOutput(); /* show options in effect in JSON return */ jsonWriteString(jw, argBrowser, browserExistString); if (specificYear > 0) jsonWriteNumber(jw, argYear, specificYear); if (isNotEmpty(refSeqCategory)) jsonWriteString(jw, argCategory, refSeqCategory); if (isNotEmpty(versionStatus)) jsonWriteString(jw, argStatus, versionStatus); if (isNotEmpty(assemblyLevel)) jsonWriteString(jw, argLevel, assemblyLevel); +jsonWriteString(jw, argLiftable, liftableStr); long long itemCount = 0; long long totalMatchCount = 0; char **words; AllocArray(words, wordCount); (void) chopByWhite(searchString, words, wordCount); if (1 == wordCount) { boolean doQuote = TRUE; /* already quoted, let it go as-is */ if (startsWith("\"", words[0]) && endsWith(words[0],"\"")) doQuote = FALSE; /* already wildcard, let it go as-is */ if (endsWith(words[0],"*")) doQuote = FALSE;