ad7b21f3a2
fix cascading delete issue * test passing
119 lines
3.9 KiB
Go
119 lines
3.9 KiB
Go
package document
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"queryorchestration/internal/database/repository"
|
|
"queryorchestration/internal/harddelete"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// HardDelete permanently removes a document and all its dependent data from the database.
|
|
// The delete cascade executes in FK order within a single transaction:
|
|
// - field extraction array fields, versions, and extractions
|
|
// - clean entries and cleans
|
|
// - document entries (S3 references)
|
|
// - the document row itself (documentLabels auto-cascade via ON DELETE CASCADE)
|
|
//
|
|
// batch_document_outcomes.document_id is SET NULL by the database FK constraint,
|
|
// preserving batch outcome history when a document is removed.
|
|
//
|
|
// S3 source files are intentionally NOT deleted. Their paths are collected before
|
|
// the transaction, logged at INFO level, and returned for optional verbose responses.
|
|
//
|
|
// Parameters:
|
|
// - ctx: request context
|
|
// - documentID: UUID of the document to delete
|
|
//
|
|
// Returns:
|
|
// - []harddelete.S3PathInfo: S3 paths of orphaned source documents
|
|
// - error: NotFoundError if the document does not exist, or a database error
|
|
func (s *Service) HardDelete(ctx context.Context, documentID uuid.UUID) ([]harddelete.S3PathInfo, error) {
|
|
queries := s.cfg.GetDBQueries()
|
|
|
|
// Collect S3 paths before starting the transaction so we can log them
|
|
// even if the transaction fails for some reason.
|
|
s3Rows, err := queries.CollectDocumentS3Paths(ctx, documentID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to collect S3 paths for document %s: %w", documentID, err)
|
|
}
|
|
|
|
s3Paths := make([]harddelete.S3PathInfo, len(s3Rows))
|
|
for i, row := range s3Rows {
|
|
s3Paths[i] = harddelete.S3PathInfo{
|
|
DocumentID: documentID,
|
|
Bucket: row.Bucket,
|
|
Key: row.Key,
|
|
}
|
|
}
|
|
|
|
err = s.cfg.ExecuteDBTransaction(ctx, func(ctx context.Context, q *repository.Queries) error {
|
|
return DeleteDocumentCascade(ctx, q, documentID)
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to hard-delete document %s: %w", documentID, err)
|
|
}
|
|
|
|
harddelete.LogOrphanedS3Paths(s3Paths)
|
|
|
|
return s3Paths, nil
|
|
}
|
|
|
|
// DeleteDocumentCascade deletes a document and all dependent rows in FK order
|
|
// using the provided transactional queries. This function does NOT collect S3 paths
|
|
// or log them -- the caller is responsible for that.
|
|
// Exported so that folder and client delete services can reuse the cascade logic.
|
|
//
|
|
// Parameters:
|
|
// - ctx: request context
|
|
// - q: transactional repository queries
|
|
// - documentID: UUID of the document to delete
|
|
//
|
|
// Returns:
|
|
// - error: NotFoundError if the document does not exist, or a database error
|
|
func DeleteDocumentCascade(ctx context.Context, q *repository.Queries, documentID uuid.UUID) error {
|
|
// Delete in FK dependency order (children before parents)
|
|
|
|
if err := q.DeleteDocumentFieldExtractionArrayFields(ctx, documentID); err != nil {
|
|
return fmt.Errorf("delete field extraction array fields: %w", err)
|
|
}
|
|
|
|
if err := q.DeleteDocumentFieldExtractionVersions(ctx, documentID); err != nil {
|
|
return fmt.Errorf("delete field extraction versions: %w", err)
|
|
}
|
|
|
|
if err := q.DeleteDocumentFieldExtractions(ctx, documentID); err != nil {
|
|
return fmt.Errorf("delete field extractions: %w", err)
|
|
}
|
|
|
|
if err := q.DeleteDocumentCleanEntries(ctx, documentID); err != nil {
|
|
return fmt.Errorf("delete clean entries: %w", err)
|
|
}
|
|
|
|
if err := q.DeleteDocumentCleans(ctx, documentID); err != nil {
|
|
return fmt.Errorf("delete cleans: %w", err)
|
|
}
|
|
|
|
if err := q.DeleteDocumentEntries(ctx, documentID); err != nil {
|
|
return fmt.Errorf("delete document entries: %w", err)
|
|
}
|
|
|
|
// documentLabels auto-cascade via ON DELETE CASCADE, but the document
|
|
// row itself must be explicitly deleted.
|
|
rowsAffected, err := q.HardDeleteDocument(ctx, documentID)
|
|
if err != nil {
|
|
return fmt.Errorf("delete document: %w", err)
|
|
}
|
|
|
|
if rowsAffected == 0 {
|
|
return &harddelete.NotFoundError{
|
|
ResourceType: "document",
|
|
ResourceID: documentID.String(),
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|