Copy-and-Paste 2: Basic copy and paste task#147
Conversation
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…-documentdb into dev/xingfan/111-copy-and-paste-2-implement-basic-copy-and-paste-task
…into dev/xingfan/111-copy-and-paste-2-implement-basic-copy-and-paste-task
|
@xingfan-git look at the new additions to our base feature-branch, the Task API is now simpler to use, and, progress monitoring is in place ( #114 ) . I didn't have enough time to implement the fancy one - it's in the queue for later ( #182 ). There is a test command in there in the feature branch, just open the command palette and search for demo. You'll see the Recording.2025-06-18.192942.mp4 |
There was a problem hiding this comment.
Pull Request Overview
This PR implements a basic copy-paste functionality for MongoDB collections, allowing users to copy documents from one collection to another. The implementation follows a database-agnostic design pattern using interfaces for document reading and writing operations.
Key changes:
- Adds copy-paste utility interfaces and types for generic document operations
- Implements a task-based copy-paste collection feature with memory-efficient streaming
- Provides MongoDB-specific implementations for document reading and writing operations
Reviewed Changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/copyPasteUtils.ts | Defines interfaces and types for copy-paste operations including conflict resolution strategies |
| src/services/tasks/CopyPasteCollectionTask.ts | Implements the main task logic for copying collections with buffer-based streaming |
| src/documentdb/DocumentProvider.ts | MongoDB-specific implementations of DocumentReader and DocumentWriter interfaces |
| src/documentdb/ClustersClient.ts | Adds transaction support and helper methods for MongoDB operations |
| src/commands/copyCollection/copyCollection.ts | Simple command to mark a collection for copying |
| src/commands/pasteCollection/pasteCollection.ts | Command to execute the copy-paste operation with user confirmation |
| src/commands/importDocuments/importDocuments.ts | Updates to use new MongoDB error handling patterns |
| src/extensionVariables.ts | Adds temporary storage for copied collection reference |
| package.json | Registers new copy and paste collection commands |
| l10n/bundle.l10n.json | Adds localization strings for the new functionality |
|
|
||
| // TODO: TN improve this: This is a temporary solution to get going. | ||
| export let copiedCollectionNode: CollectionItem | undefined; | ||
|
|
There was a problem hiding this comment.
This TODO comment indicates technical debt. Consider implementing a more robust clipboard-like service instead of storing state in global extension variables, which could lead to issues with concurrent operations or extension state management.
| /** | |
| * Clipboard-like service for managing copied collection nodes. | |
| */ | |
| export class CollectionClipboardService { | |
| private static _copiedCollectionNode: CollectionItem | undefined; | |
| public static setCopiedCollectionNode(node: CollectionItem | undefined): void { | |
| CollectionClipboardService._copiedCollectionNode = node; | |
| } | |
| public static getCopiedCollectionNode(): CollectionItem | undefined { | |
| return CollectionClipboardService._copiedCollectionNode; | |
| } | |
| public static clearCopiedCollectionNode(): void { | |
| CollectionClipboardService._copiedCollectionNode = undefined; | |
| } | |
| } |
|
|
||
| // Check type of sourceNode or targetNodeAdd commentMore actions | ||
| // Currently we only support CollectionItem types | ||
| // Later we need to check if they are supported types that with document reader and writer implementations |
There was a problem hiding this comment.
The comment contains a formatting error: 'targetNodeAdd commentMore actions' should be properly formatted and clarified.
| // Later we need to check if they are supported types that with document reader and writer implementations | |
| // Check type of sourceNode and targetNode | |
| // Currently we only support CollectionItem types | |
| // Later we need to check if they are supported types that have document reader and writer implementations |
| ); | ||
|
|
||
| // void vscode.window.showInformationMessage(`${sourceInfo}\n${targetInfo}`); | ||
| // Confirm the copy operation with the userAdd commentMore actions |
There was a problem hiding this comment.
The comment contains a formatting error: 'userAdd commentMore actions' should be properly formatted and clarified.
| // Confirm the copy operation with the userAdd commentMore actions | |
| // Confirm the copy operation with the user |
| // config.source.databaseName, | ||
| // config.source.collectionName, | ||
| // ); | ||
|
|
There was a problem hiding this comment.
This commented-out code block should be removed if it's not needed, as it adds unnecessary clutter to the codebase.
| if (result.errors !== null) { | ||
| // For basic implementation with abort strategy, any error should fail the task | ||
| if (this.config.onConflict === ConflictResolutionStrategy.Abort) { | ||
| const firstError = result.errors[0] as { error: Error }; |
There was a problem hiding this comment.
Using type assertion with 'as' violates the TypeScript strict mode guidelines. The code should use proper type guards or handle the possibility that errors[0] might not exist or have the expected structure.
| const session = client.startTransaction(); | ||
| const collection = client.getCollection(databaseName, collectionName); | ||
| try { | ||
| // eslint-disable-next-line @typescript-eslint/no-unsafe-return |
There was a problem hiding this comment.
The eslint-disable comment indicates unsafe type handling. The getOperation() method should be properly typed instead of disabling type safety warnings.
…aste-2-implement-basic-copy-and-paste-task
tnaum-ms
left a comment
There was a problem hiding this comment.
@xingfan-git I added my review comments and now I'll move forward with changes, as discussed.
This means that my comments might be out of date, but please read these anyway to receive feedback.
We'll discuss next step in a dedicated PR.
| collectionName: string, | ||
| documents: Document[], | ||
| ): Promise<InsertDocumentsResult> { | ||
| ordered: boolean = true, |
There was a problem hiding this comment.
we should improve documentation here
| ): AsyncIterable<DocumentDetails> { | ||
| const client = await ClustersClient.getClient(connectionId); | ||
|
|
||
| const documentStream = client.streamDocuments(databaseName, collectionName, new AbortController().signal); |
There was a problem hiding this comment.
let's make sure we can abort this.
| try { | ||
| const insertResult = await client.insertDocuments( | ||
| databaseName, | ||
| collectionName, | ||
| documentsToProcess as Document[], | ||
| false, | ||
| ); | ||
| return { | ||
| count: insertResult.insertedCount, | ||
| errorOccurred: false, | ||
| }; | ||
| } catch (error) { | ||
| if (isMongoBulkWriteError(error)) { | ||
| // Handle MongoDB bulk write errors | ||
| // It could be a partial failure, so we need to check the result | ||
| return { | ||
| count: error.result.insertedCount, | ||
| errorOccurred: true, | ||
| }; | ||
| } else { | ||
| // Handle other errors | ||
| return { | ||
| count: 0, | ||
| errorOccurred: true, | ||
| }; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
@xingfan-git thanks for this update. Let's keep features/fixes separate to simplify feature delivery. This could be in a separate issue/branch/PR
|
|
||
| // Wait for the task to complete | ||
| while (task.getStatus().state === TaskState.Running || task.getStatus().state === TaskState.Initializing) { | ||
| // Simple polling with a small delay | ||
| await new Promise((resolve) => setTimeout(resolve, 100)); | ||
| } | ||
|
|
||
| // Check final status and show result | ||
| const finalStatus = task.getStatus(); | ||
| if (finalStatus.state === TaskState.Completed) { | ||
| void vscode.window.showInformationMessage( | ||
| l10n.t('Collection copied successfully: {0}', finalStatus.message || ''), | ||
| ); | ||
| } else if (finalStatus.state === TaskState.Failed) { | ||
| const errorToThrow = | ||
| finalStatus.error instanceof Error ? finalStatus.error : new Error('Copy operation failed'); | ||
| throw errorToThrow; | ||
| } else if (finalStatus.state === TaskState.Stopped) { | ||
| void vscode.window.showInformationMessage(l10n.t('Copy operation was cancelled.')); | ||
| } | ||
| } catch (error) { | ||
| const errorMessage = error instanceof Error ? error.message : String(error); | ||
| void vscode.window.showErrorMessage(l10n.t('Failed to copy collection: {0}', errorMessage)); | ||
| throw error; | ||
| } finally { | ||
| // Clean up - remove the task from the service after completion | ||
| try { | ||
| const task = TaskService.listTasks().find((t) => t.type === 'copy-paste-collection'); | ||
| if (task) { | ||
| await TaskService.deleteTask(task.id); | ||
| } | ||
| } catch (cleanupError) { | ||
| // Log cleanup error but don't throw | ||
| console.warn('Failed to clean up copy-paste task:', cleanupError); | ||
| } | ||
| } |
There was a problem hiding this comment.
@xingfan-git we should not wait for the task to complete here - it's a fire and forget scenario. If the task needs work to be done once it finishes, it needs to be done in the task itself. if we need extra "post task" UI step etc., and you think that the architecture does not support it, then it's something we should consier in the architecture and extend the Task API.
I'll remove/comment it out for discussion.
| return documents; | ||
| } | ||
|
|
||
| async countDocuments(databaseName: string, collectionName: string, findQuery: string = '{}'): Promise<number> { |
There was a problem hiding this comment.
TODO: create an issue to add support for "abort" support to runQuery and for countDocuments
| if (signal.aborted) { | ||
| // Cleanup any remaining buffer | ||
| if (buffer.length > 0) { | ||
| await this.flushBuffer(buffer); | ||
| } | ||
| return; |
There was a problem hiding this comment.
hm, let's just abort without writing more into the target collection.
| export interface DocumentDetails { | ||
| /** | ||
| * The document's unique identifier (e.g., _id in MongoDB) | ||
| */ | ||
| id: unknown; | ||
|
|
||
| /** | ||
| * The document content treated as opaque data by the core task logic. | ||
| * Specific readers/writers will know how to interpret/serialize this. | ||
| * For MongoDB, this would typically be a BSON document. | ||
| */ | ||
| documentContent: unknown; | ||
| } |
There was a problem hiding this comment.
@xingfan-git let's discuss adding a size estimate here so maybe we can get this from the document reader and keep it so that it's always available.
tnaum-ms
left a comment
There was a problem hiding this comment.
submitting comments
| const count = await collection.countDocuments(findQueryObj, { | ||
| // Use a read preference of 'primary' to ensure we get the most up-to-date | ||
| // count, especially important for sharded clusters. | ||
| readPreference: 'primary', |
Closes #111