diff --git a/api/src/org/labkey/api/data/BaseSelector.java b/api/src/org/labkey/api/data/BaseSelector.java index 45e107032c1..29db54d144b 100644 --- a/api/src/org/labkey/api/data/BaseSelector.java +++ b/api/src/org/labkey/api/data/BaseSelector.java @@ -50,8 +50,6 @@ * A partial, base implementation of {@link org.labkey.api.data.Selector}. This class manipulates result sets but doesn't * generate them. Subclasses include ExecutingSelector (which executes SQL to generate a result set) and ResultSetSelector, * which takes an externally generated ResultSet (e.g., from JDBC metadata calls) and allows Selector operations on it. - * User: adam - * Date: 12/11/12 */ public abstract class BaseSelector> extends JdbcCommand implements Selector diff --git a/api/src/org/labkey/api/data/CachedResultSetBuilder.java b/api/src/org/labkey/api/data/CachedResultSetBuilder.java new file mode 100644 index 00000000000..4f9b1ddd8f1 --- /dev/null +++ b/api/src/org/labkey/api/data/CachedResultSetBuilder.java @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2013-2018 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.api.data; + +import org.labkey.api.collections.ResultSetRowMapFactory; +import org.labkey.api.collections.RowMap; + +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * Builder for CachedResultSets, given a {@code ResultSet} or a {@code List>} + */ +public abstract class CachedResultSetBuilder> +{ + protected boolean _requireClose = true; + protected StackTraceElement[] _stackTrace = null; + + public static FromResultSet create(ResultSet rs) + { + return new FromResultSet(rs); + } + + public static FromListOfMaps create(List> maps) + { + return create(maps, maps.get(0).keySet()); + } + + /** + * Create CachedResultSetBuilder from a list of maps and collection of column names. For the most flexibility, the + * maps may need to be case-insensitive. How do you tell? If the maps have data and the keys match the columnNames, + * but the ResultSet rowMap values are all null. + * @param maps List of row data, possibly case-insensitive maps + * @param columnNames Collection of column names + * + * TODO: A case-insensitive option for the builder, but there may be performance impact for very large result sets + * if the implementation were simply to wrap each incoming map with CaseInsensitiveHashMap. For now, onus is on the + * caller to provide case insensitive maps when necessary. + */ + public static FromListOfMaps create(List> maps, Collection columnNames) + { + return new FromListOfMaps(maps, columnNames); + } + + public abstract C getThis(); + + public C setRequireClose(boolean requireClose) + { + _requireClose = requireClose; + return getThis(); + } + + public C setStackTrace(StackTraceElement[] stackTrace) + { + _stackTrace = stackTrace; + return getThis(); + } + + public final static class FromResultSet extends CachedResultSetBuilder + { + private final ResultSet _rs; + + private int _maxRows = Table.ALL_ROWS; + private QueryLogging _queryLogging = QueryLogging.emptyQueryLogging(); + + private FromResultSet(ResultSet rs) + { + _rs = rs; + } + + @Override + public FromResultSet getThis() + { + return this; + } + + public FromResultSet setMaxRows(int maxRows) + { + _maxRows = maxRows; + return this; + } + + public FromResultSet setQueryLogging(QueryLogging queryLogging) + { + _queryLogging = queryLogging; + return this; + } + + public CachedResultSet build() throws SQLException + { + try (ResultSet rs = new LoggingResultSetWrapper(_rs, _queryLogging)) // TODO: avoid if we're passed a read-only and empty one?? + { + // Snowflake auto-closes metadata after reading the last row, so cache that metadata first + ResultSetMetaData md = new CachedResultSetMetaData(rs.getMetaData()); + + ArrayList> list = new ArrayList<>(); + + if (_maxRows == Table.ALL_ROWS) + _maxRows = Integer.MAX_VALUE; + + ResultSetRowMapFactory factory = ResultSetRowMapFactory.create(rs); + + // Note: we check in this order to avoid consuming the "extra" row used to detect complete vs. not + while (list.size() < _maxRows && rs.next()) + list.add(factory.getRowMap(rs)); + + // If we have another row, then we're not complete + boolean isComplete = !rs.next(); + + return new CachedResultSet(md, list, isComplete, _requireClose, _stackTrace); + } + } + } + + public final static class FromListOfMaps extends CachedResultSetBuilder + { + private final List> _maps; + private final Collection _columnNames; + + private ResultSetMetaData _md = null; + private boolean _isComplete = true; + + private FromListOfMaps(List> maps, Collection columnNames) + { + _maps = maps; + _columnNames = columnNames; + } + + @Override + public FromListOfMaps getThis() + { + return this; + } + + public FromListOfMaps setMetaData(ResultSetMetaData md) + { + _md = md; + return this; + } + + public FromListOfMaps setComplete(boolean complete) + { + _isComplete = complete; + return this; + } + + public CachedResultSet build() + { + if (_md == null) + _md = createMetaData(_columnNames); + + return new CachedResultSet(_md, convertToRowMaps(_md, _maps), _isComplete, _requireClose, _stackTrace); + } + } + + private static ResultSetMetaData createMetaData(Collection columnNames) + { + ResultSetMetaDataImpl md = new ResultSetMetaDataImpl(columnNames.size()); + for (String columnName : columnNames) + { + ResultSetMetaDataImpl.ColumnMetaData col = new ResultSetMetaDataImpl.ColumnMetaData(); + col.columnName = columnName; + col.columnLabel = columnName; + md.addColumn(col); + } + + return md; + } + + private static ArrayList> convertToRowMaps(ResultSetMetaData md, List> maps) + { + ArrayList> list = new ArrayList<>(); + + ResultSetRowMapFactory factory; + try + { + factory = ResultSetRowMapFactory.create(md); + } + catch (SQLException e) + { + throw new RuntimeSQLException(e); + } + + for (Map map : maps) + { + list.add(factory.getRowMap(map)); + } + + return list; + } +} diff --git a/api/src/org/labkey/api/data/CachedResultSets.java b/api/src/org/labkey/api/data/CachedResultSets.java deleted file mode 100644 index 67ac2fec66e..00000000000 --- a/api/src/org/labkey/api/data/CachedResultSets.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (c) 2013-2018 LabKey Corporation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.labkey.api.data; - -import org.jetbrains.annotations.Nullable; -import org.labkey.api.collections.ResultSetRowMapFactory; -import org.labkey.api.collections.RowMap; - -import java.sql.ResultSet; -import java.sql.ResultSetMetaData; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; - -/** - * Factory methods that create CachedResultSets, plus a couple helpers - */ -public class CachedResultSets -{ - public static CachedResultSet create(ResultSet rs, boolean cacheMetaData, boolean requireClose, int maxRows) throws SQLException - { - return create(rs, cacheMetaData, requireClose, maxRows, null, QueryLogging.emptyQueryLogging()); - } - - public static CachedResultSet create(ResultSet rsIn, boolean cacheMetaData, boolean requireClose, int maxRows, @Nullable StackTraceElement[] stackTrace, QueryLogging queryLogging) throws SQLException - { - try (ResultSet rs = new LoggingResultSetWrapper(rsIn, queryLogging)) // TODO: avoid if we're passed a read-only and empty one?? - { - // Snowflake auto-closes metadata after reading the last row, so cache that metadata first - ResultSetMetaData md = cacheMetaData ? new CachedResultSetMetaData(rs.getMetaData()) : rs.getMetaData(); - - ArrayList> list = new ArrayList<>(); - - if (maxRows == Table.ALL_ROWS) - maxRows = Integer.MAX_VALUE; - - ResultSetRowMapFactory factory = ResultSetRowMapFactory.create(rs); - - // Note: we check in this order to avoid consuming the "extra" row used to detect complete vs. not - while (list.size() < maxRows && rs.next()) - list.add(factory.getRowMap(rs)); - - // If we have another row, then we're not complete - boolean isComplete = !rs.next(); - - return new CachedResultSet(md, list, isComplete, requireClose, stackTrace); - } - } - - public static CachedResultSet create(ResultSetMetaData md, List> maps, boolean isComplete) - { - return new CachedResultSet(md, convertToRowMaps(md, maps), isComplete, true, null); - } - - public static CachedResultSet create(List> maps) - { - return create(maps, maps.get(0).keySet()); - } - - /** - * Create CachedResultSet from a list of maps and collection of column names. For the most flexibility, the maps - * may need to be case-insensitive. How do you tell? If the maps have data and the keys match the columnNames, but - * the ResultSet rowMap values are all null. - * @param maps List of row data, possibly case-insensitive maps - * @param columnNames Collection of column names - * - * TODO: A case insensitive overload of this method, but there may be performance impact for very large result sets - * if the implementation were simply to wrap each incoming map with CaseInsensitiveHashMap. For now, onus is on the - * caller to provide case insensitive maps when necessary. - */ - public static CachedResultSet create(List> maps, Collection columnNames) - { - ResultSetMetaData md = createMetaData(columnNames); - - // Avoid error message from CachedResultSet.finalize() about unclosed CachedResultSet. - try (CachedResultSet crs = new CachedResultSet(md, convertToRowMaps(md, maps), true, true, null)) - { - return crs; - } - } - - private static ResultSetMetaData createMetaData(Collection columnNames) - { - ResultSetMetaDataImpl md = new ResultSetMetaDataImpl(columnNames.size()); - for (String columnName : columnNames) - { - ResultSetMetaDataImpl.ColumnMetaData col = new ResultSetMetaDataImpl.ColumnMetaData(); - col.columnName = columnName; - col.columnLabel = columnName; - md.addColumn(col); - } - - return md; - } - - private static ArrayList> convertToRowMaps(ResultSetMetaData md, List> maps) - { - ArrayList> list = new ArrayList<>(); - - ResultSetRowMapFactory factory; - try - { - factory = ResultSetRowMapFactory.create(md); - } - catch (SQLException e) - { - throw new RuntimeSQLException(e); - } - - for (Map map : maps) - { - list.add(factory.getRowMap(map)); - } - - return list; - } -} diff --git a/api/src/org/labkey/api/data/DbScope.java b/api/src/org/labkey/api/data/DbScope.java index dcd387315b6..50af17eb9c9 100644 --- a/api/src/org/labkey/api/data/DbScope.java +++ b/api/src/org/labkey/api/data/DbScope.java @@ -1878,7 +1878,7 @@ private static void detectUnexpectedConnections(Connection conn, LabKeyDataSourc stmt.setString(1, databaseName); stmt.setString(2, applicationName); - try (CachedResultSet rs = CachedResultSets.create(stmt.executeQuery(), true, true, 1000)) + try (CachedResultSet rs = CachedResultSetBuilder.create(stmt.executeQuery()).setMaxRows(1000).build()) { count = rs.getSize(); if (count != 0) diff --git a/api/src/org/labkey/api/data/ResultSetSelector.java b/api/src/org/labkey/api/data/ResultSetSelector.java index 1b83764d5bf..b91522b925e 100644 --- a/api/src/org/labkey/api/data/ResultSetSelector.java +++ b/api/src/org/labkey/api/data/ResultSetSelector.java @@ -103,7 +103,7 @@ protected TableResultSet wrapResultSet(ResultSet rs, Connection conn, boolean ca { if (cache) { - return CachedResultSets.create(rs, true, requireClose, Table.ALL_ROWS); + return CachedResultSetBuilder.create(rs).setRequireClose(requireClose).build(); } else { diff --git a/api/src/org/labkey/api/data/ResultSetSelectorTestCase.java b/api/src/org/labkey/api/data/ResultSetSelectorTestCase.java index bdcaa86239c..dad39e5ddc7 100644 --- a/api/src/org/labkey/api/data/ResultSetSelectorTestCase.java +++ b/api/src/org/labkey/api/data/ResultSetSelectorTestCase.java @@ -79,7 +79,7 @@ private void test(DbScope scope, ResultSet rs, Class clazz) throws SQLExc assertEquals("Non-scrollable ResultSet can't be used with ScrollToTop", e.getMessage()); } - rs = CachedResultSets.create(rs, true, true, Table.ALL_ROWS, null, QueryLogging.emptyQueryLogging()); + rs = CachedResultSetBuilder.create(rs).build(); } ResultSetSelector selector = new ResultSetSelector(scope, rs); diff --git a/api/src/org/labkey/api/data/SqlExecutingSelector.java b/api/src/org/labkey/api/data/SqlExecutingSelector.java index 5f564a5fc10..e9b0d05ddf6 100644 --- a/api/src/org/labkey/api/data/SqlExecutingSelector.java +++ b/api/src/org/labkey/api/data/SqlExecutingSelector.java @@ -211,7 +211,7 @@ protected TableResultSet wrapResultSet(ResultSet rs, Connection conn, boolean ca if (cache) { // Cache ResultSet and meta data - return CachedResultSets.create(rs, true, requireClose, _maxRows, _loggingStacktrace, getQueryLogging()); + return CachedResultSetBuilder.create(rs).setRequireClose(requireClose).setMaxRows(_maxRows).setStackTrace(_loggingStacktrace).setQueryLogging(getQueryLogging()).build(); } else { diff --git a/pipeline/src/org/labkey/pipeline/analysis/ProtocolManagementWebPart.java b/pipeline/src/org/labkey/pipeline/analysis/ProtocolManagementWebPart.java index 6600b73996c..ec11a4039a7 100644 --- a/pipeline/src/org/labkey/pipeline/analysis/ProtocolManagementWebPart.java +++ b/pipeline/src/org/labkey/pipeline/analysis/ProtocolManagementWebPart.java @@ -18,7 +18,7 @@ import org.labkey.api.data.ActionButton; import org.labkey.api.data.BaseColumnInfo; import org.labkey.api.data.ButtonBar; -import org.labkey.api.data.CachedResultSets; +import org.labkey.api.data.CachedResultSetBuilder; import org.labkey.api.data.ColumnInfo; import org.labkey.api.data.DataRegion; import org.labkey.api.data.RenderContext; @@ -101,7 +101,7 @@ private ButtonBar createButtonBar() private void createResults() // Accept filter & sort ? Tough to use standard UI components the way this is wired in. { List> rows = getProtocols().stream().map(Protocol::toMap).collect(Collectors.toList()); - ResultSet rs = CachedResultSets.create(rows, Arrays.asList("taskId", "name", "pipeline", "archived")); + ResultSet rs = CachedResultSetBuilder.create(rows, Arrays.asList("taskId", "name", "pipeline", "archived")).build(); try { List colInfos = DataRegion.colInfosFromMetaData(rs.getMetaData()); diff --git a/query/src/org/labkey/query/controllers/QueryController.java b/query/src/org/labkey/query/controllers/QueryController.java index 0ac394da7ac..17eab5ba6aa 100644 --- a/query/src/org/labkey/query/controllers/QueryController.java +++ b/query/src/org/labkey/query/controllers/QueryController.java @@ -97,7 +97,7 @@ import org.labkey.api.data.AnalyticsProviderItem; import org.labkey.api.data.BaseColumnInfo; import org.labkey.api.data.ButtonBar; -import org.labkey.api.data.CachedResultSets; +import org.labkey.api.data.CachedResultSetBuilder; import org.labkey.api.data.ColumnHeaderType; import org.labkey.api.data.ColumnInfo; import org.labkey.api.data.CompareType; @@ -1755,26 +1755,26 @@ else if (ti instanceof LinkedTableInfo) { JdbcMetaDataSelector columnSelector = new JdbcMetaDataSelector(locator, (dbmd, l) -> dbmd.getColumns(l.getCatalogName(), l.getSchemaNamePattern(), l.getTableNamePattern(), null)); - result.addView(new ResultSetView(CachedResultSets.create(columnSelector.getResultSet(), true, true, Table.ALL_ROWS), "Table Meta Data")); + result.addView(new ResultSetView(CachedResultSetBuilder.create(columnSelector.getResultSet()).build(), "Table Meta Data")); JdbcMetaDataSelector pkSelector = new JdbcMetaDataSelector(locator, (dbmd, l) -> dbmd.getPrimaryKeys(l.getCatalogName(), l.getSchemaName(), l.getTableName())); - result.addView(new ResultSetView(CachedResultSets.create(pkSelector.getResultSet(), true, true, Table.ALL_ROWS), "Primary Key Meta Data")); + result.addView(new ResultSetView(CachedResultSetBuilder.create(pkSelector.getResultSet()).build(), "Primary Key Meta Data")); if (dialect.canCheckIndices(ti)) { JdbcMetaDataSelector indexSelector = new JdbcMetaDataSelector(locator, (dbmd, l) -> dbmd.getIndexInfo(l.getCatalogName(), l.getSchemaName(), l.getTableName(), false, false)); - result.addView(new ResultSetView(CachedResultSets.create(indexSelector.getResultSet(), true, true, Table.ALL_ROWS), "Other Index Meta Data")); + result.addView(new ResultSetView(CachedResultSetBuilder.create(indexSelector.getResultSet()).build(), "Other Index Meta Data")); } JdbcMetaDataSelector ikSelector = new JdbcMetaDataSelector(locator, (dbmd, l) -> dbmd.getImportedKeys(l.getCatalogName(), l.getSchemaName(), l.getTableName())); - result.addView(new ResultSetView(CachedResultSets.create(ikSelector.getResultSet(), true, true, Table.ALL_ROWS), "Imported Keys Meta Data")); + result.addView(new ResultSetView(CachedResultSetBuilder.create(ikSelector.getResultSet()).build(), "Imported Keys Meta Data")); JdbcMetaDataSelector ekSelector = new JdbcMetaDataSelector(locator, (dbmd, l) -> dbmd.getExportedKeys(l.getCatalogName(), l.getSchemaName(), l.getTableName())); - result.addView(new ResultSetView(CachedResultSets.create(ekSelector.getResultSet(), true, true, Table.ALL_ROWS), "Exported Keys Meta Data")); + result.addView(new ResultSetView(CachedResultSetBuilder.create(ekSelector.getResultSet()).build(), "Exported Keys Meta Data")); } return result; } @@ -1827,7 +1827,7 @@ public ModelAndView getView(Object form, BindException errors) throws Exception ActionURL url = new ActionURL(RawTableMetaDataAction.class, getContainer()) .addParameter("schemaName", _schemaName) .addParameter("query.queryName", null); - tablesView = new ResultSetView(CachedResultSets.create(selector.getResultSet(), true, true, Table.ALL_ROWS), "Tables", "TABLE_NAME", url) + tablesView = new ResultSetView(CachedResultSetBuilder.create(selector.getResultSet()).build(), "Tables", "TABLE_NAME", url) { @Override protected boolean shouldLink(ResultSet rs) throws SQLException