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
+30 -14
View File
@@ -399,7 +399,7 @@ type SendMessageResult struct {
Chat database.Chat
}
// EditMessageOptions controls in-place user message edits.
// EditMessageOptions controls user message edits via soft-delete and re-insert.
type EditMessageOptions struct {
ChatID uuid.UUID
CreatedBy uuid.UUID
@@ -407,7 +407,7 @@ type EditMessageOptions struct {
Content []codersdk.ChatMessagePart
}
// EditMessageResult contains the updated user message and chat status.
// EditMessageResult contains the replacement user message and chat status.
type EditMessageResult struct {
Message database.ChatMessage
Chat database.Chat
@@ -710,7 +710,8 @@ func (p *Server) checkUsageLimit(ctx context.Context, store database.Store, owne
return nil
}
// EditMessage updates a user message in-place, truncates all following messages,
// EditMessage marks the old user message as deleted, soft-deletes all
// following messages, inserts a new message with the updated content,
// clears queued messages, and moves the chat into pending status.
func (p *Server) EditMessage(
ctx context.Context,
@@ -756,28 +757,43 @@ func (p *Server) EditMessage(
return ErrEditedMessageNotUser
}
updatedMessage, err := tx.UpdateChatMessageByID(ctx, database.UpdateChatMessageByIDParams{
ModelConfigID: uuid.NullUUID{},
Content: content,
ID: opts.EditedMessageID,
})
// Soft-delete the original message instead of updating in place
// so that usage/cost data is preserved.
err = tx.SoftDeleteChatMessageByID(ctx, opts.EditedMessageID)
if err != nil {
return xerrors.Errorf("update chat message: %w", err)
return xerrors.Errorf("soft-delete edited message: %w", err)
}
err = tx.DeleteChatMessagesAfterID(ctx, database.DeleteChatMessagesAfterIDParams{
// Soft-delete all messages that came after the edited one.
err = tx.SoftDeleteChatMessagesAfterID(ctx, database.SoftDeleteChatMessagesAfterIDParams{
ChatID: opts.ChatID,
AfterID: opts.EditedMessageID,
})
if err != nil {
return xerrors.Errorf("delete later chat messages: %w", err)
return xerrors.Errorf("soft-delete later chat messages: %w", err)
}
// Insert a new message with the updated content.
msgParams := database.InsertChatMessagesParams{ //nolint:exhaustruct // Fields populated by appendChatMessage.
ChatID: opts.ChatID,
}
appendChatMessage(&msgParams, newChatMessage(
database.ChatMessageRoleUser,
content,
existing.Visibility,
existing.ModelConfigID.UUID,
chatprompt.CurrentContentVersion,
).withCreatedBy(opts.CreatedBy))
newMessages, err := insertChatMessageWithStore(ctx, tx, msgParams)
if err != nil {
return xerrors.Errorf("insert replacement message: %w", err)
}
newMessage := newMessages[0]
err = tx.DeleteAllChatQueuedMessages(ctx, opts.ChatID)
if err != nil {
return xerrors.Errorf("delete queued messages: %w", err)
}
updatedChat, err := tx.UpdateChatStatus(ctx, database.UpdateChatStatusParams{
ID: opts.ChatID,
Status: database.ChatStatusPending,
@@ -790,7 +806,7 @@ func (p *Server) EditMessage(
return xerrors.Errorf("set chat pending: %w", err)
}
result.Message = updatedMessage
result.Message = newMessage
result.Chat = updatedChat
return nil
}, nil)
@@ -2709,7 +2725,7 @@ func (p *Server) runChat(
err := p.db.InTx(func(tx database.Store) error {
// Verify this worker still owns the chat before
// inserting messages. This closes the race where
// EditMessage truncates history and clears worker_id
// EditMessage soft-deletes history and clears worker_id
// while persistInterruptedStep (which uses an
// uncancelable context) is still running.
//
+4 -2
View File
@@ -562,7 +562,9 @@ func TestEditMessageUpdatesAndTruncatesAndClearsQueue(t *testing.T) {
Content: []codersdk.ChatMessagePart{codersdk.ChatMessageText("edited")},
})
require.NoError(t, err)
require.Equal(t, editedMessageID, editResult.Message.ID)
// The edited message is soft-deleted and a new message is inserted,
// so the returned message ID will differ from the original.
require.NotEqual(t, editedMessageID, editResult.Message.ID)
require.Equal(t, database.ChatStatusPending, editResult.Chat.Status)
require.False(t, editResult.Chat.WorkerID.Valid)
@@ -576,7 +578,7 @@ func TestEditMessageUpdatesAndTruncatesAndClearsQueue(t *testing.T) {
})
require.NoError(t, err)
require.Len(t, messages, 1)
require.Equal(t, editedMessageID, messages[0].ID)
require.Equal(t, editResult.Message.ID, messages[0].ID)
onlyMessage := db2sdk.ChatMessage(messages[0])
require.Len(t, onlyMessage.Content, 1)
require.Equal(t, "edited", onlyMessage.Content[0].Text)
+6 -2
View File
@@ -2663,7 +2663,9 @@ func TestPatchChatMessage(t *testing.T) {
},
})
require.NoError(t, err)
require.Equal(t, userMessageID, edited.ID)
// The edited message is soft-deleted and a new one is inserted,
// so the returned ID will differ from the original.
require.NotEqual(t, userMessageID, edited.ID)
require.Equal(t, codersdk.ChatMessageRoleUser, edited.Role)
foundEditedText := false
@@ -2753,7 +2755,9 @@ func TestPatchChatMessage(t *testing.T) {
},
})
require.NoError(t, err)
require.Equal(t, userMessageID, edited.ID)
// The edited message is soft-deleted and a new one is inserted,
// so the returned ID will differ from the original.
require.NotEqual(t, userMessageID, edited.ID)
// Assert the edit response preserves the file_id.
var foundText, foundFile bool
+26 -12
View File
@@ -1824,18 +1824,6 @@ func (q *querier) DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, u
return q.db.DeleteApplicationConnectAPIKeysByUserID(ctx, userID)
}
func (q *querier) DeleteChatMessagesAfterID(ctx context.Context, arg database.DeleteChatMessagesAfterIDParams) error {
// Authorize update on the parent chat.
chat, err := q.db.GetChatByID(ctx, arg.ChatID)
if err != nil {
return err
}
if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil {
return err
}
return q.db.DeleteChatMessagesAfterID(ctx, arg)
}
func (q *querier) DeleteChatModelConfigByID(ctx context.Context, id uuid.UUID) error {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil {
return err
@@ -5391,6 +5379,32 @@ func (q *querier) SelectUsageEventsForPublishing(ctx context.Context, arg time.T
return q.db.SelectUsageEventsForPublishing(ctx, arg)
}
func (q *querier) SoftDeleteChatMessageByID(ctx context.Context, id int64) error {
msg, err := q.db.GetChatMessageByID(ctx, id)
if err != nil {
return err
}
chat, err := q.db.GetChatByID(ctx, msg.ChatID)
if err != nil {
return err
}
if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil {
return err
}
return q.db.SoftDeleteChatMessageByID(ctx, id)
}
func (q *querier) SoftDeleteChatMessagesAfterID(ctx context.Context, arg database.SoftDeleteChatMessagesAfterIDParams) error {
chat, err := q.db.GetChatByID(ctx, arg.ChatID)
if err != nil {
return err
}
if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil {
return err
}
return q.db.SoftDeleteChatMessagesAfterID(ctx, arg)
}
func (q *querier) TryAcquireLock(ctx context.Context, id int64) (bool, error) {
return q.db.TryAcquireLock(ctx, id)
}
+14 -3
View File
@@ -401,16 +401,27 @@ func (s *MethodTestSuite) TestChats() {
dbm.EXPECT().UnarchiveChatByID(gomock.Any(), chat.ID).Return(nil).AnyTimes()
check.Args(chat.ID).Asserts(chat, policy.ActionUpdate).Returns()
}))
s.Run("DeleteChatMessagesAfterID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
s.Run("SoftDeleteChatMessagesAfterID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
chat := testutil.Fake(s.T(), faker, database.Chat{})
arg := database.DeleteChatMessagesAfterIDParams{
arg := database.SoftDeleteChatMessagesAfterIDParams{
ChatID: chat.ID,
AfterID: 123,
}
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
dbm.EXPECT().DeleteChatMessagesAfterID(gomock.Any(), arg).Return(nil).AnyTimes()
dbm.EXPECT().SoftDeleteChatMessagesAfterID(gomock.Any(), arg).Return(nil).AnyTimes()
check.Args(arg).Asserts(chat, policy.ActionUpdate).Returns()
}))
s.Run("SoftDeleteChatMessageByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
chat := testutil.Fake(s.T(), faker, database.Chat{})
msg := database.ChatMessage{
ID: 456,
ChatID: chat.ID,
}
dbm.EXPECT().GetChatMessageByID(gomock.Any(), msg.ID).Return(msg, nil).AnyTimes()
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
dbm.EXPECT().SoftDeleteChatMessageByID(gomock.Any(), msg.ID).Return(nil).AnyTimes()
check.Args(msg.ID).Asserts(chat, policy.ActionUpdate).Returns()
}))
s.Run("DeleteChatModelConfigByID", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
id := uuid.New()
dbm.EXPECT().DeleteChatModelConfigByID(gomock.Any(), id).Return(nil).AnyTimes()
+16 -8
View File
@@ -384,14 +384,6 @@ func (m queryMetricsStore) DeleteApplicationConnectAPIKeysByUserID(ctx context.C
return r0
}
func (m queryMetricsStore) DeleteChatMessagesAfterID(ctx context.Context, arg database.DeleteChatMessagesAfterIDParams) error {
start := time.Now()
r0 := m.s.DeleteChatMessagesAfterID(ctx, arg)
m.queryLatencies.WithLabelValues("DeleteChatMessagesAfterID").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "DeleteChatMessagesAfterID").Inc()
return r0
}
func (m queryMetricsStore) DeleteChatModelConfigByID(ctx context.Context, id uuid.UUID) error {
start := time.Now()
r0 := m.s.DeleteChatModelConfigByID(ctx, id)
@@ -3776,6 +3768,22 @@ func (m queryMetricsStore) SelectUsageEventsForPublishing(ctx context.Context, n
return r0, r1
}
func (m queryMetricsStore) SoftDeleteChatMessageByID(ctx context.Context, id int64) error {
start := time.Now()
r0 := m.s.SoftDeleteChatMessageByID(ctx, id)
m.queryLatencies.WithLabelValues("SoftDeleteChatMessageByID").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "SoftDeleteChatMessageByID").Inc()
return r0
}
func (m queryMetricsStore) SoftDeleteChatMessagesAfterID(ctx context.Context, arg database.SoftDeleteChatMessagesAfterIDParams) error {
start := time.Now()
r0 := m.s.SoftDeleteChatMessagesAfterID(ctx, arg)
m.queryLatencies.WithLabelValues("SoftDeleteChatMessagesAfterID").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "SoftDeleteChatMessagesAfterID").Inc()
return r0
}
func (m queryMetricsStore) TryAcquireLock(ctx context.Context, pgTryAdvisoryXactLock int64) (bool, error) {
start := time.Now()
r0, r1 := m.s.TryAcquireLock(ctx, pgTryAdvisoryXactLock)
+28 -14
View File
@@ -598,20 +598,6 @@ func (mr *MockStoreMockRecorder) DeleteApplicationConnectAPIKeysByUserID(ctx, us
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteApplicationConnectAPIKeysByUserID", reflect.TypeOf((*MockStore)(nil).DeleteApplicationConnectAPIKeysByUserID), ctx, userID)
}
// DeleteChatMessagesAfterID mocks base method.
func (m *MockStore) DeleteChatMessagesAfterID(ctx context.Context, arg database.DeleteChatMessagesAfterIDParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteChatMessagesAfterID", ctx, arg)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteChatMessagesAfterID indicates an expected call of DeleteChatMessagesAfterID.
func (mr *MockStoreMockRecorder) DeleteChatMessagesAfterID(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteChatMessagesAfterID", reflect.TypeOf((*MockStore)(nil).DeleteChatMessagesAfterID), ctx, arg)
}
// DeleteChatModelConfigByID mocks base method.
func (m *MockStore) DeleteChatModelConfigByID(ctx context.Context, id uuid.UUID) error {
m.ctrl.T.Helper()
@@ -7112,6 +7098,34 @@ func (mr *MockStoreMockRecorder) SelectUsageEventsForPublishing(ctx, now any) *g
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SelectUsageEventsForPublishing", reflect.TypeOf((*MockStore)(nil).SelectUsageEventsForPublishing), ctx, now)
}
// SoftDeleteChatMessageByID mocks base method.
func (m *MockStore) SoftDeleteChatMessageByID(ctx context.Context, id int64) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SoftDeleteChatMessageByID", ctx, id)
ret0, _ := ret[0].(error)
return ret0
}
// SoftDeleteChatMessageByID indicates an expected call of SoftDeleteChatMessageByID.
func (mr *MockStoreMockRecorder) SoftDeleteChatMessageByID(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SoftDeleteChatMessageByID", reflect.TypeOf((*MockStore)(nil).SoftDeleteChatMessageByID), ctx, id)
}
// SoftDeleteChatMessagesAfterID mocks base method.
func (m *MockStore) SoftDeleteChatMessagesAfterID(ctx context.Context, arg database.SoftDeleteChatMessagesAfterIDParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SoftDeleteChatMessagesAfterID", ctx, arg)
ret0, _ := ret[0].(error)
return ret0
}
// SoftDeleteChatMessagesAfterID indicates an expected call of SoftDeleteChatMessagesAfterID.
func (mr *MockStoreMockRecorder) SoftDeleteChatMessagesAfterID(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SoftDeleteChatMessagesAfterID", reflect.TypeOf((*MockStore)(nil).SoftDeleteChatMessagesAfterID), ctx, arg)
}
// TryAcquireLock mocks base method.
func (m *MockStore) TryAcquireLock(ctx context.Context, pgTryAdvisoryXactLock int64) (bool, error) {
m.ctrl.T.Helper()
+2 -1
View File
@@ -1290,7 +1290,8 @@ CREATE TABLE chat_messages (
created_by uuid,
content_version smallint NOT NULL,
total_cost_micros bigint,
runtime_ms bigint
runtime_ms bigint,
deleted boolean DEFAULT false NOT NULL
);
CREATE SEQUENCE chat_messages_id_seq
@@ -0,0 +1,2 @@
DELETE FROM chat_messages WHERE deleted = true;
ALTER TABLE chat_messages DROP COLUMN deleted;
@@ -0,0 +1 @@
ALTER TABLE chat_messages ADD COLUMN deleted boolean NOT NULL DEFAULT false;
+1
View File
@@ -4225,6 +4225,7 @@ type ChatMessage struct {
ContentVersion int16 `db:"content_version" json:"content_version"`
TotalCostMicros sql.NullInt64 `db:"total_cost_micros" json:"total_cost_micros"`
RuntimeMs sql.NullInt64 `db:"runtime_ms" json:"runtime_ms"`
Deleted bool `db:"deleted" json:"deleted"`
}
type ChatModelConfig struct {
+2 -1
View File
@@ -98,7 +98,6 @@ type sqlcQuerier interface {
// be recreated.
DeleteAllWebpushSubscriptions(ctx context.Context) error
DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error
DeleteChatMessagesAfterID(ctx context.Context, arg DeleteChatMessagesAfterIDParams) error
DeleteChatModelConfigByID(ctx context.Context, id uuid.UUID) error
DeleteChatProviderByID(ctx context.Context, id uuid.UUID) error
DeleteChatQueuedMessage(ctx context.Context, arg DeleteChatQueuedMessageParams) error
@@ -756,6 +755,8 @@ type sqlcQuerier interface {
// for the table.
// The CTE and the reorder is required because UPDATE doesn't guarantee order.
SelectUsageEventsForPublishing(ctx context.Context, now time.Time) ([]UsageEvent, error)
SoftDeleteChatMessageByID(ctx context.Context, id int64) error
SoftDeleteChatMessagesAfterID(ctx context.Context, arg SoftDeleteChatMessagesAfterIDParams) error
// Non blocking lock. Returns true if the lock was acquired, false otherwise.
//
// This must be called from within a transaction. The lock will be automatically
+54 -25
View File
@@ -3605,24 +3605,6 @@ func (q *sqlQuerier) DeleteAllChatQueuedMessages(ctx context.Context, chatID uui
return err
}
const deleteChatMessagesAfterID = `-- name: DeleteChatMessagesAfterID :exec
DELETE FROM
chat_messages
WHERE
chat_id = $1::uuid
AND id > $2::bigint
`
type DeleteChatMessagesAfterIDParams struct {
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
AfterID int64 `db:"after_id" json:"after_id"`
}
func (q *sqlQuerier) DeleteChatMessagesAfterID(ctx context.Context, arg DeleteChatMessagesAfterIDParams) error {
_, err := q.db.ExecContext(ctx, deleteChatMessagesAfterID, arg.ChatID, arg.AfterID)
return err
}
const deleteChatQueuedMessage = `-- name: DeleteChatQueuedMessage :exec
DELETE FROM chat_queued_messages WHERE id = $1 AND chat_id = $2
`
@@ -4189,11 +4171,12 @@ func (q *sqlQuerier) GetChatDiffStatusesByChatIDs(ctx context.Context, chatIds [
const getChatMessageByID = `-- name: GetChatMessageByID :one
SELECT
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted
FROM
chat_messages
WHERE
id = $1::bigint
AND deleted = false
`
func (q *sqlQuerier) GetChatMessageByID(ctx context.Context, id int64) (ChatMessage, error) {
@@ -4219,19 +4202,21 @@ func (q *sqlQuerier) GetChatMessageByID(ctx context.Context, id int64) (ChatMess
&i.ContentVersion,
&i.TotalCostMicros,
&i.RuntimeMs,
&i.Deleted,
)
return i, err
}
const getChatMessagesByChatID = `-- name: GetChatMessagesByChatID :many
SELECT
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted
FROM
chat_messages
WHERE
chat_id = $1::uuid
AND id > $2::bigint
AND visibility IN ('user', 'both')
AND deleted = false
ORDER BY
created_at ASC
`
@@ -4270,6 +4255,7 @@ func (q *sqlQuerier) GetChatMessagesByChatID(ctx context.Context, arg GetChatMes
&i.ContentVersion,
&i.TotalCostMicros,
&i.RuntimeMs,
&i.Deleted,
); err != nil {
return nil, err
}
@@ -4286,7 +4272,7 @@ func (q *sqlQuerier) GetChatMessagesByChatID(ctx context.Context, arg GetChatMes
const getChatMessagesByChatIDDescPaginated = `-- name: GetChatMessagesByChatIDDescPaginated :many
SELECT
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted
FROM
chat_messages
WHERE
@@ -4296,6 +4282,7 @@ WHERE
ELSE true
END
AND visibility IN ('user', 'both')
AND deleted = false
ORDER BY
id DESC
LIMIT
@@ -4337,6 +4324,7 @@ func (q *sqlQuerier) GetChatMessagesByChatIDDescPaginated(ctx context.Context, a
&i.ContentVersion,
&i.TotalCostMicros,
&i.RuntimeMs,
&i.Deleted,
); err != nil {
return nil, err
}
@@ -4360,6 +4348,7 @@ WITH latest_compressed_summary AS (
WHERE
chat_id = $1::uuid
AND compressed = TRUE
AND deleted = false
AND visibility = 'model'
ORDER BY
created_at DESC,
@@ -4368,12 +4357,13 @@ WITH latest_compressed_summary AS (
1
)
SELECT
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted
FROM
chat_messages
WHERE
chat_id = $1::uuid
AND visibility IN ('model', 'both')
AND deleted = false
AND (
(
role = 'system'
@@ -4437,6 +4427,7 @@ func (q *sqlQuerier) GetChatMessagesForPromptByChatID(ctx context.Context, chatI
&i.ContentVersion,
&i.TotalCostMicros,
&i.RuntimeMs,
&i.Deleted,
); err != nil {
return nil, err
}
@@ -4641,12 +4632,13 @@ func (q *sqlQuerier) GetChats(ctx context.Context, arg GetChatsParams) ([]Chat,
const getLastChatMessageByRole = `-- name: GetLastChatMessageByRole :one
SELECT
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted
FROM
chat_messages
WHERE
chat_id = $1::uuid
AND role = $2::chat_message_role
AND deleted = false
ORDER BY
created_at DESC, id DESC
LIMIT
@@ -4681,6 +4673,7 @@ func (q *sqlQuerier) GetLastChatMessageByRole(ctx context.Context, arg GetLastCh
&i.ContentVersion,
&i.TotalCostMicros,
&i.RuntimeMs,
&i.Deleted,
)
return i, err
}
@@ -4908,7 +4901,7 @@ SELECT
NULLIF(UNNEST($16::bigint[]), 0),
NULLIF(UNNEST($17::bigint[]), 0)
RETURNING
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted
`
type InsertChatMessagesParams struct {
@@ -4978,6 +4971,7 @@ func (q *sqlQuerier) InsertChatMessages(ctx context.Context, arg InsertChatMessa
&i.ContentVersion,
&i.TotalCostMicros,
&i.RuntimeMs,
&i.Deleted,
); err != nil {
return nil, err
}
@@ -5174,6 +5168,40 @@ func (q *sqlQuerier) ResolveUserChatSpendLimit(ctx context.Context, userID uuid.
return effective_limit_micros, err
}
const softDeleteChatMessageByID = `-- name: SoftDeleteChatMessageByID :exec
UPDATE
chat_messages
SET
deleted = true
WHERE
id = $1::bigint
`
func (q *sqlQuerier) SoftDeleteChatMessageByID(ctx context.Context, id int64) error {
_, err := q.db.ExecContext(ctx, softDeleteChatMessageByID, id)
return err
}
const softDeleteChatMessagesAfterID = `-- name: SoftDeleteChatMessagesAfterID :exec
UPDATE
chat_messages
SET
deleted = true
WHERE
chat_id = $1::uuid
AND id > $2::bigint
`
type SoftDeleteChatMessagesAfterIDParams struct {
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
AfterID int64 `db:"after_id" json:"after_id"`
}
func (q *sqlQuerier) SoftDeleteChatMessagesAfterID(ctx context.Context, arg SoftDeleteChatMessagesAfterIDParams) error {
_, err := q.db.ExecContext(ctx, softDeleteChatMessagesAfterID, arg.ChatID, arg.AfterID)
return err
}
const unarchiveChatByID = `-- name: UnarchiveChatByID :exec
UPDATE chats SET archived = false, updated_at = NOW() WHERE id = $1::uuid
`
@@ -5259,7 +5287,7 @@ SET
WHERE
id = $3::bigint
RETURNING
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted
`
type UpdateChatMessageByIDParams struct {
@@ -5291,6 +5319,7 @@ func (q *sqlQuerier) UpdateChatMessageByID(ctx context.Context, arg UpdateChatMe
&i.ContentVersion,
&i.TotalCostMicros,
&i.RuntimeMs,
&i.Deleted,
)
return i, err
}
+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