feat: add deleted flag to chat messages for soft-delete (#23223)

Adds a `deleted` boolean column to the `chat_messages` table. Messages
are never physically deleted from the database — instead they are marked
as deleted so that usage and cost data is preserved.

## Changes

### Migration
- New migration (000444) adds `deleted boolean NOT NULL DEFAULT false`
to `chat_messages`

### SQL queries
- `DeleteChatMessagesAfterID` → `SoftDeleteChatMessagesAfterID` (UPDATE
SET deleted=true instead of DELETE)
- New `SoftDeleteChatMessageByID` query for single-message soft-delete
- All read queries now filter `deleted = false`:
  - `GetChatMessageByID`
  - `GetChatMessagesByChatID`
  - `GetChatMessagesByChatIDDescPaginated`
  - `GetChatMessagesForPromptByChatID` (both CTE and main query)
  - `GetLastChatMessageByRole`
- Cost/usage queries (`GetChatCostSummary`, `GetChatCostPerModel`, etc.)
intentionally still include deleted messages to preserve accurate spend
tracking

### EditMessage behavior
- Previously: updated the message content in-place + hard-deleted
subsequent messages
- Now: soft-deletes the original message + soft-deletes subsequent
messages + inserts a new message with the updated content
- This preserves the original message data (tokens, cost, content) in
the database
This commit is contained in:
Kyle Carberry
2026-03-18 14:37:09 -04:00
committed by GitHub
parent f395e2e9c2
commit 1f0d896fc9
14 changed files with 205 additions and 85 deletions
+19 -3
View File
@@ -5,13 +5,23 @@ WHERE id = @id OR root_chat_id = @id;
-- name: UnarchiveChatByID :exec
UPDATE chats SET archived = false, updated_at = NOW() WHERE id = @id::uuid;
-- name: DeleteChatMessagesAfterID :exec
DELETE FROM
-- name: SoftDeleteChatMessagesAfterID :exec
UPDATE
chat_messages
SET
deleted = true
WHERE
chat_id = @chat_id::uuid
AND id > @after_id::bigint;
-- name: SoftDeleteChatMessageByID :exec
UPDATE
chat_messages
SET
deleted = true
WHERE
id = @id::bigint;
-- name: GetChatByID :one
SELECT
*
@@ -26,7 +36,8 @@ SELECT
FROM
chat_messages
WHERE
id = @id::bigint;
id = @id::bigint
AND deleted = false;
-- name: GetChatMessagesByChatID :many
SELECT
@@ -37,6 +48,7 @@ WHERE
chat_id = @chat_id::uuid
AND id > @after_id::bigint
AND visibility IN ('user', 'both')
AND deleted = false
ORDER BY
created_at ASC;
@@ -52,6 +64,7 @@ WHERE
ELSE true
END
AND visibility IN ('user', 'both')
AND deleted = false
ORDER BY
id DESC
LIMIT
@@ -66,6 +79,7 @@ WITH latest_compressed_summary AS (
WHERE
chat_id = @chat_id::uuid
AND compressed = TRUE
AND deleted = false
AND visibility = 'model'
ORDER BY
created_at DESC,
@@ -80,6 +94,7 @@ FROM
WHERE
chat_id = @chat_id::uuid
AND visibility IN ('model', 'both')
AND deleted = false
AND (
(
role = 'system'
@@ -496,6 +511,7 @@ FROM
WHERE
chat_id = @chat_id::uuid
AND role = @role::chat_message_role
AND deleted = false
ORDER BY
created_at DESC, id DESC
LIMIT