diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/WebSQLResultsInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/WebSQLResultsInfo.java index ca2bc074e5..3fd68da3c2 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/WebSQLResultsInfo.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/WebSQLResultsInfo.java @@ -26,7 +26,9 @@ import org.jkiss.dbeaver.model.struct.DBSDataContainer; import org.jkiss.dbeaver.model.struct.DBSTypedObject; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * Web query results info. @@ -82,6 +84,18 @@ public DBDRowIdentifier getDefaultRowIdentifier() { return null; } + @NotNull + public Set getRowIdentifiers() { + Set rowIdentifiers = new HashSet<>(); + for (DBDAttributeBinding column : attributes) { + DBDRowIdentifier rowIdentifier = column.getRowIdentifier(); + if (rowIdentifier != null) { + rowIdentifiers.add(rowIdentifier); + } + } + return rowIdentifiers; + } + public DBSAttributeBase getAttribute(String attributeName) { DBPDataSource dataSource = dataContainer.getDataSource(); 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 4ea954fae9..28b3b20261 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 @@ -55,6 +55,7 @@ import java.lang.reflect.InvocationTargetException; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; /** * Web SQL processor. @@ -315,56 +316,67 @@ public WebSQLExecuteInfo updateResultsDataBatch( @Nullable List addedRows, @Nullable WebDataFormat dataFormat) throws DBException { - Map resultBatches = new LinkedHashMap<>(); - + List newResultSetRows = new ArrayList<>(); KeyDataReceiver keyReceiver = new KeyDataReceiver(contextInfo.getResults(resultsId)); - - DBSDataManipulator dataManipulator = generateUpdateResultsDataBatch( - monitor, contextInfo, resultsId, updatedRows, deletedRows, addedRows, dataFormat, resultBatches, keyReceiver); - WebSQLResultsInfo resultsInfo = contextInfo.getResults(resultsId); + Set rowIdentifierList = new HashSet<>(); + // several row identifiers could be if we update result set table with join + // we can't add or delete rows from result set table with join + if (!CommonUtils.isEmpty(deletedRows) || !CommonUtils.isEmpty(addedRows)) { + rowIdentifierList.add(resultsInfo.getDefaultRowIdentifier()); + } else if (!CommonUtils.isEmpty(updatedRows)) { + rowIdentifierList = resultsInfo.getRowIdentifiers(); + } + long totalUpdateCount = 0; WebSQLExecuteInfo result = new WebSQLExecuteInfo(); List queryResults = new ArrayList<>(); + for (var rowIdentifier : rowIdentifierList) { + Map resultBatches = new LinkedHashMap<>(); + DBSDataManipulator dataManipulator = generateUpdateResultsDataBatch( + monitor, resultsInfo, rowIdentifier, updatedRows, deletedRows, addedRows, dataFormat, resultBatches, keyReceiver); + + + DBCExecutionContext executionContext = getExecutionContext(dataManipulator); + try (DBCSession session = executionContext.openSession(monitor, DBCExecutionPurpose.USER, "Update data in container")) { + DBCTransactionManager txnManager = DBUtils.getTransactionManager(executionContext); + boolean revertToAutoCommit = false; + if (txnManager != null && txnManager.isSupportsTransactions() && txnManager.isAutoCommit()) { + txnManager.setAutoCommit(monitor, false); + revertToAutoCommit = true; + } + try { + Map options = Collections.emptyMap(); + for (Map.Entry rb : resultBatches.entrySet()) { + DBSDataManipulator.ExecuteBatch batch = rb.getKey(); + Object[] rowValues = rb.getValue(); + keyReceiver.setRow(rowValues); + DBCStatistics statistics = batch.execute(session, options); + + // Patch result rows (adapt to web format) + for (int i = 0; i < rowValues.length; i++) { + rowValues[i] = WebSQLUtils.makeWebCellValue(webSession, resultsInfo.getAttributeByPosition(i), rowValues[i], dataFormat); + } - DBCExecutionContext executionContext = getExecutionContext(dataManipulator); - try (DBCSession session = executionContext.openSession(monitor, DBCExecutionPurpose.USER, "Update data in container")) { - DBCTransactionManager txnManager = DBUtils.getTransactionManager(executionContext); - boolean revertToAutoCommit = false; - if (txnManager != null && txnManager.isSupportsTransactions() && txnManager.isAutoCommit()) { - txnManager.setAutoCommit(monitor, false); - revertToAutoCommit = true; - } - try { - Map options = Collections.emptyMap(); - for (Map.Entry rb : resultBatches.entrySet()) { - DBSDataManipulator.ExecuteBatch batch = rb.getKey(); - Object[] rowValues = rb.getValue(); - keyReceiver.setRow(rowValues); - DBCStatistics statistics = batch.execute(session, options); - - // Patch result rows (adapt to web format) - for (int i = 0; i < rowValues.length; i++) { - rowValues[i] = WebSQLUtils.makeWebCellValue(webSession, resultsInfo.getAttributeByPosition(i), rowValues[i], dataFormat); + totalUpdateCount += statistics.getRowsUpdated(); + result.setDuration(result.getDuration() + statistics.getExecuteTime()); + newResultSetRows.add(rowValues); } - totalUpdateCount += statistics.getRowsUpdated(); - result.setDuration(result.getDuration() + statistics.getExecuteTime()); - } - - if (txnManager != null && txnManager.isSupportsTransactions()) { - txnManager.commit(session); - } - } catch (Exception e) { - if (txnManager != null && txnManager.isSupportsTransactions()) { - txnManager.rollback(session, null); - } - throw new DBCException("Error persisting data changes", e); - } finally { - if (revertToAutoCommit) { - txnManager.setAutoCommit(monitor, true); + if (txnManager != null && txnManager.isSupportsTransactions()) { + txnManager.commit(session); + } + } catch (Exception e) { + if (txnManager != null && txnManager.isSupportsTransactions()) { + txnManager.rollback(session, null); + } + throw new DBCException("Error persisting data changes", e); + } finally { + if (revertToAutoCommit) { + txnManager.setAutoCommit(monitor, true); + } } } } @@ -376,7 +388,7 @@ public WebSQLExecuteInfo updateResultsDataBatch( WebSQLQueryResults updateResults = new WebSQLQueryResults(webSession, dataFormat); updateResults.setUpdateRowCount(totalUpdateCount); updateResults.setResultSet(updatedResultSet); - updatedResultSet.setRows(resultBatches.values().toArray(new Object[0][])); + updatedResultSet.setRows(newResultSetRows.toArray(new Object[0][])); queryResults.add(updateResults); @@ -396,26 +408,42 @@ public String generateResultsDataUpdateScript( { Map resultBatches = new LinkedHashMap<>(); - DBSDataManipulator dataManipulator = generateUpdateResultsDataBatch( - monitor, contextInfo, resultsId, updatedRows, deletedRows, addedRows, dataFormat, resultBatches, null); - List actions = new ArrayList<>(); - - DBCExecutionContext executionContext = getExecutionContext(dataManipulator); - try (DBCSession session = executionContext.openSession(monitor, DBCExecutionPurpose.USER, "Update data in container")) { - Map options = Collections.emptyMap(); - for (DBSDataManipulator.ExecuteBatch batch : resultBatches.keySet()) { - batch.generatePersistActions(session, actions, options); + WebSQLResultsInfo resultsInfo = contextInfo.getResults(resultsId); + Set rowIdentifierList = new HashSet<>(); + // several row identifiers could be if we update result set table with join + // we can't add or delete rows from result set table with join + if (!CommonUtils.isEmpty(deletedRows) || !CommonUtils.isEmpty(addedRows)) { + rowIdentifierList.add(resultsInfo.getDefaultRowIdentifier()); + } else if (!CommonUtils.isEmpty(updatedRows)) { + rowIdentifierList = resultsInfo.getRowIdentifiers(); + } + StringBuilder sqlBuilder = new StringBuilder(); + for (var rowIdentifier : rowIdentifierList) { + DBSDataManipulator dataManipulator = generateUpdateResultsDataBatch( + monitor, resultsInfo, rowIdentifier, updatedRows, deletedRows, addedRows, dataFormat, resultBatches, null); + + List actions = new ArrayList<>(); + + DBCExecutionContext executionContext = getExecutionContext(dataManipulator); + try (DBCSession session = executionContext.openSession(monitor, DBCExecutionPurpose.USER, "Update data in container")) { + Map options = Collections.emptyMap(); + for (DBSDataManipulator.ExecuteBatch batch : resultBatches.keySet()) { + batch.generatePersistActions(session, actions, options); + } } - } - return SQLUtils.generateScript(executionContext.getDataSource(), actions.toArray(new DBEPersistAction[0]), false); + sqlBuilder.append( + SQLUtils.generateScript(executionContext.getDataSource(), actions.toArray(new DBEPersistAction[0]), false) + ); + } + return sqlBuilder.toString(); } private DBSDataManipulator generateUpdateResultsDataBatch( @NotNull DBRProgressMonitor monitor, - @NotNull WebSQLContextInfo contextInfo, - @NotNull String resultsId, + @NotNull WebSQLResultsInfo resultsInfo, + @NotNull DBDRowIdentifier rowIdentifier, @Nullable List updatedRows, @Nullable List deletedRows, @Nullable List addedRows, @@ -424,10 +452,7 @@ private DBSDataManipulator generateUpdateResultsDataBatch( @Nullable DBDDataReceiver keyReceiver) throws DBException { - WebSQLResultsInfo resultsInfo = contextInfo.getResults(resultsId); - DBDRowIdentifier rowIdentifier = resultsInfo.getDefaultRowIdentifier(); - checkRowIdentifier(resultsInfo, rowIdentifier); DBSEntity dataContainer = rowIdentifier.getEntity(); checkDataEditAllowed(dataContainer); DBSDataManipulator dataManipulator = (DBSDataManipulator) dataContainer; @@ -448,7 +473,9 @@ private DBSDataManipulator generateUpdateResultsDataBatch( if (!CommonUtils.isEmpty(updatedRows)) { for (WebSQLResultsRow row : updatedRows) { - Map updateValues = row.getUpdateValues(); + Map updateValues = row.getUpdateValues().entrySet().stream() + .filter(x -> CommonUtils.equalObjects(allAttributes[CommonUtils.toInt(x.getKey())].getRowIdentifier(), rowIdentifier)) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); if (CommonUtils.isEmpty(row.getData()) || CommonUtils.isEmpty(updateValues)) { continue; } diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/ITreeNodeState.ts b/webapp/packages/core-blocks/src/Tree/TreeNode/ITreeNodeState.ts index 0fc859df39..712f03f81d 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/ITreeNodeState.ts +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/ITreeNodeState.ts @@ -11,6 +11,7 @@ export interface ITreeNodeState { disabled?: boolean; loading?: boolean; selected?: boolean; + /** It is true when the node is neither selected nor unselected, used to indicate a mixed or partial selection in a group of sub-options */ indeterminateSelected?: boolean; externalExpanded?: boolean; expanded?: boolean; diff --git a/webapp/packages/core-cli/configs/webpack.product.dev.config.js b/webapp/packages/core-cli/configs/webpack.product.dev.config.js index 32653b5b61..44be0161a2 100644 --- a/webapp/packages/core-cli/configs/webpack.product.dev.config.js +++ b/webapp/packages/core-cli/configs/webpack.product.dev.config.js @@ -61,10 +61,12 @@ module.exports = (env, argv) => { proxy: { '/api': { target: envServer, + secure: false, }, '/api/ws': { target: `${urlObject.protocol === 'https:' ? 'wss:' : 'ws:'}//${urlObject.hostname}:${urlObject.port}/api/ws`, ws: true, + secure: false, }, }, onListening: function (devServer, ...args) { diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVResultSetGroupingPresentation.tsx b/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVResultSetGroupingPresentation.tsx index 9ab3d72630..62d9456b3d 100755 --- a/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVResultSetGroupingPresentation.tsx +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVResultSetGroupingPresentation.tsx @@ -15,11 +15,11 @@ import { CaptureViewContext, useDataContext } from '@cloudbeaver/core-view'; import { DataPresentationComponent, IDatabaseResultSet, TableViewerLoader } from '@cloudbeaver/plugin-data-viewer'; import { DATA_CONTEXT_DV_DDM_RS_GROUPING } from './DataContext/DATA_CONTEXT_DV_DDM_RS_GROUPING'; +import { DEFAULT_GROUPING_QUERY_OPERATION } from './DEFAULT_GROUPING_QUERY_OPERATION'; import type { IGroupingQueryState } from './IGroupingQueryState'; import { useGroupingData } from './useGroupingData'; import { useGroupingDataModel } from './useGroupingDataModel'; import { useGroupingDnDColumns } from './useGroupingDnDColumns'; -import { DEFAULT_GROUPING_QUERY_OPERATION } from './DEFAULT_GROUPING_QUERY_OPERATION'; const styles = css` drop-area { diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataModel.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataModel.ts index 44be18e79c..84ce052e64 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataModel.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataModel.ts @@ -16,12 +16,15 @@ export interface IRequestEventData; } +/** Represents an interface for interacting with a database. It is used for managing and requesting data. */ export interface IDatabaseDataModel { readonly id: string; readonly name: string | null; readonly source: IDatabaseDataSource; + /** Holds metadata about a data request. */ readonly requestInfo: IRequestInfo; readonly supportedDataFormats: ResultDataFormat[]; + /** Represents the value by which the number of loaded rows will be increased when loading the next data portion */ readonly countGain: number; readonly onOptionsChange: IExecutor; diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataOptions.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataOptions.ts index 50f56e43e0..95a970d28d 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataOptions.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataOptions.ts @@ -12,7 +12,9 @@ export interface IDatabaseDataOptions { connectionKey: IConnectionInfoParams; schema?: string; catalog?: string; + /** A raw string representation of the query filter conditions ("id=4") */ whereFilter: string; + /** A complex object that can represent filters and sorting options of the result set */ constraints: SqlDataFilterConstraint[]; readLogs?: boolean; } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts index 00d33aa2a8..8a081b7f7c 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts @@ -17,6 +17,7 @@ export interface IRequestInfo { readonly originalQuery: string; readonly requestDuration: number; readonly requestMessage: string; + /** A string representation of the filters constraints applied to the data request. Also returns as it is in case of whereFilter */ readonly requestFilter: string; readonly source: string | null; } @@ -30,11 +31,13 @@ export interface IDatabaseDataSource; readonly results: TResult[]; readonly offset: number; readonly count: number; + /** Options of the previous request */ readonly prevOptions: Readonly | null; readonly options: TOptions | null; readonly requestInfo: IRequestInfo; @@ -80,6 +83,8 @@ export interface IDatabaseDataSource this; retry: () => Promise; + /** Allows to perform an asynchronous action on the data source, this action will wait previous action to finish and save or load requests. + * The data source will have a loading and disabled state while performing an action */ runTask: (task: () => Promise) => Promise; requestData: () => Promise | void; refreshData: () => Promise | void; diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx index d16c21b5fd..066b65ce0f 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx @@ -99,6 +99,7 @@ interface Props { resultIndex: number | undefined; presentationId: string | undefined; valuePresentationId: string | null | undefined; + /** Display data in simple mode, some features will be hidden or disabled */ simple?: boolean; context?: IDataContext; className?: string; diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTree.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTree.tsx index ce6db591ae..efa8684aa5 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTree.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTree.tsx @@ -43,9 +43,12 @@ import { IElementsTreeOptions, useElementsTree } from './useElementsTree'; import { useElementsTreeFolderExplorer } from './useElementsTreeFolderExplorer'; export interface ElementsTreeProps extends IElementsTreeOptions, React.PropsWithChildren { + /** Specifies the root path for the tree. ROOT_NODE_PATH will be used if not defined */ root?: string; selectionTree?: boolean; + /** Specifies a custom control component for navigation tree */ control?: NavTreeControlComponent; + /** A placeholder component to be displayed when the elements tree is empty */ emptyPlaceholder?: React.FC; className?: string; settingsElements?: PlaceholderElement[]; diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts index ab8d236078..18e63c8e9f 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts @@ -71,6 +71,7 @@ export interface IElementsTreeOptions { renderers?: IElementsTreeCustomRenderer[]; nodeInfoTransformers?: IElementsTreeCustomNodeInfo[]; expandStateGetters?: IElementsTreeNodeExpandInfoGetter[]; + /** Allows to pass external state. It can be used to manipulate a tree state from the outside or to store it in an external state */ localState?: MetadataMap; getChildren: (id: string) => string[] | undefined; loadChildren: (id: string, manual: boolean) => Promise;