Files
coder/coderd/database/queries/chatfiles.sql
T
Michael Suchacz 57f38b5c24 fix: keep chat attachments while a linking chat exists
Fixes https://linear.app/codercom/issue/CODAGT-616/keep-chat-attachments-while-chats-remain-unarchived

Chat attachments could disappear even though the chat was still available. This happened when a message was saved without recording which attachments it used, or when cleanup deleted attachments before an archived chat itself was removed.

Creating a chat, sending or queuing a message, and editing a message now record both the message and which attachments it uses as one operation. If the chat is already at the 50-attachment limit, the chat change fails without being partially saved.

Concurrent attachment writes serialize the 50-file cap per chat. Cleanup locks candidates and checks again for new links before deleting. If a file becomes unavailable after input validation, create, send, and edit return a clear client error and roll back the chat change.

An attachment stays available while any chat that uses it still exists. After an archived chat reaches the end of its retention period and is deleted, an old attachment that no remaining chat uses can be cleaned up. The retention guide and unavailable-attachment UI text document this lifecycle. This change cannot restore attachments that were already deleted.

The database migration adds two indexes so attachment cleanup stays fast as attachments accumulate.

> This PR was authored by Mux (AI) on Mike's behalf.
2026-08-11 13:53:15 +02:00

49 lines
1.8 KiB
SQL

-- name: InsertChatFile :one
INSERT INTO chat_files (owner_id, organization_id, name, mimetype, data)
VALUES (@owner_id::uuid, @organization_id::uuid, @name::text, @mimetype::text, @data::bytea)
RETURNING id, owner_id, organization_id, created_at, name, mimetype;
-- name: GetChatFileByID :one
SELECT * FROM chat_files WHERE id = @id::uuid;
-- name: GetChatFilesByIDs :many
SELECT * FROM chat_files WHERE id = ANY(@ids::uuid[]);
-- name: GetChatFileDataPrefixesByIDs :many
-- GetChatFileDataPrefixesByIDs returns a bounded prefix of each
-- file's content, keeping full blobs out of server memory. Owner and
-- organization columns support row-level authorization.
SELECT id, owner_id, organization_id, substr(data, 1, @prefix_bytes::int) AS data_prefix
FROM chat_files
WHERE id = ANY(@ids::uuid[]);
-- name: GetChatFileMetadataByChatID :many
-- GetChatFileMetadataByChatID returns lightweight file metadata for
-- all files linked to a chat. The data column is excluded to avoid
-- loading file content.
SELECT cf.id, cf.owner_id, cf.organization_id, cf.name, cf.mimetype, cf.created_at
FROM chat_files cf
JOIN chat_file_links cfl ON cfl.file_id = cf.id
WHERE cfl.chat_id = @chat_id::uuid
ORDER BY cf.created_at ASC;
-- name: GetOldUnlinkedChatFileIDs :many
-- Locks candidate rows against foreign-key inserts for the transaction.
SELECT cf.id
FROM chat_files cf
WHERE cf.created_at < @before_time::timestamptz
AND NOT EXISTS (
SELECT 1 FROM chat_file_links cfl WHERE cfl.file_id = cf.id
)
ORDER BY cf.created_at ASC
LIMIT @limit_count
FOR UPDATE OF cf SKIP LOCKED;
-- name: DeleteUnlinkedChatFilesByIDs :execrows
DELETE FROM chat_files cf
WHERE cf.id = ANY(@ids::uuid[])
AND cf.created_at < @before_time::timestamptz
AND NOT EXISTS (
SELECT 1 FROM chat_file_links cfl WHERE cfl.file_id = cf.id
);