diff --git a/internal/store/attachments_test.go b/internal/store/attachments_test.go index 7bbb545..3e3b929 100644 --- a/internal/store/attachments_test.go +++ b/internal/store/attachments_test.go @@ -90,6 +90,39 @@ func TestAttachmentMediaUpdatesAndFilters(t *testing.T) { require.Empty(t, rows) } +func TestAttachmentUpsertMovesDuplicateIDAndKeepsMedia(t *testing.T) { + ctx := context.Background() + s, err := Open(ctx, filepath.Join(t.TempDir(), "discrawl.db")) + require.NoError(t, err) + defer func() { _ = s.Close() }() + + require.NoError(t, seedAttachmentForGuild(ctx, s, "g1", "c1", "m1", "a1")) + require.NoError(t, s.UpdateAttachmentMedia(ctx, AttachmentMediaUpdate{ + AttachmentID: "a1", + MediaPath: "attachments/aa/hash-file.png", + ContentSHA256: "hash", + ContentSize: 4, + FetchedAt: "2026-05-15T12:05:00Z", + FetchStatus: "fetched", + })) + require.NoError(t, seedAttachmentForGuild(ctx, s, "g1", "c2", "m2", "a1")) + + rows, err := s.ListAttachments(ctx, AttachmentListOptions{MessageID: "m1"}) + require.NoError(t, err) + require.Empty(t, rows) + + rows, err = s.ListAttachments(ctx, AttachmentListOptions{MessageID: "m2"}) + require.NoError(t, err) + require.Len(t, rows, 1) + require.Equal(t, "a1", rows[0].AttachmentID) + require.Equal(t, "m2", rows[0].MessageID) + require.Equal(t, "c2", rows[0].ChannelID) + require.Equal(t, "attachments/aa/hash-file.png", rows[0].MediaPath) + require.Equal(t, "hash", rows[0].ContentSHA256) + require.Equal(t, int64(4), rows[0].ContentSize) + require.Equal(t, "fetched", rows[0].FetchStatus) +} + func seedAttachmentForGuild(ctx context.Context, s *Store, guildID, channelID, messageID, attachmentID string) error { if err := s.UpsertGuild(ctx, GuildRecord{ID: guildID, Name: guildID, RawJSON: `{}`}); err != nil { return err diff --git a/internal/store/sqlc/queries.sql b/internal/store/sqlc/queries.sql index 5e3a4d4..3f75399 100644 --- a/internal/store/sqlc/queries.sql +++ b/internal/store/sqlc/queries.sql @@ -382,7 +382,34 @@ insert into message_attachments( attachment_id, message_id, guild_id, channel_id, author_id, filename, content_type, size, url, proxy_url, text_content, media_path, content_sha256, content_size, fetched_at, fetch_status, fetch_error, updated_at -) values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); +) values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +on conflict(attachment_id) do update set + message_id = excluded.message_id, + guild_id = excluded.guild_id, + channel_id = excluded.channel_id, + author_id = excluded.author_id, + filename = excluded.filename, + content_type = excluded.content_type, + size = excluded.size, + url = excluded.url, + proxy_url = excluded.proxy_url, + text_content = excluded.text_content, + media_path = coalesce(excluded.media_path, message_attachments.media_path), + content_sha256 = coalesce(excluded.content_sha256, message_attachments.content_sha256), + content_size = case + when excluded.content_size > 0 then excluded.content_size + else message_attachments.content_size + end, + fetched_at = coalesce(excluded.fetched_at, message_attachments.fetched_at), + fetch_status = case + when excluded.fetch_status <> '' then excluded.fetch_status + else message_attachments.fetch_status + end, + fetch_error = case + when excluded.fetch_error <> '' then excluded.fetch_error + else message_attachments.fetch_error + end, + updated_at = excluded.updated_at; -- name: DeleteMentionEventsByMessage :exec delete from mention_events diff --git a/internal/store/storedb/queries.sql.go b/internal/store/storedb/queries.sql.go index adaf1bf..fd68c73 100644 --- a/internal/store/storedb/queries.sql.go +++ b/internal/store/storedb/queries.sql.go @@ -452,6 +452,33 @@ insert into message_attachments( content_type, size, url, proxy_url, text_content, media_path, content_sha256, content_size, fetched_at, fetch_status, fetch_error, updated_at ) values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +on conflict(attachment_id) do update set + message_id = excluded.message_id, + guild_id = excluded.guild_id, + channel_id = excluded.channel_id, + author_id = excluded.author_id, + filename = excluded.filename, + content_type = excluded.content_type, + size = excluded.size, + url = excluded.url, + proxy_url = excluded.proxy_url, + text_content = excluded.text_content, + media_path = coalesce(excluded.media_path, message_attachments.media_path), + content_sha256 = coalesce(excluded.content_sha256, message_attachments.content_sha256), + content_size = case + when excluded.content_size > 0 then excluded.content_size + else message_attachments.content_size + end, + fetched_at = coalesce(excluded.fetched_at, message_attachments.fetched_at), + fetch_status = case + when excluded.fetch_status <> '' then excluded.fetch_status + else message_attachments.fetch_status + end, + fetch_error = case + when excluded.fetch_error <> '' then excluded.fetch_error + else message_attachments.fetch_error + end, + updated_at = excluded.updated_at ` type InsertMessageAttachmentParams struct {