package sqlite import ( "context" "database/sql" "strings" "github.com/pkg/errors" storepb "github.com/usememos/memos/proto/gen/store" "github.com/usememos/memos/store" ) const deleteUserBatchSize = 500 type deleteUserMemoRef struct { ID int32 UID string } type deleteUserTargetSet struct { memos []deleteUserMemoRef attachments []*store.Attachment attachmentIDs []int32 userSettingKeys []storepb.UserSetting_Key inboxIDs []int32 } func (d *DB) DeleteUser(ctx context.Context, delete *store.DeleteUser) (*store.DeleteUserResult, error) { tx, err := d.db.BeginTx(ctx, nil) if err != nil { return nil, errors.Wrap(err, "failed to begin delete user transaction") } defer func() { _ = tx.Rollback() }() targets, err := collectDeleteUserTargets(ctx, tx, delete.ID) if err != nil { return nil, errors.Wrap(err, "failed to collect delete user targets") } if err := deleteUserTargetsTx(ctx, tx, delete.ID, targets); err != nil { return nil, errors.Wrap(err, "failed to delete user targets") } if store.GetDeleteUserFailpoint(ctx) == store.DeleteUserFailpointBeforeCommit { return nil, errors.New("delete user failpoint before commit") } if err := tx.Commit(); err != nil { return nil, errors.Wrap(err, "failed to commit delete user transaction") } return &store.DeleteUserResult{ Attachments: targets.attachments, UserSettingKeys: targets.userSettingKeys, }, nil } func collectDeleteUserTargets(ctx context.Context, tx *sql.Tx, userID int32) (*deleteUserTargetSet, error) { targets := &deleteUserTargetSet{} memos, err := listDeleteUserMemoTree(ctx, tx, userID) if err != nil { return nil, err } targets.memos = memos attachments, err := listDeleteUserAttachments(ctx, tx, userID, memoIDsFromRefs(memos)) if err != nil { return nil, err } targets.attachments = attachments targets.attachmentIDs = attachmentIDsFromList(attachments) userSettingKeys, err := listDeleteUserSettingKeys(ctx, tx, userID) if err != nil { return nil, err } targets.userSettingKeys = userSettingKeys inboxIDs, err := listDeleteUserInboxIDs(ctx, tx, userID, memoIDSetFromRefs(memos)) if err != nil { return nil, err } targets.inboxIDs = inboxIDs return targets, nil } func deleteUserTargetsTx(ctx context.Context, tx *sql.Tx, userID int32, targets *deleteUserTargetSet) error { memoIDs := memoIDsFromRefs(targets.memos) contentIDs := memoContentIDsFromRefs(targets.memos) if err := deleteReactionsByContentIDsTx(ctx, tx, contentIDs); err != nil { return err } if err := deleteAttachmentsByIDsTx(ctx, tx, targets.attachmentIDs); err != nil { return err } if err := deleteReactionsByCreatorTx(ctx, tx, userID); err != nil { return err } if err := deleteMemoSharesTx(ctx, tx, userID, memoIDs); err != nil { return err } if err := deleteInboxesByIDsTx(ctx, tx, targets.inboxIDs); err != nil { return err } if err := deleteUserIdentitiesTx(ctx, tx, userID); err != nil { return err } if err := deleteUserSettingsTx(ctx, tx, userID); err != nil { return err } if err := deleteMemoRelationsTx(ctx, tx, memoIDs); err != nil { return err } if err := deleteMemosTx(ctx, tx, memoIDs); err != nil { return err } if err := deleteUserRowTx(ctx, tx, userID); err != nil { return err } return nil } func listDeleteUserMemoTree(ctx context.Context, tx *sql.Tx, userID int32) ([]deleteUserMemoRef, error) { return listDeleteUserMemoTreeRecursive(ctx, tx, userID) } func listDeleteUserMemoTreeRecursive(ctx context.Context, tx *sql.Tx, userID int32) ([]deleteUserMemoRef, error) { rows, err := tx.QueryContext(ctx, ` WITH RECURSIVE memo_tree(id, uid) AS ( SELECT id, uid FROM memo WHERE creator_id = `+deleteUserPlaceholder(1)+` UNION SELECT child.id, child.uid FROM memo child JOIN memo_relation rel ON rel.memo_id = child.id AND rel.type = 'COMMENT' JOIN memo_tree parent ON rel.related_memo_id = parent.id ) SELECT id, uid FROM memo_tree `, userID) if err != nil { return nil, err } defer rows.Close() memos := make([]deleteUserMemoRef, 0) for rows.Next() { var memo deleteUserMemoRef if err := rows.Scan(&memo.ID, &memo.UID); err != nil { return nil, err } memos = append(memos, memo) } if err := rows.Err(); err != nil { return nil, err } return memos, nil } func listDeleteUserAttachments(ctx context.Context, tx *sql.Tx, userID int32, memoIDs []int32) ([]*store.Attachment, error) { attachments := make([]*store.Attachment, 0) seen := make(map[int32]struct{}) if err := appendDeleteUserAttachments(ctx, tx, ` SELECT id, uid, creator_id, memo_id, storage_type, reference, payload FROM attachment WHERE creator_id = `+deleteUserPlaceholder(1), []any{userID}, seen, &attachments); err != nil { return nil, err } for _, batch := range deleteUserBatches(memoIDs, deleteUserBatchSize) { clause, args := deleteUserInClause(1, batch) if err := appendDeleteUserAttachments(ctx, tx, ` SELECT id, uid, creator_id, memo_id, storage_type, reference, payload FROM attachment WHERE memo_id IN `+clause, args, seen, &attachments); err != nil { return nil, err } } return attachments, nil } func appendDeleteUserAttachments(ctx context.Context, tx *sql.Tx, query string, args []any, seen map[int32]struct{}, attachments *[]*store.Attachment) error { rows, err := tx.QueryContext(ctx, query, args...) if err != nil { return err } defer rows.Close() for rows.Next() { attachment := &store.Attachment{} var memoID sql.NullInt32 var storageType string var payloadBytes []byte if err := rows.Scan(&attachment.ID, &attachment.UID, &attachment.CreatorID, &memoID, &storageType, &attachment.Reference, &payloadBytes); err != nil { return err } if _, exists := seen[attachment.ID]; exists { continue } seen[attachment.ID] = struct{}{} if memoID.Valid { attachment.MemoID = &memoID.Int32 } attachment.StorageType = storepb.AttachmentStorageType(storepb.AttachmentStorageType_value[storageType]) payload := &storepb.AttachmentPayload{} if len(payloadBytes) > 0 { if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil { return err } } attachment.Payload = payload *attachments = append(*attachments, attachment) } return rows.Err() } func listDeleteUserSettingKeys(ctx context.Context, tx *sql.Tx, userID int32) ([]storepb.UserSetting_Key, error) { rows, err := tx.QueryContext(ctx, deleteUserSettingKeysQuery(), userID) if err != nil { return nil, err } defer rows.Close() keys := make([]storepb.UserSetting_Key, 0) for rows.Next() { var keyString string if err := rows.Scan(&keyString); err != nil { return nil, err } key := storepb.UserSetting_Key(storepb.UserSetting_Key_value[keyString]) keys = append(keys, key) } if err := rows.Err(); err != nil { return nil, err } return keys, nil } func deleteUserSettingKeysQuery() string { return `SELECT key FROM user_setting WHERE user_id = ` + deleteUserPlaceholder(1) } func listDeleteUserInboxIDs(ctx context.Context, tx *sql.Tx, userID int32, memoIDSet map[int32]struct{}) ([]int32, error) { directIDs, err := listDeleteUserDirectInboxIDs(ctx, tx, userID) if err != nil { return nil, err } inboxIDs := append([]int32{}, directIDs...) if len(memoIDSet) == 0 { return inboxIDs, nil } memoIDs, err := listDeleteUserMemoReferencedInboxIDs(ctx, tx, userID, memoIDSet) if err != nil { return nil, err } return append(inboxIDs, memoIDs...), nil } func listDeleteUserDirectInboxIDs(ctx context.Context, tx *sql.Tx, userID int32) ([]int32, error) { rows, err := tx.QueryContext(ctx, ` SELECT id FROM inbox WHERE sender_id = `+deleteUserPlaceholder(1)+` OR receiver_id = `+deleteUserPlaceholder(2), userID, userID) if err != nil { return nil, err } defer rows.Close() inboxIDs := make([]int32, 0) for rows.Next() { var inboxID int32 if err := rows.Scan(&inboxID); err != nil { return nil, err } inboxIDs = append(inboxIDs, inboxID) } if err := rows.Err(); err != nil { return nil, err } return inboxIDs, nil } func listDeleteUserMemoReferencedInboxIDs(ctx context.Context, tx *sql.Tx, userID int32, memoIDSet map[int32]struct{}) ([]int32, error) { rows, err := tx.QueryContext(ctx, ` SELECT id, message FROM inbox WHERE sender_id <> `+deleteUserPlaceholder(1)+` AND receiver_id <> `+deleteUserPlaceholder(2), userID, userID) if err != nil { return nil, err } defer rows.Close() inboxIDs := make([]int32, 0) for rows.Next() { var ( inboxID int32 messageRaw []byte ) if err := rows.Scan(&inboxID, &messageRaw); err != nil { return nil, err } if len(messageRaw) == 0 { continue } message := &storepb.InboxMessage{} if err := protojsonUnmarshaler.Unmarshal(messageRaw, message); err != nil { return nil, err } if inboxMessageTouchesMemoSet(message, memoIDSet) { inboxIDs = append(inboxIDs, inboxID) } } if err := rows.Err(); err != nil { return nil, err } return inboxIDs, nil } func inboxMessageTouchesMemoSet(message *storepb.InboxMessage, memoIDSet map[int32]struct{}) bool { if message == nil { return false } switch message.Type { case storepb.InboxMessage_MEMO_COMMENT: payload := message.GetMemoComment() if payload == nil { return false } return memoIDInSet(payload.MemoId, memoIDSet) || memoIDInSet(payload.RelatedMemoId, memoIDSet) case storepb.InboxMessage_MEMO_MENTION: payload := message.GetMemoMention() if payload == nil { return false } return memoIDInSet(payload.MemoId, memoIDSet) || memoIDInSet(payload.RelatedMemoId, memoIDSet) default: return false } } func memoIDInSet(id int32, memoIDSet map[int32]struct{}) bool { if id == 0 { return false } _, exists := memoIDSet[id] return exists } func deleteReactionsByContentIDsTx(ctx context.Context, tx *sql.Tx, contentIDs []string) error { for _, batch := range deleteUserBatches(contentIDs, deleteUserBatchSize) { clause, args := deleteUserInClause(1, batch) if _, err := tx.ExecContext(ctx, `DELETE FROM reaction WHERE content_id IN `+clause, args...); err != nil { return err } } return nil } func deleteAttachmentsByIDsTx(ctx context.Context, tx *sql.Tx, attachmentIDs []int32) error { for _, batch := range deleteUserBatches(attachmentIDs, deleteUserBatchSize) { clause, args := deleteUserInClause(1, batch) if _, err := tx.ExecContext(ctx, `DELETE FROM attachment WHERE id IN `+clause, args...); err != nil { return err } } return nil } func deleteReactionsByCreatorTx(ctx context.Context, tx *sql.Tx, userID int32) error { _, err := tx.ExecContext(ctx, `DELETE FROM reaction WHERE creator_id = `+deleteUserPlaceholder(1), userID) return err } func deleteMemoSharesTx(ctx context.Context, tx *sql.Tx, userID int32, memoIDs []int32) error { if _, err := tx.ExecContext(ctx, `DELETE FROM memo_share WHERE creator_id = `+deleteUserPlaceholder(1), userID); err != nil { return err } for _, batch := range deleteUserBatches(memoIDs, deleteUserBatchSize) { clause, args := deleteUserInClause(1, batch) if _, err := tx.ExecContext(ctx, `DELETE FROM memo_share WHERE memo_id IN `+clause, args...); err != nil { return err } } return nil } func deleteInboxesByIDsTx(ctx context.Context, tx *sql.Tx, inboxIDs []int32) error { for _, batch := range deleteUserBatches(inboxIDs, deleteUserBatchSize) { clause, args := deleteUserInClause(1, batch) if _, err := tx.ExecContext(ctx, `DELETE FROM inbox WHERE id IN `+clause, args...); err != nil { return err } } return nil } func deleteUserIdentitiesTx(ctx context.Context, tx *sql.Tx, userID int32) error { _, err := tx.ExecContext(ctx, `DELETE FROM user_identity WHERE user_id = `+deleteUserPlaceholder(1), userID) return err } func deleteUserSettingsTx(ctx context.Context, tx *sql.Tx, userID int32) error { _, err := tx.ExecContext(ctx, `DELETE FROM user_setting WHERE user_id = `+deleteUserPlaceholder(1), userID) return err } func deleteMemoRelationsTx(ctx context.Context, tx *sql.Tx, memoIDs []int32) error { for _, batch := range deleteUserBatches(memoIDs, deleteUserBatchSize) { memoClause, args := deleteUserInClause(1, batch) relatedClause, relatedArgs := deleteUserInClause(len(args)+1, batch) query := `DELETE FROM memo_relation WHERE memo_id IN ` + memoClause + ` OR related_memo_id IN ` + relatedClause args = append(args, relatedArgs...) if _, err := tx.ExecContext(ctx, query, args...); err != nil { return err } } return nil } func deleteMemosTx(ctx context.Context, tx *sql.Tx, memoIDs []int32) error { for _, batch := range deleteUserBatches(memoIDs, deleteUserBatchSize) { clause, args := deleteUserInClause(1, batch) if _, err := tx.ExecContext(ctx, `DELETE FROM memo WHERE id IN `+clause, args...); err != nil { return err } } return nil } func deleteUserRowTx(ctx context.Context, tx *sql.Tx, userID int32) error { _, err := tx.ExecContext(ctx, `DELETE FROM user WHERE id = `+deleteUserPlaceholder(1), userID) return err } func deleteUserPlaceholder(_ int) string { return "?" } func deleteUserInClause[T any](start int, values []T) (string, []any) { placeholders := make([]string, 0, len(values)) args := make([]any, 0, len(values)) for i, value := range values { placeholders = append(placeholders, deleteUserPlaceholder(start+i)) args = append(args, value) } return "(" + strings.Join(placeholders, ", ") + ")", args } func deleteUserBatches[T any](values []T, size int) [][]T { if len(values) == 0 { return nil } if size <= 0 { size = len(values) } batches := make([][]T, 0, (len(values)+size-1)/size) for start := 0; start < len(values); start += size { end := start + size if end > len(values) { end = len(values) } batches = append(batches, values[start:end]) } return batches } func memoIDsFromRefs(memos []deleteUserMemoRef) []int32 { ids := make([]int32, 0, len(memos)) for _, memo := range memos { ids = append(ids, memo.ID) } return ids } func memoIDSetFromRefs(memos []deleteUserMemoRef) map[int32]struct{} { idSet := make(map[int32]struct{}, len(memos)) for _, memo := range memos { idSet[memo.ID] = struct{}{} } return idSet } func memoContentIDsFromRefs(memos []deleteUserMemoRef) []string { contentIDs := make([]string, 0, len(memos)) for _, memo := range memos { contentIDs = append(contentIDs, "memos/"+memo.UID) } return contentIDs } func attachmentIDsFromList(attachments []*store.Attachment) []int32 { ids := make([]int32, 0, len(attachments)) for _, attachment := range attachments { if attachment == nil { continue } ids = append(ids, attachment.ID) } return ids }