9af188ea6147f9edb20bd531d3ec988501cf997c
chmalee
  Fri Feb 6 12:18:25 2026 -0800
Fix jsonOutputArrays columnTypes output when more than one track is requested with /getData/track. Leaves /list/schema alone. Add option to hgTracks Downloads -> Download track data in view menu to include column headers in output, refs #36858

diff --git src/hg/hubApi/getData.c src/hg/hubApi/getData.c
index a5440083cfb..89c2f62c026 100644
--- src/hg/hubApi/getData.c
+++ src/hg/hubApi/getData.c
@@ -84,31 +84,31 @@
 	jsonWriteObjectStart(jw, NULL);
 	for (i = 0; i < columnCount; ++i)
 	    jsonDatumOut(jw, columnNames[i], row[i], jsonTypes[i]);
 	jsonWriteObjectEnd(jw);
 	}
     ++itemCount;
     }
 sqlFreeResult(&sr);
 if ((itemCount + itemsDone) >= maxItemsOutput)
     reachedMaxItems = TRUE;
 return itemCount;
 }	/*	static unsigned sqlQueryJsonOutput(...) */
 
 static void tableDataOutput(char *db, struct trackDb *tdb,
     struct sqlConnection *conn, struct jsonWrite *jw, char *track,
-    char *chrom, unsigned start, unsigned end)
+    char *chrom, unsigned start, unsigned end, struct jsonWrite *columnTypesJw)
 /* output the SQL table data for given track */
 {
 /* for MySQL select statements, name for 'chrom' 'start' 'end' to use
  *     for a table which has different names than that
  */
 char chromName[256];
 char startName[256];
 char endName[256];
 
 /* defaults, normal stuff */
 safef(chromName, sizeof(chromName), "chrom");
 safef(startName, sizeof(startName), "chromStart");
 safef(endName, sizeof(endName), "chromEnd");
 
 /* 'track' name in trackDb often refers to a SQL 'table' */
@@ -192,54 +192,54 @@
      */
     sqlDyStringPrintf(query, "select * from %s", splitSqlTable);
     }
 else if (0 == (start + end))	/* have chrom, no start,end == full chr */
     {
     if (! sqlColumnExists(conn, splitSqlTable, chromName))
 	apiErrAbort(err400, err400Msg, "track '%s' is not a position track, request track without chrom specification, genome: '%s'", track, db);
 
     jsonWriteString(jw, "chrom", chrom);
     struct chromInfo *ci = hGetChromInfo(db, chrom);
     jsonWriteNumber(jw, "start", (long long)0);
     jsonWriteNumber(jw, "end", (long long)ci->size);
     if (tdb && isWiggleDataTable(tdb->type))
 	{
 	if (jsonOutputArrays || debug)
-	    wigColumnTypes(jw);
+	    wigColumnTypes(columnTypesJw, track);
 	jsonWriteListStart(jw, chrom);
         itemsReturned += wigTableDataOutput(jw, db, splitSqlTable, chrom, 0, ci->size, 0);
 	jsonWriteListEnd(jw);
         return;	/* DONE */
 	}
     else
 	{
 	if (sqlColumnExists(conn, splitSqlTable, startName))
 	    sqlDyStringPrintf(query, "select * from %s where %s='%s' order by %s", splitSqlTable, chromName, chrom, startName);
         else
 	    sqlDyStringPrintf(query, "select * from %s where %s='%s'", splitSqlTable, chromName, chrom);
 	}
     }
 else	/* fully specified chrom:start-end */
     {
     if (! sqlColumnExists(conn, splitSqlTable, chromName))
 	apiErrAbort(err400, err400Msg, "track '%s' is not a position track, request track without chrom and start,end specifications, genome: '%s'", track, db);
 
     jsonWriteString(jw, "chrom", chrom);
     if (tdb && isWiggleDataTable(tdb->type))
 	{
 	if (jsonOutputArrays || debug)
-	    wigColumnTypes(jw);
+	    wigColumnTypes(columnTypesJw, track);
 	jsonWriteListStart(jw, chrom);
         itemsReturned += wigTableDataOutput(jw, db, splitSqlTable, chrom, start, end, 0);
 	jsonWriteListEnd(jw);
         return;	/* DONE */
 	}
     else
 	{
 	sqlDyStringPrintf(query, "select * from %s where ", splitSqlTable);
 	if (sqlColumnExists(conn, splitSqlTable, startName))
 	    {
 	    if (hti->hasBin)
 		hAddBinToQuery(start, end, query);
 	    sqlDyStringPrintf(query, "%s='%s' AND %s > %u AND %s < %u ORDER BY %s", chromName, chrom, endName, start, startName, end, startName);
 	    }
 	else
@@ -250,32 +250,32 @@
 if (debug)
     jsonWriteString(jw, "select", query->string);
 
 /* continuing, could be wiggle output with no chrom specified */
 char **columnNames = NULL;
 char **columnTypes = NULL;
 int *jsonTypes = NULL;
 struct asObject *as = asForTable(conn, splitSqlTable, tdb);
 if (! as)
     apiErrAbort(err500, err500Msg, "can not find schema definition for table '%s', genome: '%s'", splitSqlTable, db);
 struct asColumn *columnEl = as->columnList;
 int asColumnCount = slCount(columnEl);
 int columnCount = tableColumns(conn, splitSqlTable, &columnNames, &columnTypes, &jsonTypes);
 if (jsonOutputArrays || debug)
     {
-    outputSchema(tdb, jw, columnNames, columnTypes, jsonTypes, hti,
-	columnCount, asColumnCount, columnEl);
+    outputSchema(tdb, columnTypesJw, columnNames, columnTypes, jsonTypes, hti,
+	columnCount, asColumnCount, columnEl, track);
     }
 
 unsigned itemsDone = 0;
 
 /* empty chrom, needs to run through all chrom names */
 if (isEmpty(chrom))
     {
     jsonWriteObjectStart(jw, track);	/* begin track data output */
     char fullTableName[256];
     struct chromInfo *ciList = createChromInfoList(NULL, db);
     slSort(ciList, chromInfoCmp);
     struct chromInfo *ci = ciList;
     for ( ; ci && itemsDone < maxItemsOutput; ci = ci->next )
 	{
 	jsonWriteListStart(jw, ci->chrom);	/* starting a chrom output */
@@ -458,30 +458,37 @@
   hubUrl);
 
 hubAliasSetup(hubGenome);
 
 char *chrom = chrOrAlias(genome, hubUrl);
 
 struct trackDb *tdb = obtainTdb(hubGenome, NULL);
 
 if (NULL == tdb)
     apiErrAbort(err400, err400Msg, "failed to find a track hub definition in genome=%s for endpoint '/getData/track'  given hubUrl='%s'", genome, hubUrl);
 
 struct jsonWrite *jw = apiStartOutput();
 jsonWriteString(jw, "hubUrl", hubUrl);
 jsonWriteString(jw, "genome", genome);
 
+struct jsonWrite *columnTypesJw = NULL;
+if (jsonOutputArrays || debug)
+    {
+    columnTypesJw = jsonWriteNew();
+    jsonWriteObjectStart(columnTypesJw, "columnTypes");
+    }
+
 // allow optional comma sep list of tracks
 char *tracks[100];
 int numTracks = chopByChar(trackArg, ',', tracks, sizeof(tracks));
 int i = 0;
 for (i = 0; i < numTracks; i++)
     {
     char *track = cloneString(tracks[i]);
     struct trackDb *thisTrack = findTrackDb(track, tdb);
     if (NULL == thisTrack)
         apiErrAbort(err400, err400Msg, "failed to find specified track=%s in genome=%s for endpoint '/getData/track'  given hubUrl='%s'", track, genome, hubUrl);
     if (trackHasNoData(thisTrack))
         apiErrAbort(err400, err400Msg, "container track '%s' does not contain data, use the children of this container for data access", track);
     if (! isSupportedType(thisTrack->type))
         apiErrAbort(err415, err415Msg, "track type '%s' for track=%s not supported at this time", thisTrack->type, track);
 
@@ -513,61 +520,67 @@
         uEnd = sqlUnsigned(end);
         jsonWriteNumber(jw, "start", uStart);
         jsonWriteNumber(jw, "end", uEnd);
         }
 
     jsonWriteString(jw, "bigDataUrl", bigDataUrl);
     jsonWriteString(jw, "trackType", thisTrack->type);
 
     if (allowedBigBedType(thisTrack->type))
         {
         struct asObject *as = bigBedAsOrDefault(bbi);
         if (! as)
         apiErrAbort(err500, err500Msg, "can not find schema definition for bigDataUrl '%s', track=%s genome: '%s' for endpoint '/getData/track' given hubUrl='%s'", bigDataUrl, track, genome, hubUrl);
         struct sqlFieldType *fiList = sqlFieldTypesFromAs(as);
         if (jsonOutputArrays || debug)
-            bigColumnTypes(jw, fiList, as);
+            bigColumnTypes(columnTypesJw, fiList, as, track);
 
         jsonWriteListStart(jw, track);
         unsigned itemsDone = 0;
         if (isEmpty(chrom))
         {
         struct bbiChromInfo *bci;
         for (bci = chromList; bci && (itemsDone < maxItemsOutput); bci = bci->next)
             {
             itemsDone += bbiDataOutput(jw, bbi, bci->name, 0, bci->size,
             fiList, thisTrack, itemsDone);
             }
             if (itemsDone >= maxItemsOutput)
             reachedMaxItems = TRUE;
         }
         else
         itemsDone += bbiDataOutput(jw, bbi, chrom, uStart, uEnd, fiList,
             thisTrack, itemsDone);
         itemsReturned += itemsDone;
         jsonWriteListEnd(jw);
         }
     else if (startsWith("bigWig", thisTrack->type))
         {
         if (jsonOutputArrays || debug)
-        wigColumnTypes(jw);
+        wigColumnTypes(columnTypesJw, track);
         jsonWriteObjectStart(jw, track);
         bigWigData(jw, bbi, chrom, uStart, uEnd);
         jsonWriteObjectEnd(jw);
         }
     bbiFileClose(&bbi);
     }
+if (jsonOutputArrays || debug)
+    {
+    jsonWriteObjectEnd(columnTypesJw);
+    jsonWriteAppend(jw, NULL, columnTypesJw);
+    jsonWriteFree(&columnTypesJw);
+    }
 apiFinishOutput(0, NULL, jw);
 }	/*	static void getHubTrackData(char *hubUrl)	*/
 
 static void getTrackData()
 /* return data from a track, optionally just one chrom data,
  *  optionally just one section of that chrom data
  */
 {
 char *db = cgiOptionalString("genome");
 char *chrom = chrOrAlias(db, NULL);
 char *start = cgiOptionalString("start");
 char *end = cgiOptionalString("end");
 /* 'track' name in trackDb often refers to a SQL 'table' */
 char *trackArg = cgiOptionalString("track");
 //char *sqlTable = cloneString(trackArg); /* might be something else */
@@ -596,30 +609,36 @@
 if (NULL == conn)
     apiErrAbort(err400, err400Msg, "can not find genome 'genome=%s' for endpoint '/getData/track", db);
 
 struct jsonWrite *jw = apiStartOutput();
 jsonWriteString(jw, "genome", db);
 
 // load the tracks
 struct trackDb *tdbList = NULL;
 cartTrackDbInitForApi(NULL, db, &tdbList, NULL, TRUE);
 
 // allow optional comma sep list of tracks
 char *tracks[100];
 int numTracks = chopByChar(trackArg, ',', tracks, sizeof(tracks));
 int i = 0;
 struct hash *trackHash = hashNew(0); // let hub tracks work
+struct jsonWrite *columnTypesJw = NULL;
+if (jsonOutputArrays || debug)
+    {
+    columnTypesJw = jsonWriteNew();
+    jsonWriteObjectStart(columnTypesJw, "columnTypes");
+    }
 for (i = 0; i < numTracks; i++)
     {
     char *track = cloneString(tracks[i]);
     char *sqlTable = cloneString(track);
 
     if (cartTrackDbIsAccessDenied(db, sqlTable) ||
             (cartTrackDbIsNoGenome(db, sqlTable) && !(chrom && start && end)))
         apiErrAbort(err403, err403Msg, "this data request: 'db=%s;track=%s' is protected data, see also: https://genome.ucsc.edu/FAQ/FAQdownloads.html#download40", db, track);
     struct trackDb *thisTrack = tdbForTrack(db, track, &tdbList);
     if (NULL == thisTrack)
         {
         // maybe we have a hub track, try to look it up
         if (startsWith("hub_", track))
             {
             thisTrack = hubConnectAddHubForTrackAndFindTdb(db, track, &tdbList, trackHash);
@@ -744,63 +763,69 @@
 
     /* when start, end given, show them */
     if ( uEnd > uStart )
         {
         jsonWriteNumber(jw, "start", uStart);
         jsonWriteNumber(jw, "end", uEnd);
         }
 
     if (thisTrack && allowedBigBedType(thisTrack->type))
         {
         struct asObject *as = bigBedAsOrDefault(bbi);
         if (! as)
             apiErrAbort(err500, err500Msg, "can not find schema definition for bigDataUrl '%s', track=%s genome='%s' for endpoint '/getData/track'", bigDataUrl, track, db);
         struct sqlFieldType *fiList = sqlFieldTypesFromAs(as);
         if (jsonOutputArrays || debug)
-            bigColumnTypes(jw, fiList, as);
+            bigColumnTypes(columnTypesJw, fiList, as, track);
 
         jsonWriteListStart(jw, track);
         unsigned itemsDone = 0;
         if (isEmpty(chrom))
             {
             struct bbiChromInfo *bci;
             for (bci = chromList; bci && (itemsDone < maxItemsOutput); bci = bci->next)
                 {
                 itemsDone += bbiDataOutput(jw, bbi, bci->name, 0, bci->size,
                     fiList, thisTrack, itemsDone);
                 }
                 if (itemsDone >= maxItemsOutput)
                     reachedMaxItems = TRUE;
             }
         else
             itemsDone += bbiDataOutput(jw, bbi, chrom, uStart, uEnd, fiList,
                     thisTrack, itemsDone);
         itemsReturned += itemsDone;
         jsonWriteListEnd(jw);
         }
     else if (thisTrack && startsWith("bigWig", thisTrack->type))
         {
         if (jsonOutputArrays || debug)
-            wigColumnTypes(jw);
+            wigColumnTypes(columnTypesJw, track);
 
         jsonWriteObjectStart(jw, track);
         bigWigData(jw, bbi, chrom, uStart, uEnd);
         jsonWriteObjectEnd(jw);
         bbiFileClose(&bbi);
         }
     else
-        tableDataOutput(db, thisTrack, conn, jw, track, chrom, uStart, uEnd);
+        tableDataOutput(db, thisTrack, conn, jw, track, chrom, uStart, uEnd, columnTypesJw);
+    }
+if (jsonOutputArrays || debug)
+    {
+    jsonWriteObjectEnd(columnTypesJw);
+    jsonWriteAppend(jw, NULL, columnTypesJw);
+    jsonWriteFree(&columnTypesJw);  
     }
 
 apiFinishOutput(0, NULL, jw);
 hFreeConn(&conn);
 }	/*	static void getTrackData()	*/
 
 static void getSequenceData(char *db, char *hubUrl)
 /* return DNA sequence, given at least a genome=name and chrom=chr,
    optionally start and end, might be a track hub for UCSC database  */
 {
 char *chrom = chrOrAlias(db, hubUrl);
 char *start = cgiOptionalString("start");
 char *end = cgiOptionalString("end");
 boolean revComp = FALSE;
 char *revCompStr = cgiOptionalString("revComp");