Implement bulk delete for nosql

This commit is contained in:
Laurent Nguyen 2024-04-29 18:26:46 +02:00
parent 0887998461
commit 299672435e
4 changed files with 132 additions and 37 deletions

View File

@ -1,9 +1,9 @@
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
export const getEntityName = (): string => { export const getEntityName = (multiple?: boolean): string => {
if (userContext.apiType === "Mongo") { if (userContext.apiType === "Mongo") {
return "document"; return multiple ? "documents" : "document";
} }
return "item"; return multiple ? "items" : "item";
}; };

View File

@ -1,3 +1,4 @@
import { BulkOperationType, OperationInput, PartitionKey } from "@azure/cosmos";
import { CollectionBase } from "../../Contracts/ViewModels"; import { CollectionBase } from "../../Contracts/ViewModels";
import DocumentId from "../../Explorer/Tree/DocumentId"; import DocumentId from "../../Explorer/Tree/DocumentId";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
@ -24,3 +25,42 @@ export const deleteDocument = async (collection: CollectionBase, documentId: Doc
clearMessage(); clearMessage();
} }
}; };
/**
* Bulk delete documents
* @param collection
* @param documentId
* @returns array of ids that were successfully deleted
*/
export const deleteDocuments = async (
collection: CollectionBase,
documentIds: {
id: string;
partitionKey?: PartitionKey;
}[],
): Promise<string[]> => {
const clearMessage = logConsoleProgress(`Deleting ${documentIds.length} ${getEntityName(true)}`);
try {
const v2Container = await client().database(collection.databaseId).container(collection.id());
const operations: OperationInput[] = documentIds.map((documentId) => ({
...documentId,
operationType: BulkOperationType.Delete,
}));
const bulkResult = await v2Container.items.bulk(operations);
const result: string[] = [];
documentIds.forEach((documentId, index) => {
if (bulkResult[index].statusCode === 204) {
result.push(documentId.id);
}
});
logConsoleInfo(`Successfully deleted ${getEntityName(true)}: ${result.length} out of ${documentIds.length}`);
// TODO: handle case result.length != documentIds.length
return result;
} catch (error) {
handleError(error, "DeleteDocuments", `Error while deleting ${documentIds.length} ${getEntityName(true)}`);
throw error;
} finally {
clearMessage();
}
};

View File

@ -7,7 +7,7 @@ import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import MongoUtility from "Common/MongoUtility"; import MongoUtility from "Common/MongoUtility";
import { StyleConstants } from "Common/StyleConstants"; import { StyleConstants } from "Common/StyleConstants";
import { createDocument } from "Common/dataAccess/createDocument"; import { createDocument } from "Common/dataAccess/createDocument";
import { deleteDocument } from "Common/dataAccess/deleteDocument"; import { deleteDocuments as deleteNoSqlDocuments } from "Common/dataAccess/deleteDocument";
import { queryDocuments } from "Common/dataAccess/queryDocuments"; import { queryDocuments } from "Common/dataAccess/queryDocuments";
import { readDocument } from "Common/dataAccess/readDocument"; import { readDocument } from "Common/dataAccess/readDocument";
import { updateDocument } from "Common/dataAccess/updateDocument"; import { updateDocument } from "Common/dataAccess/updateDocument";
@ -734,37 +734,41 @@ const DocumentsTabComponent: React.FunctionComponent<{
// setEditorState, // setEditorState,
]); ]);
let __deleteDocument = useCallback( /**
(documentId: DocumentId): Promise<void> => deleteDocument(_collection, documentId), * Implementation using bulk delete
[_collection], */
); let _deleteDocuments = useCallback(
async (toDeleteDocumentIds: DocumentId[]): Promise<string[]> => {
const _deleteDocuments = useCallback(
(documentId: DocumentId): Promise<DocumentId> => {
onExecutionErrorChange(false); onExecutionErrorChange(false);
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocument, { const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocuments, {
dataExplorerArea: Constants.Areas.Tab, dataExplorerArea: Constants.Areas.Tab,
tabTitle, tabTitle,
}); });
setIsExecuting(true); setIsExecuting(true);
return __deleteDocument(documentId) return deleteNoSqlDocuments(
_collection,
toDeleteDocumentIds.map((id) => ({
id: id.id(),
partitionKey: id.partitionKeyValue,
})),
)
.then( .then(
() => { (deletedIds) => {
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
Action.DeleteDocument, Action.DeleteDocuments,
{ {
dataExplorerArea: Constants.Areas.Tab, dataExplorerArea: Constants.Areas.Tab,
tabTitle, tabTitle,
}, },
startKey, startKey,
); );
return documentId; return deletedIds;
}, },
(error) => { (error) => {
onExecutionErrorChange(true); onExecutionErrorChange(true);
console.error(error); console.error(error);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.DeleteDocument, Action.DeleteDocuments,
{ {
dataExplorerArea: Constants.Areas.Tab, dataExplorerArea: Constants.Areas.Tab,
tabTitle, tabTitle,
@ -778,21 +782,20 @@ const DocumentsTabComponent: React.FunctionComponent<{
) )
.finally(() => setIsExecuting(false)); .finally(() => setIsExecuting(false));
}, },
[__deleteDocument, onExecutionErrorChange, tabTitle], [_collection, onExecutionErrorChange, tabTitle],
); );
const deleteDocuments = useCallback( const deleteDocuments = useCallback(
(toDeleteDocumentIds: DocumentId[]): void => { (toDeleteDocumentIds: DocumentId[]): void => {
onExecutionErrorChange(false); onExecutionErrorChange(false);
setIsExecuting(true); setIsExecuting(true);
const promises = toDeleteDocumentIds.map((documentId) => _deleteDocuments(documentId)); _deleteDocuments(toDeleteDocumentIds)
Promise.all(promises) .then((deletedIds: string[]) => {
.then((deletedDocumentIds: DocumentId[]) => {
const newDocumentIds = [...documentIds]; const newDocumentIds = [...documentIds];
deletedDocumentIds.forEach((deletedDocumentId) => { deletedIds.forEach((deletedId) => {
if (deletedDocumentId !== undefined) { if (deletedId !== undefined) {
// documentIds.remove((documentId: DocumentId) => documentId.rid === selectedDocumentId.rid); // documentIds.remove((documentId: DocumentId) => documentId.rid === selectedDocumentId.rid);
const index = toDeleteDocumentIds.findIndex((documentId) => documentId.rid === deletedDocumentId.rid); const index = toDeleteDocumentIds.findIndex((documentId) => documentId.rid === deletedId);
if (index !== -1) { if (index !== -1) {
newDocumentIds.splice(index, 1); newDocumentIds.splice(index, 1);
} }
@ -1335,9 +1338,60 @@ const DocumentsTabComponent: React.FunctionComponent<{
return partitionKeyProperty; return partitionKeyProperty;
}); });
__deleteDocument = (documentId: DocumentId): Promise<void> => /**
* Mongo implementation
* TODO: update proxy to use mongo driver deleteMany
*/
_deleteDocuments = (toDeleteDocumentIds: DocumentId[]): Promise<string[]> => {
const promises = toDeleteDocumentIds.map((documentId) => _deleteDocument(documentId));
return Promise.all(promises);
};
const __deleteDocument = (documentId: DocumentId): Promise<void> =>
MongoProxyClient.deleteDocument(_collection.databaseId, _collection as ViewModels.Collection, documentId); MongoProxyClient.deleteDocument(_collection.databaseId, _collection as ViewModels.Collection, documentId);
const _deleteDocument = useCallback(
(documentId: DocumentId): Promise<string> => {
onExecutionErrorChange(false);
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocument, {
dataExplorerArea: Constants.Areas.Tab,
tabTitle,
});
setIsExecuting(true);
return __deleteDocument(documentId)
.then(
() => {
TelemetryProcessor.traceSuccess(
Action.DeleteDocument,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle,
},
startKey,
);
return documentId.rid;
},
(error) => {
onExecutionErrorChange(true);
console.error(error);
TelemetryProcessor.traceFailure(
Action.DeleteDocument,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey,
);
return undefined;
},
)
.finally(() => setIsExecuting(false));
},
[__deleteDocument, onExecutionErrorChange, tabTitle],
);
onSaveNewDocumentClick = useCallback((): Promise<unknown> => { onSaveNewDocumentClick = useCallback((): Promise<unknown> => {
const documentContent = JSON.parse(selectedDocumentContent); const documentContent = JSON.parse(selectedDocumentContent);
// this.displayedError(""); // this.displayedError("");
@ -1615,12 +1669,12 @@ const DocumentsTabComponent: React.FunctionComponent<{
<div <div
className="tab-pane active" className="tab-pane active"
/* data-bind=" /* data-bind="
setTemplateReady: true, setTemplateReady: true,
attr:{ attr:{
id: tabId id: tabId
}, },
visible: isActive" visible: isActive"
*/ */
role="tabpanel" role="tabpanel"
style={{ display: "flex" }} style={{ display: "flex" }}
> >
@ -1709,9 +1763,9 @@ const DocumentsTabComponent: React.FunctionComponent<{
onClick={() => refreshDocumentsGrid(true)} onClick={() => refreshDocumentsGrid(true)}
disabled={!applyFilterButton.enabled} disabled={!applyFilterButton.enabled}
/* data-bind=" /* data-bind="
click: refreshDocumentsGrid.bind($data, true), click: refreshDocumentsGrid.bind($data, true),
enable: applyFilterButton.enabled" enable: applyFilterButton.enabled"
*/ */
aria-label="Apply filter" aria-label="Apply filter"
tabIndex={0} tabIndex={0}
> >
@ -1723,9 +1777,9 @@ const DocumentsTabComponent: React.FunctionComponent<{
<button <button
className="filterbtnstyle queryButton" className="filterbtnstyle queryButton"
/* data-bind=" /* data-bind="
visible: !isPreferredApiMongoDB && isExecuting, visible: !isPreferredApiMongoDB && isExecuting,
click: onAbortQueryClick" click: onAbortQueryClick"
*/ */
aria-label="Cancel Query" aria-label="Cancel Query"
onClick={() => queryAbortController.abort()} onClick={() => queryAbortController.abort()}
tabIndex={0} tabIndex={0}

View File

@ -138,6 +138,7 @@ export enum Action {
QueryGenerationFromCopilotPrompt, QueryGenerationFromCopilotPrompt,
QueryEdited, QueryEdited,
ExecuteQueryGeneratedFromQueryCopilot, ExecuteQueryGeneratedFromQueryCopilot,
DeleteDocuments,
} }
export const ActionModifiers = { export const ActionModifiers = {