Skip to content
Open
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
3 changes: 3 additions & 0 deletions api/src/org/labkey/api/action/ApiQueryResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ public void render(ApiResponseWriter writer) throws Exception
long rowCount = _rowCount > 0 ? _rowCount : _offset + _numRespRows;
writer.writeProperty("rowCount", rowCount);

if (_dataRegion.isTotalRowsCapped())
writer.writeProperty("rowCountCapped", true);

if (_includeMetaData)
{
// messages, but only if metadata is requested
Expand Down
14 changes: 14 additions & 0 deletions api/src/org/labkey/api/data/DataRegion.java
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ public class DataRegion extends DisplayElement
private boolean _horizontalGroups = true;
private boolean _errorCreatingResults = false;
private Long _totalRows = null; // total rows in the query or null if unknown
private boolean _totalRowsCapped = false; // true when _totalRows was capped at maxCount rather than counted exactly
private Integer _rowCount = null; // number of rows in the result set or null if unknown
private boolean _complete = false; // true if all rows are in the ResultSet
private boolean _buttonBarRendered = false;
Expand Down Expand Up @@ -860,6 +861,14 @@ public Map<String, List<Aggregate.Result>> getAggregateResults(RenderContext ctx
_totalRows = 0L;
if (countStarResult.getValue() instanceof Number)
_totalRows = ((Number) countStarResult.getValue()).longValue();

// The cap only takes effect when count star is the sole aggregate (same precondition as the SELECT 1 optimization), so it never truncates a summary-stat query.
int maxCount = getSettings() != null ? getSettings().getMaxCount() : 0;
if (baseAggregates.isEmpty() && maxCount > 0 && _totalRows > maxCount)
{
_totalRows = (long) maxCount;
_totalRowsCapped = true;
}
}
}
}
Expand Down Expand Up @@ -892,6 +901,11 @@ public Long getTotalRows()
return _totalRows;
}

public boolean isTotalRowsCapped()
{
return _totalRowsCapped;
}

public void setTotalRows(Long totalRows)
{
if (_totalRows == null)
Expand Down
6 changes: 4 additions & 2 deletions api/src/org/labkey/api/data/RenderContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -434,10 +434,12 @@ protected void close(@Nullable ResultSet rs, @Nullable Connection conn)

selector.setNamedParameters(parameters);

// GitHub Issue 1534: Cap row counts for React grid pagination
int maxCount = settings != null ? settings.getMaxCount() : 0;
if (async)
return selector.getAggregatesAsync(aggregates, getViewContext().getResponse());
return selector.getAggregatesAsync(aggregates, getViewContext().getResponse(), maxCount);
else
return selector.getAggregates(aggregates);
return selector.getAggregates(aggregates, maxCount);
}

return Collections.emptyMap();
Expand Down
32 changes: 30 additions & 2 deletions api/src/org/labkey/api/data/TableSelector.java
Original file line number Diff line number Diff line change
Expand Up @@ -554,15 +554,32 @@ public boolean exists()

// TODO: Convert to return Map<FieldKey, List<Aggregate.Result>>
public Map<String, List<Result>> getAggregates(final List<Aggregate> aggregates)
{
return getAggregates(aggregates, 0);
}

/**
* @param maxCount when > 0 and the only aggregate is COUNT(*), bounds the inner select to maxCount + 1 rows so the database can stop early.
*/
public Map<String, List<Result>> getAggregates(final List<Aggregate> aggregates, int maxCount)
{
// If we are only asking for the COUNT(*) aggregate, then we don't need to include all of the table columns in the subselect.
// This can make a big performance difference for Sample Type and Data Class tables as they can then skip
// the join between the exp schema base table and the materialized table for the given table.
Collection<ColumnInfo> aggColumns = aggregates.size() == 1 && aggregates.getFirst().isCountStar() ? getRowCountingSelectColumns(_table) : _columns;
boolean countStarOnly = aggregates.size() == 1 && aggregates.getFirst().isCountStar();
Collection<ColumnInfo> aggColumns = countStarOnly ? getRowCountingSelectColumns(_table) : _columns;

final AggregateSqlFactory sqlFactory = new AggregateSqlFactory(_filter, aggregates, aggColumns);
ResultSetFactory resultSetFactory = new ExecutingResultSetFactory(sqlFactory);

// Setting _maxRows threads LIMIT maxCount + 1 through TableSqlFactory.getSql() into the inner select; restore it after.
boolean cap = maxCount > 0 && countStarOnly;
var maxRows = _maxRows;
if (cap)
_maxRows = maxCount + 1;

try
{
return resultSetFactory.handleResultSet((rs, conn) -> {
Map<String, List<Result>> results = new CaseInsensitiveHashMap<>();

Expand All @@ -589,17 +606,28 @@ public Map<String, List<Result>> getAggregates(final List<Aggregate> aggregates)

return results;
});
}
finally
{
if (cap)
_maxRows = maxRows;
}
}

public Map<String, List<Result>> getAggregatesAsync(final List<Aggregate> aggregates, HttpServletResponse response)
{
return getAggregatesAsync(aggregates, response, 0);
}

public Map<String, List<Result>> getAggregatesAsync(final List<Aggregate> aggregates, HttpServletResponse response, int maxCount)
{
setLogger(ConnectionWrapper.getConnectionLogger());
AsyncQueryRequest<Map<String, List<Result>>> asyncRequest = new AsyncQueryRequest<>(response, getAsyncResourceName("getAggregates"), getAsyncSpanTags());
setAsyncRequest(asyncRequest);

try
{
return asyncRequest.waitForResult(() -> getAggregates(aggregates));
return asyncRequest.waitForResult(() -> getAggregates(aggregates, maxCount));
}
catch (SQLException e)
{
Expand Down
1 change: 1 addition & 0 deletions api/src/org/labkey/api/query/QueryParam.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public enum QueryParam implements SafeToRenderEnum

offset,
maxRows,
maxCount,
showRows,
ignoreFilter,

Expand Down
28 changes: 28 additions & 0 deletions api/src/org/labkey/api/query/QuerySettings.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ public class QuerySettings
private boolean _ignoreViewFilter;
private int _maxRows = 100;
private boolean _maxRowsSet = false; // Explicitly track setting maxRows, allows for different defaults
private int _maxCount = 0; // 0 = count exactly (unbounded); >0 caps the pagination COUNT(*) at this many rows
private long _offset = 0;
private String _selectionKey = null;

Expand Down Expand Up @@ -286,6 +287,21 @@ public void init(PropertyValues pvs)
throwParameterParseException(QueryParam.maxRows);
}
}

String maxCountParam = _getParameter(param(QueryParam.maxCount));
if (maxCountParam != null)
{
try
{
int maxCount = Integer.parseInt(maxCountParam);
if (maxCount > 0)
setMaxCount(maxCount);
}
catch (NumberFormatException nfe)
{
throwParameterParseException(QueryParam.maxCount);
}
}
}

String containerFilterNameParam = _getParameter(param(QueryParam.containerFilterName));
Expand Down Expand Up @@ -632,6 +648,18 @@ public boolean isMaxRowsSet()
return _maxRowsSet;
}

/** @return The cap on the pagination COUNT(*), or 0 for an exact (unbounded) count. */
public int getMaxCount()
{
return _maxCount;
}

/** @param maxCount count no further than this many rows; 0 restores an exact count. */
public void setMaxCount(int maxCount)
{
_maxCount = maxCount;
}

/** @return The offset parameter when {@link ShowRows#PAGINATED}, otherwise 0. */
public long getOffset()
{
Expand Down
16 changes: 8 additions & 8 deletions core/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"lint-branch-fix": "node lint.diff.mjs --currentBranch --fix"
},
"dependencies": {
"@labkey/components": "7.62.3",
"@labkey/components": "7.62.4-fb-limitMaxCount.2",
"@labkey/themes": "1.9.6"
},
"devDependencies": {
Expand Down
16 changes: 8 additions & 8 deletions experiment/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion experiment/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"test-integration": "cross-env NODE_ENV=test jest --ci --runInBand -c test/js/jest.config.integration.js"
},
"dependencies": {
"@labkey/components": "7.62.3"
"@labkey/components": "7.62.4-fb-limitMaxCount.0"
},
"devDependencies": {
"@labkey/build": "10.1.2",
Expand Down
16 changes: 8 additions & 8 deletions pipeline/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pipeline/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"build-prod": "npm run clean && cross-env NODE_ENV=production rspack build --config node_modules/@labkey/build/configs/prod.config.js"
},
"dependencies": {
"@labkey/components": "7.62.3"
"@labkey/components": "7.62.4-fb-limitMaxCount.2"
},
"devDependencies": {
"@labkey/build": "10.1.2",
Expand Down
13 changes: 13 additions & 0 deletions query/src/org/labkey/query/controllers/QueryController.java
Original file line number Diff line number Diff line change
Expand Up @@ -3480,6 +3480,7 @@ public static class APIQueryForm extends ContainerFilterQueryForm
{
private Integer _start;
private Integer _limit;
private Integer _maxCount;
private boolean _includeDetailsColumn = false;
private boolean _includeUpdateColumn = false;
private boolean _includeTotalCount = true;
Expand Down Expand Up @@ -3508,6 +3509,16 @@ public void setLimit(Integer limit)
_limit = limit;
}

public Integer getMaxCount()
{
return _maxCount;
}

public void setMaxCount(Integer maxCount)
{
_maxCount = maxCount;
}

public boolean isIncludeTotalCount()
{
return _includeTotalCount;
Expand Down Expand Up @@ -3598,6 +3609,8 @@ protected QuerySettings createQuerySettings(UserSchema schema)
}
if (getStart() != null)
results.setOffset(getStart());
if (getMaxCount() != null)
results.setMaxCount(getMaxCount());

return results;
}
Expand Down