refactor(coderd/x/chatd): insert chats directly as pending on creation (#23888)

Previously, `CreateChat` inserted the `chats` row with the DB default
status (`waiting`), then updated it to `pending` in the same transaction
via `setChatPendingWithStore`. This wasted two extra queries per chat
creation (`GetChatByID` + `UpdateChatStatus`) and rewrote the same row
immediately after inserting it.

Now `CreateChat` passes the status directly to `InsertChat`, so the row
is written once in its final create-time state. The
`setChatPendingWithStore` helper is removed entirely. `InsertChat` now
requires an explicit `status` parameter at all callsites instead of
relying on a DB column default.

## Motivation

On an experimental branch we're trialing firing all chatd notifications
from plpgsql triggers. The old two-step insert made that awkward: in an
`AFTER INSERT` trigger, `NEW` only contained the insert-time row
(`waiting`), not the final committed state (`pending`). To emit the
correct event payload the trigger had to be deferred and re-read the row
from `chats` at commit time.

With this change, `NEW` already contains the correct row to publish — no
deferred trigger, no extra `SELECT`, simpler and cheaper trigger logic.

That said, this seems like a worthwhile change regardless of the trigger
experiment: writing the final row state once removes unnecessary DB work
on every chat creation and makes the create path easier to reason about.
This commit is contained in:
Ethan
2026-04-02 14:13:51 +11:00
committed by GitHub
parent fc1e0beb3b
commit 7757cd8e08
12 changed files with 78 additions and 35 deletions
+3 -1
View File
@@ -721,7 +721,9 @@ func (s *MethodTestSuite) TestChats() {
check.Args(threshold).Asserts(rbac.ResourceChat, policy.ActionRead).Returns(chats)
}))
s.Run("InsertChat", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
arg := testutil.Fake(s.T(), faker, database.InsertChatParams{})
arg := testutil.Fake(s.T(), faker, database.InsertChatParams{
Status: database.ChatStatusWaiting,
})
chat := testutil.Fake(s.T(), faker, database.Chat{OwnerID: arg.OwnerID})
dbm.EXPECT().InsertChat(gomock.Any(), arg).Return(chat, nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceChat.WithOwner(arg.OwnerID.String()), policy.ActionCreate).Returns(chat)
+14
View File
@@ -1285,6 +1285,7 @@ func TestGetAuthorizedChats(t *testing.T) {
// Create 3 chats owned by owner.
for i := range 3 {
_, err := db.InsertChat(ctx, database.InsertChatParams{
Status: database.ChatStatusWaiting,
OwnerID: owner.ID,
LastModelConfigID: modelCfg.ID,
Title: fmt.Sprintf("owner chat %d", i+1),
@@ -1295,6 +1296,7 @@ func TestGetAuthorizedChats(t *testing.T) {
// Create 2 chats owned by member.
for i := range 2 {
_, err := db.InsertChat(ctx, database.InsertChatParams{
Status: database.ChatStatusWaiting,
OwnerID: member.ID,
LastModelConfigID: modelCfg.ID,
Title: fmt.Sprintf("member chat %d", i+1),
@@ -1416,6 +1418,7 @@ func TestGetAuthorizedChats(t *testing.T) {
})
for i := range 7 {
_, err := db.InsertChat(ctx, database.InsertChatParams{
Status: database.ChatStatusWaiting,
OwnerID: paginationUser.ID,
LastModelConfigID: modelCfg.ID,
Title: fmt.Sprintf("pagination chat %d", i+1),
@@ -9472,6 +9475,7 @@ func TestInsertChatMessages(t *testing.T) {
)
chat, err := store.InsertChat(ctx, database.InsertChatParams{
Status: database.ChatStatusWaiting,
OwnerID: user.ID,
LastModelConfigID: modelConfigA.ID,
Title: "test-chat-" + uuid.NewString(),
@@ -9641,6 +9645,7 @@ func TestGetChatMessagesForPromptByChatID(t *testing.T) {
newChat := func(t *testing.T) database.Chat {
t.Helper()
chat, err := db.InsertChat(ctx, database.InsertChatParams{
Status: database.ChatStatusWaiting,
OwnerID: user.ID,
LastModelConfigID: modelCfg.ID,
Title: "test-chat-" + uuid.NewString(),
@@ -10014,6 +10019,7 @@ func TestGetPRInsights(t *testing.T) {
createChat := func(t *testing.T, store database.Store, userID, mcID uuid.UUID, title string) database.Chat {
t.Helper()
chat, err := store.InsertChat(context.Background(), database.InsertChatParams{
Status: database.ChatStatusWaiting,
OwnerID: userID,
LastModelConfigID: mcID,
Title: title,
@@ -10149,6 +10155,7 @@ func TestGetPRInsights(t *testing.T) {
createChildChat := func(t *testing.T, store database.Store, userID, mcID, parentID, rootID uuid.UUID, title string) database.Chat {
t.Helper()
chat, err := store.InsertChat(context.Background(), database.InsertChatParams{
Status: database.ChatStatusWaiting,
OwnerID: userID,
LastModelConfigID: mcID,
Title: title,
@@ -10538,6 +10545,7 @@ func TestChatPinOrderQueries(t *testing.T) {
t.Helper()
chat, err := db.InsertChat(ctx, database.InsertChatParams{
Status: database.ChatStatusWaiting,
OwnerID: ownerID,
LastModelConfigID: modelCfgID,
Title: title,
@@ -10718,6 +10726,7 @@ func TestChatLabels(t *testing.T) {
require.NoError(t, err)
chat, err := db.InsertChat(ctx, database.InsertChatParams{
Status: database.ChatStatusWaiting,
OwnerID: owner.ID,
LastModelConfigID: modelCfg.ID,
Title: "labeled-chat",
@@ -10740,6 +10749,7 @@ func TestChatLabels(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitMedium)
chat, err := db.InsertChat(ctx, database.InsertChatParams{
Status: database.ChatStatusWaiting,
OwnerID: owner.ID,
LastModelConfigID: modelCfg.ID,
Title: "no-labels-chat",
@@ -10755,6 +10765,7 @@ func TestChatLabels(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitMedium)
chat, err := db.InsertChat(ctx, database.InsertChatParams{
Status: database.ChatStatusWaiting,
OwnerID: owner.ID,
LastModelConfigID: modelCfg.ID,
Title: "update-labels-chat",
@@ -10795,6 +10806,7 @@ func TestChatLabels(t *testing.T) {
require.NoError(t, err)
chat, err := db.InsertChat(ctx, database.InsertChatParams{
Status: database.ChatStatusWaiting,
OwnerID: owner.ID,
LastModelConfigID: modelCfg.ID,
Title: "original-title",
@@ -10831,6 +10843,7 @@ func TestChatLabels(t *testing.T) {
labelsJSON, err := json.Marshal(tc.labels)
require.NoError(t, err)
_, err = db.InsertChat(ctx, database.InsertChatParams{
Status: database.ChatStatusWaiting,
OwnerID: owner.ID,
LastModelConfigID: modelCfg.ID,
Title: tc.title,
@@ -10916,6 +10929,7 @@ func TestChatHasUnread(t *testing.T) {
require.NoError(t, err)
chat, err := store.InsertChat(ctx, database.InsertChatParams{
Status: database.ChatStatusWaiting,
OwnerID: user.ID,
LastModelConfigID: modelCfg.ID,
Title: "test-chat-" + uuid.NewString(),
+6 -2
View File
@@ -5707,6 +5707,7 @@ INSERT INTO chats (
last_model_config_id,
title,
mode,
status,
mcp_server_ids,
labels
) VALUES (
@@ -5719,8 +5720,9 @@ INSERT INTO chats (
$7::uuid,
$8::text,
$9::chat_mode,
COALESCE($10::uuid[], '{}'::uuid[]),
COALESCE($11::jsonb, '{}'::jsonb)
$10::chat_status,
COALESCE($11::uuid[], '{}'::uuid[]),
COALESCE($12::jsonb, '{}'::jsonb)
)
RETURNING
id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context
@@ -5736,6 +5738,7 @@ type InsertChatParams struct {
LastModelConfigID uuid.UUID `db:"last_model_config_id" json:"last_model_config_id"`
Title string `db:"title" json:"title"`
Mode NullChatMode `db:"mode" json:"mode"`
Status ChatStatus `db:"status" json:"status"`
MCPServerIDs []uuid.UUID `db:"mcp_server_ids" json:"mcp_server_ids"`
Labels pqtype.NullRawMessage `db:"labels" json:"labels"`
}
@@ -5751,6 +5754,7 @@ func (q *sqlQuerier) InsertChat(ctx context.Context, arg InsertChatParams) (Chat
arg.LastModelConfigID,
arg.Title,
arg.Mode,
arg.Status,
pq.Array(arg.MCPServerIDs),
arg.Labels,
)
+2
View File
@@ -392,6 +392,7 @@ INSERT INTO chats (
last_model_config_id,
title,
mode,
status,
mcp_server_ids,
labels
) VALUES (
@@ -404,6 +405,7 @@ INSERT INTO chats (
@last_model_config_id::uuid,
@title::text,
sqlc.narg('mode')::chat_mode,
@status::chat_status,
COALESCE(@mcp_server_ids::uuid[], '{}'::uuid[]),
COALESCE(sqlc.narg('labels')::jsonb, '{}'::jsonb)
)