diff --git a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls index e4db4689e6..b51bb1cd2f 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls @@ -93,6 +93,11 @@ type SQLResultColumn { supportedOperations: [DataTypeLogicalOperation!]! } +type SQLResultRowMetaData { + data: [Object]! + metaData: Object +} + type DatabaseDocument { id: String contentType: String @@ -103,7 +108,8 @@ type DatabaseDocument { type SQLResultSet { id: ID! columns: [ SQLResultColumn ] - rows: [ [ Object ] ] + rows: [ [ Object ] ] @deprecated + rowsWithMetaData: [SQLResultRowMetaData] # True means that resultset was generated by single entity query # New rows can be added, old rows can be deleted @@ -112,6 +118,8 @@ type SQLResultSet { hasMoreData: Boolean! # can't update data or load LOB file if hasRowIdentifier = false hasRowIdentifier: Boolean! + hasChildrenCollection: Boolean! + isSupportsDataFilter: Boolean! } type SQLQueryResults { @@ -144,6 +152,12 @@ type SQLExecuteInfo { input SQLResultRow { data: [ Object ]! updateValues: Object + metaData: Object +} + +input SQLResultRowMetaDataInput { + data: [Object] + metaData: Object! } type DataTypeLogicalOperation { @@ -386,4 +400,5 @@ extend type Mutation { ): AsyncTaskInfo! asyncSqlRowDataCountResult(taskId: ID!): Int! + } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java index be9715377c..995be53566 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java @@ -331,7 +331,7 @@ public class WebSQLProcessor implements WebSessionProvider { { // we don't need to add same row several times // (it can be when we update the row from RS with several tables) - Set newResultSetRows = new LinkedHashSet<>(); + Set newResultSetRows = new LinkedHashSet<>(); KeyDataReceiver keyReceiver = new KeyDataReceiver(contextInfo.getResults(resultsId)); WebSQLResultsInfo resultsInfo = contextInfo.getResults(resultsId); @@ -372,7 +372,7 @@ public class WebSQLProcessor implements WebSessionProvider { totalUpdateCount += statistics.getRowsUpdated(); result.setDuration(result.getDuration() + statistics.getExecuteTime()); - newResultSetRows.add(rowValues); + newResultSetRows.add(new WebSQLQueryResultSetRow(rowValues, null)); } if (txnManager != null && txnManager.isSupportsTransactions()) { @@ -399,7 +399,7 @@ public class WebSQLProcessor implements WebSessionProvider { WebSQLQueryResults updateResults = new WebSQLQueryResults(webSession, dataFormat); updateResults.setUpdateRowCount(totalUpdateCount); updateResults.setResultSet(updatedResultSet); - updatedResultSet.setRows(newResultSetRows.toArray(new Object[0][])); + updatedResultSet.setRows(List.of(newResultSetRows.toArray(new WebSQLQueryResultSetRow[0]))); queryResults.add(updateResults); @@ -410,7 +410,7 @@ public class WebSQLProcessor implements WebSessionProvider { private void getUpdatedRowsInfo( @NotNull WebSQLResultsInfo resultsInfo, - @NotNull Set newResultSetRows, + @NotNull Set newResultSetRows, @Nullable WebDataFormat dataFormat, @NotNull DBRProgressMonitor monitor) throws DBCException { @@ -420,8 +420,8 @@ public class WebSQLProcessor implements WebSessionProvider { "Refresh row(s) after insert/update") ) { boolean canRefreshResults = resultsInfo.canRefreshResults(); - for (Object[] row : newResultSetRows) { - if (row.length == 0) { + for (WebSQLQueryResultSetRow row : newResultSetRows) { + if (row.getData().length == 0) { continue; } if (!canRefreshResults) { @@ -438,7 +438,7 @@ public class WebSQLProcessor implements WebSessionProvider { if (attr.getRowIdentifier() == null) { continue; } - final Object keyValue = row[attr.getOrdinalPosition()]; + final Object keyValue = row.getData()[attr.getOrdinalPosition()]; if (DBUtils.isNullValue(keyValue)) { hasKey = false; break; @@ -455,7 +455,7 @@ public class WebSQLProcessor implements WebSessionProvider { } DBDDataFilter filter = new DBDDataFilter(constraints); DBSDataContainer dataContainer = resultsInfo.getDataContainer(); - WebRowDataReceiver dataReceiver = new WebRowDataReceiver(resultsInfo.getAttributes(), row, dataFormat); + WebRowDataReceiver dataReceiver = new WebRowDataReceiver(resultsInfo.getAttributes(), row.getData(), dataFormat); dataContainer.readData( new AbstractExecutionSource(dataContainer, getExecutionContext(dataContainer), this), session, @@ -471,14 +471,14 @@ public class WebSQLProcessor implements WebSessionProvider { private void makeWebCellRow( @NotNull WebSQLResultsInfo resultsInfo, - @NotNull Object[] row, + @NotNull WebSQLQueryResultSetRow row, @Nullable WebDataFormat dataFormat ) throws DBCException { - for (int i = 0; i < row.length; i++) { - row[i] = WebSQLUtils.makeWebCellValue( + for (int i = 0; i < row.getData().length; i++) { + row.getData()[i] = WebSQLUtils.makeWebCellValue( webSession, resultsInfo.getAttributeByPosition(i), - row[i], + row.getData()[i], dataFormat); } } @@ -563,6 +563,14 @@ public class WebSQLProcessor implements WebSessionProvider { Map updateValues = row.getUpdateValues().entrySet().stream() .filter(x -> CommonUtils.equalObjects(allAttributes[CommonUtils.toInt(x.getKey())].getRowIdentifier(), rowIdentifier)) .collect(HashMap::new, (m,v) -> m.put(v.getKey(), v.getValue()), HashMap::putAll); + + Map metaData; + if (row.getMetaData() != null) { + metaData = new HashMap<>(row.getMetaData()); + } else { + metaData = new HashMap<>(); + } + if (finalRow.length == 0 || CommonUtils.isEmpty(updateValues)) { continue; } @@ -582,7 +590,7 @@ public class WebSQLProcessor implements WebSessionProvider { boolean isDocumentValue = keyAttributes.length == 1 && keyAttribute.getDataKind() == DBPDataKind.DOCUMENT && dataContainer instanceof DBSDocumentLocator; if (isDocumentValue) { rowValues[updateAttributes.length + i] = - makeDocumentInputValue(session, (DBSDocumentLocator) dataContainer, resultsInfo, row); + makeDocumentInputValue(session, (DBSDocumentLocator) dataContainer, resultsInfo, row, metaData); } else { rowValues[updateAttributes.length + i] = keyAttribute.getValueHandler().getValueFromObject( session, @@ -651,6 +659,7 @@ public class WebSQLProcessor implements WebSessionProvider { if (keyAttributes.length > 0 && !CommonUtils.isEmpty(deletedRows)) { for (WebSQLResultsRow row : deletedRows) { Object[] keyData = row.getData(); + Map keyMetaData = row.getMetaData(); if (keyData.length == 0) { continue; } @@ -658,20 +667,37 @@ public class WebSQLProcessor implements WebSessionProvider { boolean isDocumentKey = keyAttributes.length == 1 && keyAttributes[0].getDataKind() == DBPDataKind.DOCUMENT; - for (int i = 0; i < allAttributes.length; i++) { - if (isDocumentKey || ArrayUtils.contains(keyAttributes, allAttributes[i])) { - Object realCellValue = convertInputCellValue(session, allAttributes[i], - keyData[i], withoutExecution); - delKeyAttributes.put(allAttributes[i], realCellValue); + if (dataContainer instanceof DBSDocumentLocator dataLocator) { + Map keyMap = new LinkedHashMap<>(); + DBDAttributeBinding[] attributes = resultsInfo.getAttributes(); + for (int j = 0; j < attributes.length; j++) { + DBDAttributeBinding attr = attributes[j]; + Object plainValue = WebSQLUtils.makePlainCellValue(session, attr, row.getData()[j]); + keyMap.put(attr.getName(), plainValue); } - } + DBDDocument document = dataLocator.findDocument(session, keyMap, keyMetaData); - DBSDataManipulator.ExecuteBatch deleteBatch = dataManipulator.deleteData( - session, - delKeyAttributes.keySet().toArray(new DBSAttributeBase[0]), - executionSource); - deleteBatch.add(delKeyAttributes.values().toArray()); - resultBatches.put(deleteBatch, new Object[0]); + DBSDataManipulator.ExecuteBatch deleteBatch = dataManipulator.deleteData( + session, + keyAttributes, + executionSource); + deleteBatch.add(new Object[] {document}); + resultBatches.put(deleteBatch, new Object[0]); + } else { + for (int i = 0; i < allAttributes.length; i++) { + if (isDocumentKey || ArrayUtils.contains(keyAttributes, allAttributes[i])) { + Object realCellValue = convertInputCellValue(session, allAttributes[i], + keyData[i], withoutExecution); + delKeyAttributes.put(allAttributes[i], realCellValue); + } + } + DBSDataManipulator.ExecuteBatch deleteBatch = dataManipulator.deleteData( + session, + delKeyAttributes.keySet().toArray(new DBSAttributeBase[0]), + executionSource); + deleteBatch.add(delKeyAttributes.values().toArray()); + resultBatches.put(deleteBatch, new Object[0]); + } } } } @@ -693,7 +719,8 @@ public class WebSQLProcessor implements WebSessionProvider { DBCSession session, DBSDocumentLocator dataContainer, WebSQLResultsInfo resultsInfo, - WebSQLResultsRow row) throws DBException + WebSQLResultsRow row, + Map metaData) throws DBException { // Document reference DBDDocument document = null; @@ -702,16 +729,18 @@ public class WebSQLProcessor implements WebSessionProvider { for (int j = 0; j < attributes.length; j++) { DBDAttributeBinding attr = attributes[j]; Object plainValue = WebSQLUtils.makePlainCellValue(session, attr, row.getData()[j]); - if (plainValue instanceof DBDDocument) { + if (plainValue instanceof DBDDocument dbdDocument) { // FIXME: Hack for DynamoDB. We pass entire document as a key // FIXME: Let's just return it back for now - document = (DBDDocument) plainValue; - break; + if (dataContainer.isDocumentValid(dbdDocument)) { + document = (DBDDocument) plainValue; + break; + } } keyMap.put(attr.getName(), plainValue); } if (document == null) { - document = dataContainer.findDocument(session.getProgressMonitor(), keyMap); + document = dataContainer.findDocument(session, keyMap, metaData); if (document == null) { throw new DBCException("Error finding document by key " + keyMap); } @@ -826,7 +855,7 @@ public class WebSQLProcessor implements WebSessionProvider { boolean isDocumentValue = keyAttributes.length == 1 && keyAttribute.getDataKind() == DBPDataKind.DOCUMENT && dataContainer instanceof DBSDocumentLocator; if (isDocumentValue) { rowValues[i] = - makeDocumentInputValue(session, (DBSDocumentLocator) dataContainer, resultsInfo, row); + makeDocumentInputValue(session, (DBSDocumentLocator) dataContainer, resultsInfo, row, null); } else { Object inputCellValue = row.getData()[keyAttribute.getOrdinalPosition()]; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryDataReceiver.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryDataReceiver.java index a75a5cbedf..8585e0c6d9 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryDataReceiver.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryDataReceiver.java @@ -25,13 +25,15 @@ import org.jkiss.dbeaver.model.DBUtils; import org.jkiss.dbeaver.model.data.*; import org.jkiss.dbeaver.model.exec.*; import org.jkiss.dbeaver.model.impl.data.DBDValueError; +import org.jkiss.dbeaver.model.meta.MetaData; import org.jkiss.dbeaver.model.sql.DBQuotaException; import org.jkiss.dbeaver.model.struct.DBSDataContainer; import org.jkiss.dbeaver.model.struct.DBSEntity; import org.jkiss.utils.CommonUtils; -import java.util.ArrayList; -import java.util.List; +import java.lang.reflect.Method; +import java.util.*; +import java.util.stream.Collectors; class WebSQLQueryDataReceiver implements DBDDataReceiver { private static final Log log = Log.getLog(WebSQLQueryDataReceiver.class); @@ -42,7 +44,7 @@ class WebSQLQueryDataReceiver implements DBDDataReceiver { private final WebSQLQueryResultSet webResultSet = new WebSQLQueryResultSet(); private DBDAttributeBinding[] bindings; - private List rows = new ArrayList<>(); + private List rows = new ArrayList<>(); private final Number rowLimit; WebSQLQueryDataReceiver(WebSQLContextInfo contextInfo, DBSDataContainer dataContainer, WebDataFormat dataFormat) { @@ -70,6 +72,7 @@ class WebSQLQueryDataReceiver implements DBDDataReceiver { @Override public void fetchRow(DBCSession session, DBCResultSet resultSet) throws DBCException { + Map metaDataMap = null; Object[] row = new Object[bindings.length]; for (int i = 0; i < bindings.length; i++) { @@ -81,12 +84,25 @@ class WebSQLQueryDataReceiver implements DBDDataReceiver { binding.getMetaAttribute(), i); row[i] = cellValue; + if (cellValue != null) { + Method[] methods = cellValue.getClass().getMethods(); + for (Method method : methods) { + if (method.isAnnotationPresent(MetaData.class)) { + if (metaDataMap == null) { + metaDataMap = new HashMap<>(); + } + Object value = method.invoke(cellValue); + metaDataMap.put(method.getAnnotation(MetaData.class).name(), value); + } + } + } + } catch (Throwable e) { row[i] = new DBDValueError(e); } } - rows.add(row); + rows.add(new WebSQLQueryResultSetRow(row, metaDataMap)); if (rowLimit != null && rows.size() > rowLimit.longValue()) { throw new DBQuotaException( @@ -101,7 +117,7 @@ class WebSQLQueryDataReceiver implements DBDDataReceiver { DBSEntity entity = dataContainer instanceof DBSEntity ? (DBSEntity) dataContainer : null; try { - DBExecUtils.bindAttributes(session, entity, resultSet, bindings, rows); + DBExecUtils.bindAttributes(session, entity, resultSet, bindings, rows.stream().map(WebSQLQueryResultSetRow::getData).collect(Collectors.toList())); } catch (DBException e) { log.error("Error binding attributes", e); } @@ -122,15 +138,17 @@ class WebSQLQueryDataReceiver implements DBDDataReceiver { } // Convert row values - for (Object[] row : rows) { + for (WebSQLQueryResultSetRow row : rows) { for (int i = 0; i < bindings.length; i++) { DBDAttributeBinding binding = bindings[i]; - row[i] = WebSQLUtils.makeWebCellValue(webSession, binding, row[i], dataFormat); + row.getData()[i] = WebSQLUtils.makeWebCellValue(webSession, binding, row.getData()[i], dataFormat); } } webResultSet.setColumns(bindings); - webResultSet.setRows(rows.toArray(new Object[0][])); + webResultSet.setRows(List.of(rows.toArray(new WebSQLQueryResultSetRow[0]))); + webResultSet.setHasChildrenCollection(resultSet instanceof DBDSubCollectionResultSet); + webResultSet.setSupportsDataFilter(dataContainer.isFeatureSupported(DBSDataContainer.FEATURE_DATA_FILTER)); WebSQLResultsInfo resultsInfo = contextInfo.saveResult(dataContainer, bindings); webResultSet.setResultsInfo(resultsInfo); @@ -157,14 +175,14 @@ class WebSQLQueryDataReceiver implements DBDDataReceiver { // Convert original rows into new rows with leaf attributes // Extract values for leaf attributes from original row DBDAttributeBinding[] leafAttributes = leafBindings.toArray(new DBDAttributeBinding[0]); - List newRows = new ArrayList<>(); - for (Object[] row : rows) { + List newRows = new ArrayList<>(); + for (WebSQLQueryResultSetRow row : rows) { Object[] newRow = new Object[leafBindings.size()]; for (int i = 0; i < leafBindings.size(); i++) { DBDAttributeBinding leafAttr = leafBindings.get(i); try { //Object topValue = row[leafAttr.getTopParent().getOrdinalPosition()]; - Object cellValue = DBUtils.getAttributeValue(leafAttr, leafAttributes, row); + Object cellValue = DBUtils.getAttributeValue(leafAttr, leafAttributes, row.getData()); /* Object cellValue = leafAttr.getValueHandler().getValueFromObject( session, @@ -178,7 +196,7 @@ class WebSQLQueryDataReceiver implements DBDDataReceiver { newRow[i] = new DBDValueError(e); } } - newRows.add(newRow); + newRows.add(new WebSQLQueryResultSetRow(newRow, row.getMetaData())); } this.bindings = leafAttributes; this.rows = newRows; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultSet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultSet.java index 74576c2da3..73adabbb4a 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultSet.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultSet.java @@ -19,6 +19,11 @@ package io.cloudbeaver.service.sql; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.data.DBDAttributeBinding; import org.jkiss.dbeaver.model.meta.Property; +import org.jkiss.utils.Pair; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; /** * Web SQL query resultset. @@ -28,12 +33,15 @@ public class WebSQLQueryResultSet { private static final Log log = Log.getLog(WebSQLQueryResultSet.class); private WebSQLQueryResultColumn[] columns; - private Object[][] rows; + private List rows; private boolean hasMoreData; private WebSQLResultsInfo resultsInfo; private boolean singleEntity = true; private boolean hasRowIdentifier; + private boolean hasChildrenCollection; + private boolean isSupportsDataFilter; + public WebSQLQueryResultSet() { } @@ -60,11 +68,17 @@ public class WebSQLQueryResultSet { } @Property + @Deprecated public Object[][] getRows() { + return rows.stream().map(WebSQLQueryResultSetRow::getData).toArray(x -> new Object[x][1]); + } + + @Property + public List getRowsWithMetaData() { return rows; } - public void setRows(Object[][] rows) { + public void setRows(List rows) { this.rows = rows; } @@ -102,4 +116,22 @@ public class WebSQLQueryResultSet { public void setHasRowIdentifier(boolean hasRowIdentifier) { this.hasRowIdentifier = hasRowIdentifier; } + + @Property + public boolean isHasChildrenCollection() { + return hasChildrenCollection; + } + + public void setHasChildrenCollection(boolean hasSuCollection) { + this.hasChildrenCollection = hasSuCollection; + } + + @Property + public boolean isSupportsDataFilter() { + return isSupportsDataFilter; + } + + public void setSupportsDataFilter(boolean supportsDataFilter) { + isSupportsDataFilter = supportsDataFilter; + } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultSetRow.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultSetRow.java new file mode 100644 index 0000000000..30b4b15966 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultSetRow.java @@ -0,0 +1,31 @@ +package io.cloudbeaver.service.sql; + +import java.util.Map; + +public class WebSQLQueryResultSetRow { + + private Object[] data; + + private Map metaData; + + public WebSQLQueryResultSetRow(Object[] data, Map metaData) { + this.data = data; + this.metaData = metaData; + } + + public Object[] getData() { + return data; + } + + public Map getMetaData() { + return metaData; + } + + public void setData(Object[] data) { + this.data = data; + } + + public void setMetaData(Map metaData) { + this.metaData = metaData; + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResults.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResults.java index 36940912e9..8dd7690a51 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResults.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResults.java @@ -25,9 +25,11 @@ import org.jkiss.dbeaver.model.DBPDataKind; import org.jkiss.dbeaver.model.data.DBDDocument; import org.jkiss.dbeaver.model.exec.DBCException; import org.jkiss.dbeaver.model.meta.Property; +import org.jkiss.utils.Pair; import java.util.ArrayList; import java.util.List; +import java.util.Map; /** * Web SQL query results. @@ -86,16 +88,16 @@ public class WebSQLQueryResults { } List documents = new ArrayList<>(); - for (Object[] row : resultSet.getRows()) { - if (row.length != 1) { + for (WebSQLQueryResultSetRow row : resultSet.getRowsWithMetaData()) { + if (row.getData().length != 1) { log.debug("Non-document row content"); } - if (row[0] == null) { + if (row.getData()[0] == null) { documents.add(null); - } else if (row[0] instanceof DBDDocument) { - documents.add(new WebSQLDatabaseDocument(webSession, (DBDDocument) row[0])); + } else if (row.getData()[0] instanceof DBDDocument) { + documents.add(new WebSQLDatabaseDocument(webSession, (DBDDocument) row.getData()[0])); } else { - log.debug("Non-document row value: " + row[0].getClass().getName()); + log.debug("Non-document row value: " + row.getData()[0].getClass().getName()); } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLResultsRow.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLResultsRow.java index 3b01391db1..d651f44179 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLResultsRow.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLResultsRow.java @@ -16,6 +16,7 @@ */ package io.cloudbeaver.service.sql; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.model.data.json.JSONUtils; import java.util.Map; @@ -28,12 +29,16 @@ public class WebSQLResultsRow { private Object[] data; private Map updateValues; + @Nullable + private Map metaData; + public WebSQLResultsRow() { } public WebSQLResultsRow(Map map) { data = JSONUtils.getObjectList(map, "data").toArray(); updateValues = JSONUtils.getObject(map, "updateValues"); + metaData = JSONUtils.getObject(map, "metaData"); } public Object[] getData() { @@ -43,4 +48,9 @@ public class WebSQLResultsRow { public Map getUpdateValues() { return updateValues; } + + @Nullable + public Map getMetaData() { + return metaData; + } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java index dc4aef0309..8728b647c6 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java @@ -33,9 +33,7 @@ import org.jkiss.dbeaver.model.DBPDataSource; import org.jkiss.dbeaver.model.DBPDataSourceContainer; import org.jkiss.dbeaver.model.DBUtils; import org.jkiss.dbeaver.model.data.DBDAttributeBinding; -import org.jkiss.dbeaver.model.exec.DBCException; -import org.jkiss.dbeaver.model.exec.DBCLogicalOperator; -import org.jkiss.dbeaver.model.exec.DBExecUtils; +import org.jkiss.dbeaver.model.exec.*; import org.jkiss.dbeaver.model.impl.sql.BasicSQLDialect; import org.jkiss.dbeaver.model.navigator.DBNNode; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; @@ -50,6 +48,7 @@ import org.jkiss.dbeaver.model.sql.parser.SQLScriptParser; import org.jkiss.dbeaver.model.sql.registry.SQLGeneratorConfigurationRegistry; import org.jkiss.dbeaver.model.sql.registry.SQLGeneratorDescriptor; import org.jkiss.dbeaver.model.struct.DBSDataContainer; +import org.jkiss.dbeaver.model.struct.DBSEntity; import org.jkiss.dbeaver.model.struct.DBSObject; import org.jkiss.dbeaver.model.struct.DBSWrapper; import org.jkiss.dbeaver.utils.RuntimeUtils; @@ -60,9 +59,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.function.Supplier; import java.util.stream.Collectors; /** diff --git a/webapp/packages/core-sdk/src/queries/grid/getSqlExecuteTaskResults.gql b/webapp/packages/core-sdk/src/queries/grid/getSqlExecuteTaskResults.gql index ff32fdc4d1..4b633ccfc1 100644 --- a/webapp/packages/core-sdk/src/queries/grid/getSqlExecuteTaskResults.gql +++ b/webapp/packages/core-sdk/src/queries/grid/getSqlExecuteTaskResults.gql @@ -1,46 +1,47 @@ -mutation getSqlExecuteTaskResults( - $taskId: ID! -) { - result: asyncSqlExecuteResults( - taskId: $taskId - ) { - duration - statusMessage - filterText - fullQuery - results { - title - updateRowCount - sourceQuery - dataFormat - resultSet { - id - columns { - dataKind - entityName - fullTypeName - icon - label - maxLength - name - position - precision - required - readOnly - readOnlyStatus - scale - typeName - supportedOperations { - id - expression - argumentCount - } - } - rows - singleEntity - hasMoreData - hasRowIdentifier - } +mutation getSqlExecuteTaskResults($taskId: ID!) { + result: asyncSqlExecuteResults(taskId: $taskId) { + duration + statusMessage + filterText + fullQuery + results { + title + updateRowCount + sourceQuery + dataFormat + resultSet { + id + columns { + dataKind + entityName + fullTypeName + icon + label + maxLength + name + position + precision + required + readOnly + readOnlyStatus + scale + typeName + supportedOperations { + id + expression + argumentCount + } } + rowsWithMetaData { + data + metaData + } + singleEntity + hasMoreData + hasRowIdentifier + isSupportsDataFilter + hasChildrenCollection + } } + } } diff --git a/webapp/packages/core-sdk/src/queries/grid/updateResultsDataBatch.gql b/webapp/packages/core-sdk/src/queries/grid/updateResultsDataBatch.gql index 296a1fa30b..306137c418 100644 --- a/webapp/packages/core-sdk/src/queries/grid/updateResultsDataBatch.gql +++ b/webapp/packages/core-sdk/src/queries/grid/updateResultsDataBatch.gql @@ -1,33 +1,38 @@ mutation updateResultsDataBatch( - $projectId: ID! - $connectionId: ID! - $contextId: ID! - $resultsId: ID! - $updatedRows: [ SQLResultRow! ] - $deletedRows: [ SQLResultRow! ] - $addedRows: [ SQLResultRow! ] + $projectId: ID! + $connectionId: ID! + $contextId: ID! + $resultsId: ID! + $updatedRows: [SQLResultRow!] + $deletedRows: [SQLResultRow!] + $addedRows: [SQLResultRow!] ) { - result: updateResultsDataBatch( - projectId: $projectId - connectionId: $connectionId - contextId: $contextId - resultsId: $resultsId + result: updateResultsDataBatch( + projectId: $projectId + connectionId: $connectionId + contextId: $contextId + resultsId: $resultsId - updatedRows: $updatedRows - deletedRows: $deletedRows - addedRows: $addedRows - ) { - duration - filterText - results { - updateRowCount - resultSet { - id - rows - singleEntity - hasMoreData - hasRowIdentifier - } + updatedRows: $updatedRows + deletedRows: $deletedRows + addedRows: $addedRows + ) { + duration + filterText + results { + updateRowCount + resultSet { + id + rowsWithMetaData { + data + metaData } + singleEntity + hasMoreData + hasRowIdentifier + isSupportsDataFilter + hasChildrenCollection + } } -} \ No newline at end of file + } +} diff --git a/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts b/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts index 8e96004c75..d8a6618eb0 100644 --- a/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts @@ -255,8 +255,8 @@ export class ContainerDataSource extends ResultSetDataSource | undefined { - return this.tableViewerStorageService.get(modelId); + get(tableId: string): IDatabaseDataModel | undefined { + return this.tableViewerStorageService.get(tableId); } async removeTableModel(tableId: string): Promise { diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/Document/DocumentDataAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/Document/DocumentDataAction.ts index 1bc07624ff..515b013c27 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/Document/DocumentDataAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/Document/DocumentDataAction.ts @@ -21,11 +21,11 @@ export class DocumentDataAction extends DatabaseDataResultAction row[0]) || []; + return this.result.data?.rowsWithMetaData?.map(row => row.data[0]) || []; } get count(): number { - return this.result.data?.rows?.length || 0; + return this.result.data?.rowsWithMetaData?.length || 0; } constructor(source: IDatabaseDataSource) { @@ -37,6 +37,11 @@ export class DocumentDataAction extends DatabaseDataResultAction row.data[0]?.id === documentId); + return row?.metaData; + } + getIdentifier(key: IDocumentElementKey): string { return key.index.toString(); } @@ -54,8 +59,12 @@ export class DocumentDataAction extends DatabaseDataResultAction extends IDatabaseDataAction { + has(key: TKey, scope: symbol): boolean; + get(key: TKey, scope: symbol): T | undefined; + set(key: TKey, scope: symbol, value: T): void; + delete(key: TKey, scope: symbol): void; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetCacheAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetCacheAction.ts new file mode 100644 index 0000000000..c1af786deb --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetCacheAction.ts @@ -0,0 +1,150 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { makeObservable, observable } from 'mobx'; + +import { ResultDataFormat } from '@cloudbeaver/core-sdk'; + +import { DatabaseDataAction } from '../../DatabaseDataAction'; +import type { IDatabaseDataSource } from '../../IDatabaseDataSource'; +import type { IDatabaseResultSet } from '../../IDatabaseResultSet'; +import { databaseDataAction } from '../DatabaseDataActionDecorator'; +import type { IDatabaseDataCacheAction } from '../IDatabaseDataCacheAction'; +import type { IResultSetElementKey, IResultSetRowKey } from './IResultSetDataKey'; +import { ResultSetDataAction } from './ResultSetDataAction'; + +@databaseDataAction() +export class ResultSetCacheAction + extends DatabaseDataAction + implements IDatabaseDataCacheAction +{ + static dataFormat = [ResultDataFormat.Resultset]; + + private readonly cache: Map>; + + constructor(source: IDatabaseDataSource, private readonly data: ResultSetDataAction) { + super(source); + + this.cache = new Map(); + + makeObservable(this, { + cache: observable, + }); + } + + get(key: IResultSetElementKey, scope: symbol): T | undefined { + const keyCache = this.getKeyCache(key); + if (!keyCache) { + return; + } + + return keyCache.get(scope); + } + + getRow(key: IResultSetRowKey, scope: symbol): T | undefined { + const keyCache = this.getRowCache(key); + if (!keyCache) { + return; + } + + return keyCache.get(scope); + } + + has(key: IResultSetElementKey, scope: symbol) { + const keyCache = this.getKeyCache(key); + + if (!keyCache) { + return false; + } + + return keyCache.has(scope); + } + + hasRow(key: IResultSetRowKey, scope: symbol) { + const keyCache = this.getRowCache(key); + + if (!keyCache) { + return false; + } + + return keyCache.has(scope); + } + + set(key: IResultSetElementKey, scope: symbol, value: T) { + const keyCache = this.getOrCreateKeyCache(key); + + keyCache.set(scope, value); + } + + setRow(key: IResultSetRowKey, scope: symbol, value: T) { + const keyCache = this.getOrCreateRowKeyCache(key); + + keyCache.set(scope, value); + } + + delete(key: IResultSetElementKey, scope: symbol) { + const keyCache = this.getKeyCache(key); + + if (keyCache) { + keyCache.delete(scope); + } + } + + deleteRow(key: IResultSetRowKey, scope: symbol) { + const keyCache = this.getRowCache(key); + + if (keyCache) { + keyCache.delete(scope); + } + } + + afterResultUpdate() { + this.cache.clear(); + } + + dispose(): void { + this.cache.clear(); + } + + private serializeRowKey(key: IResultSetRowKey) { + return 'row:' + this.data.serializeRowKey(key); + } + + private serializeKey(key: IResultSetElementKey) { + return this.data.serialize(key); + } + + private getKeyCache(key: IResultSetElementKey) { + return this.cache.get(this.serializeKey(key)); + } + + private getRowCache(key: IResultSetRowKey) { + return this.cache.get(this.serializeRowKey(key)); + } + + private getOrCreateKeyCache(key: IResultSetElementKey) { + let keyCache = this.getKeyCache(key); + + if (!keyCache) { + keyCache = observable(new Map()); + this.cache.set(this.serializeKey(key), keyCache); + } + + return keyCache; + } + + private getOrCreateRowKeyCache(key: IResultSetRowKey) { + let keyCache = this.getRowCache(key); + + if (!keyCache) { + keyCache = observable(new Map()); + this.cache.set(this.serializeRowKey(key), keyCache); + } + + return keyCache; + } +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts index 62498fead3..53bfef3dcc 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts @@ -23,8 +23,8 @@ import type { IResultSetValue } from './ResultSetFormatAction'; export class ResultSetDataAction extends DatabaseDataResultAction { static dataFormat = [ResultDataFormat.Resultset]; - get rows(): IResultSetValue[][] { - return this.result.data?.rows || []; + get rows() { + return this.result.data?.rowsWithMetaData || []; } get columns(): SqlResultColumn[] { @@ -47,6 +47,10 @@ export class ResultSetDataAction extends DatabaseDataResultAction= this.rows.length) { + return undefined; + } + + return this.rows[row.index].metaData; } getCellValue(cell: IResultSetElementKey): IResultSetValue | undefined { @@ -99,7 +111,7 @@ export class ResultSetDataAction extends DatabaseDataResultAction(a: T, b: T): boolean { + isEqual(a: T, b: T): boolean { if (a.index !== b.index) { return false; } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetEditAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetEditAction.ts index eca935b74b..f0ecf90b0e 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetEditAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetEditAction.ts @@ -316,7 +316,7 @@ export class ResultSetEditAction extends DatabaseEditAction row.data || []); } get columns(): SqlResultColumn[] { diff --git a/webapp/packages/plugin-data-viewer/src/index.ts b/webapp/packages/plugin-data-viewer/src/index.ts index f42d855e6c..727305b0f6 100644 --- a/webapp/packages/plugin-data-viewer/src/index.ts +++ b/webapp/packages/plugin-data-viewer/src/index.ts @@ -43,6 +43,7 @@ export * from './DatabaseDataModel/Actions/IDatabaseDataFormatAction'; export * from './DatabaseDataModel/Actions/IDatabaseDataMetadataAction'; export * from './DatabaseDataModel/Actions/IDatabaseDataResultAction'; export * from './DatabaseDataModel/Actions/IDatabaseDataSelectAction'; +export * from './DatabaseDataModel/Actions/ResultSet/ResultSetCacheAction'; export * from './DatabaseDataModel/DatabaseDataAction'; export * from './DatabaseDataModel/DatabaseDataActions'; export * from './DatabaseDataModel/DatabaseDataFormat'; diff --git a/webapp/packages/plugin-data-viewer/src/locales/en.ts b/webapp/packages/plugin-data-viewer/src/locales/en.ts index 561ec3ba7e..6831fdab89 100644 --- a/webapp/packages/plugin-data-viewer/src/locales/en.ts +++ b/webapp/packages/plugin-data-viewer/src/locales/en.ts @@ -47,6 +47,7 @@ export default [ ['data_viewer_refresh_result_set', 'Refresh result set'], ['data_viewer_total_count_tooltip', 'Get total count'], ['data_viewer_total_count_failed', 'Failed to get total count'], + ['data_viewer_model_not_loaded', 'Table model is not loaded'], ['settings_data_editor', 'Data Editor'], ['settings_data_editor_disable_edit_name', 'Disable Edit'], ['settings_data_editor_disable_edit_description', 'Disable editing of data in Data Viewer'], diff --git a/webapp/packages/plugin-data-viewer/src/locales/it.ts b/webapp/packages/plugin-data-viewer/src/locales/it.ts index 96a393504d..35306f85a3 100644 --- a/webapp/packages/plugin-data-viewer/src/locales/it.ts +++ b/webapp/packages/plugin-data-viewer/src/locales/it.ts @@ -40,6 +40,7 @@ export default [ ['data_viewer_refresh_result_set', 'Refresh result set'], ['data_viewer_total_count_tooltip', 'Get total count'], ['data_viewer_total_count_failed', 'Failed to get total count'], + ['data_viewer_model_not_loaded', 'Table model is not loaded'], ['settings_data_editor', 'Data Editor'], ['settings_data_editor_disable_edit_name', 'Disable Edit'], ['settings_data_editor_disable_edit_description', 'Disable editing of data in Data Viewer'], diff --git a/webapp/packages/plugin-data-viewer/src/locales/ru.ts b/webapp/packages/plugin-data-viewer/src/locales/ru.ts index c3d723412d..05dd579c74 100644 --- a/webapp/packages/plugin-data-viewer/src/locales/ru.ts +++ b/webapp/packages/plugin-data-viewer/src/locales/ru.ts @@ -41,6 +41,7 @@ export default [ ['data_viewer_refresh_result_set', 'Обновить резалт сет'], ['data_viewer_total_count_tooltip', 'Получить количество записей'], ['data_viewer_total_count_failed', 'Не удалось получить количество записей'], + ['data_viewer_model_not_loaded', 'Не удалось загрузить модель таблицы'], ['settings_data_editor', 'Редактор данных'], ['settings_data_editor_disable_edit_name', 'Отключить редактирование'], ['settings_data_editor_disable_edit_description', 'Отключить редактирование данных'], diff --git a/webapp/packages/plugin-data-viewer/src/locales/zh.ts b/webapp/packages/plugin-data-viewer/src/locales/zh.ts index 2a70079e1b..4c0bba6c65 100644 --- a/webapp/packages/plugin-data-viewer/src/locales/zh.ts +++ b/webapp/packages/plugin-data-viewer/src/locales/zh.ts @@ -47,6 +47,7 @@ export default [ ['data_viewer_refresh_result_set', 'Refresh result set'], ['data_viewer_total_count_failed', 'Failed to get total count'], ['data_viewer_total_count_tooltip', 'Get total count'], + ['data_viewer_model_not_loaded', 'Table model is not loaded'], ['settings_data_editor', 'Data Editor'], ['settings_data_editor_disable_edit_name', 'Disable Edit'], ['settings_data_editor_disable_edit_description', 'Disable editing of data in Data Viewer'], diff --git a/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts b/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts index 569355d3d6..77aafb84cf 100644 --- a/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts +++ b/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts @@ -280,8 +280,8 @@ export class QueryDataSource