Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 42 additions & 4 deletions api/src/org/labkey/api/action/ApiQueryResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ public class ApiQueryResponse implements ApiResponse
private RenderContext _ctx = null;
private boolean _schemaEditable = false;
private boolean _metaDataOnly;
private boolean _countOnly = false;
private DataRegion _dataRegion;
private TableInfo _tinfo = null;
private List<DisplayColumn> _displayColumns = null;
Expand Down Expand Up @@ -147,6 +148,16 @@ public void render(ApiResponseWriter writer) throws Exception
if (_metaDataOnlyIncludesEmptyRowset)
writeEmptyRowset(writer);
}
else if (_countOnly)
{
loadTotalRowsOnly();
if (_dataRegion.getTotalRows() != null)
_rowCount = _dataRegion.getTotalRows();

if (_includeMetaData)
writeMetaData(writer);
writeEmptyRowset(writer);
}
else
{
// First run the query, so on potential SQLException we only serialize the exception instead of outputting all the metadata before the exception
Expand All @@ -170,8 +181,11 @@ public void render(ApiResponseWriter writer) throws Exception
_rowCount = _dataRegion.getTotalRows();
}
}
}

long rowCount = _rowCount > 0 ? _rowCount : _offset + _numRespRows;
if (!_metaDataOnly)
{
long rowCount = _rowCount > 0 || _countOnly ? _rowCount : _offset + _numRespRows;
writer.writeProperty("rowCount", rowCount);

if (_includeMetaData)
Expand Down Expand Up @@ -268,9 +282,7 @@ protected void writeMetaData(ApiResponseWriter writer) throws Exception

protected Results getResults() throws Exception
{
// If we're going to be writing JSON back, which is tolerant of extra spaces, allow async so we
// can monitor if the client has stopped listening. XML doesn't take kindly to leading spaces
_dataRegion.setAllowAsync(ApiResponseWriter.getResponseFormat(_viewContext.getRequest(), ApiResponseWriter.Format.JSON).isJson());
_dataRegion.setAllowAsync(isAllowAsync());
try
{
return _dataRegion.getResults(_ctx);
Expand All @@ -281,6 +293,26 @@ protected Results getResults() throws Exception
}
}

protected void loadTotalRowsOnly()
{
_dataRegion.setAllowAsync(isAllowAsync());
try
{
_dataRegion.loadTotalRowsOnly(_ctx);
}
catch (ConversionException e)
{
throw new ApiUsageException(e);
}
}

// If we're going to be writing JSON back, which is tolerant of extra spaces, allow async so we
// can monitor if the client has stopped listening. XML doesn't take kindly to leading spaces
private boolean isAllowAsync()
{
return ApiResponseWriter.getResponseFormat(_viewContext.getRequest(), ApiResponseWriter.Format.JSON).isJson();
}

public List<FieldKey> getFieldKeys()
{
return _fieldKeys;
Expand Down Expand Up @@ -328,6 +360,12 @@ public void includeStyle(boolean withStyle)
_doItWithStyle = withStyle;
}

/** Return only rowCount (and metadata, if requested) via a COUNT(*) query, skipping the rows query. */
public void countOnly(boolean countOnly)
{
_countOnly = countOnly;
}

protected double getFormatVersion()
{
return 8.3;
Expand Down
4 changes: 2 additions & 2 deletions api/src/org/labkey/api/assay/AssayProtocolSchema.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@
import org.labkey.api.data.RenderContext;
import org.labkey.api.data.Results;
import org.labkey.api.data.SQLFragment;
import org.labkey.api.data.ShowRows;
import org.labkey.api.data.Sort;
import org.labkey.api.data.Table;
import org.labkey.api.data.TableInfo;
import org.labkey.api.exp.PropertyDescriptor;
import org.labkey.api.exp.api.ExpProtocol;
Expand Down Expand Up @@ -711,7 +711,7 @@ public void addQCWarningIndicator(QueryView baseQueryView, ViewContext context,
QuerySettings qs = getSettings(viewContext, settings.getDataRegionName(), settings.getQueryName());

// we want all the rows
qs.setMaxRows(Table.ALL_ROWS);
qs.setShowRows(ShowRows.ALL);
QueryView allResultsQueryView = createAllResultsQueryView(viewContext, qs);

DataView dataView = allResultsQueryView.createDataView();
Expand Down
47 changes: 37 additions & 10 deletions api/src/org/labkey/api/data/DataRegion.java
Original file line number Diff line number Diff line change
Expand Up @@ -851,16 +851,7 @@ public Map<String, List<Aggregate.Result>> getAggregateResults(RenderContext ctx

newAggregates.add(Aggregate.createCountStar());
_aggregateResults = ctx.getAggregates(_displayColumns, getTable(), getSettings(), getName(), newAggregates, getQueryParameters(), isAllowAsync());
List<Aggregate.Result> result = _aggregateResults.remove(Aggregate.STAR);

//Issue 14863: add null check
if (result != null && !result.isEmpty())
{
Aggregate.Result countStarResult = result.getFirst();
_totalRows = 0L;
if (countStarResult.getValue() instanceof Number)
_totalRows = ((Number) countStarResult.getValue()).longValue();
}
setTotalRowsFromCountStar(_aggregateResults.remove(Aggregate.STAR));
}
}
else
Expand All @@ -876,6 +867,42 @@ public Map<String, List<Aggregate.Result>> getAggregateResults(RenderContext ctx
return _aggregateResults;
}

/** Sets the total row count from a COUNT(*) aggregate alone, without selecting any rows. */
public void loadTotalRowsOnly(RenderContext ctx)
{
if (!hasPermission(ctx, ReadPermission.class))
throw new UnauthorizedException();

TableInfo table = getTable();
if (null == table)
throw new NotFoundException("Table or query not found: " + getSettings().getQueryName());

DataRegion oldRegion = ctx.getCurrentRegion();
ctx.setCurrentRegion(this);
try
{
Map<String, List<Aggregate.Result>> aggregates = ctx.getAggregates(_displayColumns, table, getSettings(), getName(), List.of(Aggregate.createCountStar()), getQueryParameters(), isAllowAsync());
setTotalRowsFromCountStar(aggregates.get(Aggregate.STAR));
_aggregateResults = Collections.emptyMap();
}
finally
{
ctx.setCurrentRegion(oldRegion);
}
}

private void setTotalRowsFromCountStar(@Nullable List<Aggregate.Result> result)
{
//Issue 14863: add null check
if (result == null || result.isEmpty())
return;

Aggregate.Result countStarResult = result.getFirst();
_totalRows = 0L;
if (countStarResult.getValue() instanceof Number n)
_totalRows = n.longValue();
}

@NotNull
private List<Aggregate> getSummaryStatsAggregates(List<AnalyticsProviderItem> providers)
{
Expand Down
5 changes: 4 additions & 1 deletion api/src/org/labkey/api/data/RenderContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,10 @@ public Map<String, List<Aggregate.Result>> getAggregates(List<DisplayColumn> dis
if (null != QueryService.get())
cols = QueryService.get().ensureRequiredColumns(tinfo, cols, filter, sort, ignoredAggregateFilters);

if (!ignoredAggregateFilters.equals(_ignoredColumnFilters))
// Count-only requests skip the main query, so there's nothing to match; adopt the aggregate's set for filter messages
if (_results == null)
_ignoredColumnFilters.addAll(ignoredAggregateFilters);
else if (!ignoredAggregateFilters.equals(_ignoredColumnFilters))
{
// This should never happen, but if it did, the totals wouldn't match, so we won't calculate them.
_log.error("Aggregate filter columns do not match main. Aggregate:{} Main:{}", ignoredAggregateFilters, _ignoredColumnFilters);
Expand Down
2 changes: 1 addition & 1 deletion api/webapp/clientapi/dom/DataRegion.js
Original file line number Diff line number Diff line change
Expand Up @@ -3829,7 +3829,7 @@ if (!LABKEY.DataRegions) {
containerPath: region.containerPath,
filterArray: LABKEY.Filter.getFiltersFromParameters({ ...params, ...jsonData.filters }, params.dataRegionName),
sort: undefined,
maxRows: 1,
maxRows: 0, // GH Issue 1607: with includeTotalCount, returns only the row count
offset: 0,
includeMetadata: false,
includeDetailsColumn: false,
Expand Down
13 changes: 12 additions & 1 deletion query/src/org/labkey/query/controllers/QueryController.java
Original file line number Diff line number Diff line change
Expand Up @@ -3457,6 +3457,13 @@ public void setIncludeTotalCount(boolean includeTotalCount)
_includeTotalCount = includeTotalCount;
}

/** True when the request sent includeTotalCount rather than leaving it at its default. */
public boolean isIncludeTotalCountSet()
{
return null != getViewContext().getRequest().getParameter("includeTotalCount") ||
(null != _initParameters && null != _initParameters.getPropertyValue("includeTotalCount"));
}

public boolean isIncludeStyle()
{
return _includeStyle;
Expand Down Expand Up @@ -3577,7 +3584,10 @@ public ApiResponse execute(APIQueryForm form, BindException errors)
}

boolean isEditable = isQueryEditable(view.getTable());
boolean metaDataOnly = form.getQuerySettings().getMaxRows() == 0;
boolean noRows = form.getQuerySettings().getMaxRows() == 0;
// GH Issue 1607: maxRows=0 with an explicit includeTotalCount=true returns just the row count
boolean countOnly = noRows && form.isIncludeTotalCount() && form.isIncludeTotalCountSet();
boolean metaDataOnly = noRows && !countOnly;
boolean arrayMultiValueColumns = getRequestedApiVersion() >= 16.2;
boolean includeFormattedValue = getRequestedApiVersion() >= 17.1;

Expand Down Expand Up @@ -3607,6 +3617,7 @@ else if (getRequestedApiVersion() >= 9.1)
form.isIncludeDisplayValues(), form.isIncludeMetadata());
}
response.includeStyle(form.isIncludeStyle());
response.countOnly(countOnly);

// Issues 29515 and 32269 - force key and other non-requested columns to be sent back, but only if the client has
// requested minimal columns, as we now do for ExtJS stores
Expand Down
Loading