Thanks to visit codestin.com
Credit goes to github.com

Skip to content

DNM: feat: backfill chat_messages.search_tsv - #26995

Closed
johnstcn wants to merge 5 commits into
cian/codagt-721-add-chat_message_search_text-extraction-functionfrom
cian/codagt-722-dbpurge-sweep-to-backfill-and-maintain
Closed

DNM: feat: backfill chat_messages.search_tsv#26995
johnstcn wants to merge 5 commits into
cian/codagt-721-add-chat_message_search_text-extraction-functionfrom
cian/codagt-722-dbpurge-sweep-to-backfill-and-maintain

Conversation

@johnstcn

@johnstcn johnstcn commented Jul 6, 2026

Copy link
Copy Markdown
Member

Adds a chat message search backfill step to the dbpurge tick. Migration 000541 (CODAGT-721) added chat_messages.search_tsv with all rows NULL; this drains that pending queue in batches, newest first, and keeps it drained for new messages.

Stacked on #26968 (cian/codagt-721-add-chat_message_search_text-extraction-function). Do not merge before it.

  • New sqlc query BackfillChatMessagesSearchTsv: single select+update whose WHERE clause repeats the idx_chat_messages_search_tsv_pending predicate so the partial index serves it. Rows with no extractable text get an empty-tsvector sentinel so they leave the queue.
  • The backfill runs inside the existing purgeTick transaction (dbpurge advisory lock already held), capped at 5 batches of 10k rows per 10-minute tick to bound transaction growth; larger backlogs drain across ticks.
  • New counter coderd_dbpurge_chat_search_rows_backfilled_total.
  • The backfill authorizes chat:update, matching the AutoArchiveInactiveChats precedent; no new permissions for the dbpurge subject.

Issue: https://linear.app/codercom/issue/CODAGT-722

Implementation plan

CODAGT-722: dbpurge backfill to populate and maintain chat_messages.search_tsv

Issue: https://linear.app/codercom/issue/CODAGT-722
Depends on: CODAGT-721 schema (PR #26968, migration 000541_chat_search_schema)
Branch: cian/codagt-722-dbpurge-sweep-to-backfill-and-maintain, stacked on the 721 branch until #26968 merges. Deliverable: one draft PR.

Context

Migration 000541 added chat_messages.search_tsv (all NULL), the search GIN index, and the pending-queue partial index (idx_chat_messages_search_tsv_pending: search_tsv IS NULL AND deleted = false AND visibility IN ('user','both') AND role IN ('user','assistant'), btree on id DESC). This task drains that queue: it computes tsvectors for pending rows in batches, newest first, inside the existing dbpurge tick.

Design

Query (sqlc, in coderd/database/queries/chats.sql)

One statement, select+update combined (mirrors the benchmarked form):

-- name: BackfillChatMessagesSearchTsv :execrows
WITH batch AS (
    SELECT id FROM chat_messages
    WHERE search_tsv IS NULL
      AND deleted = false
      AND visibility IN ('user', 'both')
      AND role IN ('user', 'assistant')
    ORDER BY id DESC
    LIMIT @batch_size::int
)
UPDATE chat_messages cm
SET search_tsv = COALESCE(
    to_tsvector('simple', chat_message_search_text(cm.content)),
    ''::tsvector)
FROM batch WHERE cm.id = batch.id;
  • WHERE clause matches the pending index predicate exactly (required for the index to serve it); add a comment stating this coupling and referencing the index name.
  • COALESCE(..., '') sentinel: distinguishes "backfilled, no text" from "pending". Comment this.
  • :execrows returns affected rows; the backfill loop stops when < batch size.
  • dbauthz: follow the pattern of existing dbpurge queries (e.g. DeleteOldWorkspaceAgentLogs) for the generated authz wrapper; context is already dbauthz.AsDBPurge.

Backfill loop (in coderd/database/dbpurge/dbpurge.go)

  • New step inside the existing purgeTick transaction, after the current purge work (lock already held via TryAcquireLock(LockIDDBPurge)).
  • Constants: chatSearchBackfillBatchSize = 10_000 (benchmark sweet spot, ~800ms/batch), chatSearchBackfillMaxBatches = 5 per tick.
  • Loop: call BackfillChatMessagesSearchTsv up to maxBatches times; stop early when rows affected < batch size. Caps per-tick transaction growth at ~4-5s even on a cold 1M-row backlog.
  • Record the correctness rationale as a comment (queue membership is per-row, content immutable post-insert, soft-deletes handled by index maintenance).
  • Metrics: increment the existing coderd_dbpurge_records_purged_total pattern is wrong semantically ("purged"); add coderd_dbpurge_chat_search_rows_backfilled_total counter (no labels), registered alongside the existing ones.

Drain-rate trade-off (flagged decision)

5 batches x 10k per 10-minute tick = 50k rows/tick. A dev.coder.com-scale backlog (~500k eligible rows) drains in ~100 minutes; typical customer deployments in one or two ticks. Alternative: loop until empty on the first tick (simple, but a single multi-minute transaction holding the dbpurge lock delays all other purge work and bloats the tx). Recommendation: ship the cap; revisit only if field feedback demands faster initial drain.

Red: tests first

Postgres-backed, in coderd/database/dbpurge/dbpurge_test.go, following the existing quartz + awaitDoTick patterns (non-parallel, shared lock ID):

  1. Drain converges: seed eligible + ineligible messages (mix of no-text, tool-role, model-only, deleted, child-chat irrelevant here); run ticks; eventually zero pending rows; all eligible rows have non-NULL search_tsv; ineligible rows remain NULL.
  2. Newest first: seed > 1 batch of eligible rows with a small batch size (override via test hook or use the constant with small seed); after one batch, the backfilled rows are the highest ids.
  3. Sentinel: no-text eligible rows end up with ''::tsvector (not NULL) and do not reappear in the pending query.
  4. Per-tick bound: seed > maxBatches x batchSize rows; one tick backfills at most maxBatches x batchSize; next tick continues.
  5. Deleted rows: soft-delete a not-yet-backfilled eligible row; it is never backfilled (search_tsv stays NULL) and does not appear in pending results.
  6. Post-drain freshness: after drain, insert a new eligible message; next tick backfills it.
  7. Steady state: tick with empty queue performs no updates (rows affected 0 on first batch; loop exits).
  8. Metrics: counter increments by rows backfilled.
  9. Lock safety: reuse the existing mock-store contention test pattern to assert the backfill is skipped when the lock is held.

Batch size/max batches need to be overridable for tests (package-level vars or an Option, following how dbpurge handles clock injection; prefer an Option to keep tests race-safe).

Green: implementation

  1. Add the sqlc query; make gen (querier, dbauthz, dbmetrics, dbmock all regenerate; no audit-table entry, internal operation).
  2. Add the backfill step + metrics + options to dbpurge.
  3. Make tests pass; make fmt && make lint; run make test RUN=TestPurge (or the relevant test names) and make test-race for the new tests.

Refactor

  • Re-check the backfill loop against the rest of purgeTick for structural consistency (error wrapping style, logging fields).
  • Confirm comments capture: predicate coupling, sentinel, immutability rationale, and the drain-rate cap; nothing restating the code.

Verification

  • make gen no stray diff; make test, make test-race, make lint pass.
  • Manual EXPLAIN smoke check on a seeded DB: pending SELECT uses idx_chat_messages_search_tsv_pending.
  • Draft PR stacked on DNM: feat(coderd): add chat search schema #26968 with plan in collapsible section and Coder Agents disclosure.

This PR was generated by Coder Agents on behalf of @johnstcn.

@linear-code

linear-code Bot commented Jul 6, 2026

Copy link
Copy Markdown

CODAGT-722

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Docs preview

📖 View docs preview for docs/admin/integrations/prometheus.md

@johnstcn johnstcn changed the title feat(coderd/database/dbpurge): sweep chat_messages.search_tsv backfill feat(coderd/database/dbpurge): backfill chat_messages.search_tsv Jul 6, 2026
@johnstcn
johnstcn force-pushed the cian/codagt-721-add-chat_message_search_text-extraction-function branch from 6a771de to 55d9604 Compare July 8, 2026 10:55
@johnstcn
johnstcn force-pushed the cian/codagt-722-dbpurge-sweep-to-backfill-and-maintain branch from e11a3a8 to f367661 Compare July 8, 2026 10:57
@johnstcn

johnstcn commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

/coder-agents-review

@coder-agents-review

coder-agents-review Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Chat: Review posted | View chat
Requested: 2026-07-08 23:02 UTC by @johnstcn
Spend: $44.16 / $100.00

Review history
  • R1 (2026-07-08): 15 reviewers, 1 Nit, 5 P3, COMMENT. Review
  • R2 (2026-07-08): 6 reviewers, 6 Nit, 5 P3, APPROVE. Review
  • R3 (2026-07-08): 3 reviewers, 6 Nit, 5 P3, APPROVE. Review

deep-review v0.9.0 | Round 3 | b7239ec..01a47d9

Last posted: Round 3, 11 findings (5 P3, 6 Nit), APPROVE. Review

Finding inventory

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 P3 Author fixed (7381fb1) dbpurge.go:342 Comment opening restates function call before the incremental-safety invariant R1 Gon P2 Yes
CRF-2 P3 Author fixed (7381fb1) dbpurge.go:54 Constant comment opens with restatement of name and query detail R1 Gon P2 Yes
CRF-3 P3 Author fixed (7381fb1) chats.sql:328 SQL comment restates COALESCE expression before the semantic distinction R1 Gon P2 Yes
CRF-4 P3 Author fixed (7381fb1) dbpurge.go:352 Error message says "search vectors", ambiguous with embeddings in an AI codebase R1 Leorio Yes
CRF-5 P3 Author fixed (7381fb1) dbpurge_test.go:3039 DrainConverges never checks tsvector content; empty sentinel would pass all tests R1 Chopper Yes
CRF-6 Nit Author fixed (7381fb1) dbpurge_test.go:3068 Test comment restates what setup and assertions already show R1 Gon Yes
CRF-7 Nit Author fixed (01a47d9) dbpurge.go:69 Godoc restates parameter semantics visible in the signature R2 Gon P2 Yes
CRF-8 Nit Author fixed (01a47d9) dbpurge.go:104 Metric counter comment restates what the code shows R2 Gon P2 Yes
CRF-9 Nit Author fixed (01a47d9) dbpurge_test.go:2886 Verbose connector phrases in trap-mapping comment R2 Gon P2 Yes
CRF-10 Nit Author fixed (01a47d9) dbpurge_test.go:2982 countPending comment restates function name and WHERE clause R2 Gon P2 Yes
CRF-11 Nit Author fixed (01a47d9) dbpurge_test.go:3010 requireTsvFor comment restates function name R2 Gon P2 Yes

Round log

Round 1

Panel (15 reviewers: Bisky, Hisoka, Mafu-san, Mafuuu, Pariston, Gon, Leorio, Ging-Go, Knuckle, Killua, Meruem, Komugi, Chopper, Knov, Zoro). Netero: no findings. Panel: 4 P3, 1 Nit, 0 dropped. Reviewed against 55d9604..f367661.

Round 2

Churn guard: PROCEED (6/6 addressed). Panel (6 reviewers: Bisky, Mafuuu, Pariston, Gon, Leorio, Meruem). Netero: no findings. Panel: 5 Nit (all comment verbosity, downgraded from Gon P2), 0 P3+. Reviewed against b7239ec..554ef39.

Round 3

Churn guard: PROCEED (5/5 addressed). Panel (3 reviewers: Bisky, Mafuuu, Pariston). Netero: no findings. Panel: no findings. All 11 findings resolved. Reviewed against b7239ec..01a47d9.

About deep-review

CRF = Coder Review Finding (P0-P4, Nit, Note)

Reviewer Focus
Bisky tests
Chopper ops/errors
Churn-guard change verification
Ging language modernization
Gon naming
Hisoka edge cases
Killua perf
Kite change integrity
Knov contracts
Knuckle SQL
Komugi flake/determinism
Kurapika security
Law decomposition
Leorio docs
Luffy product
Mafu-san process
Mafuuu contracts
Melody dispatch/pairing
Meruem structural
Nami frontend
Netero mechanical checks
Pariston premise testing
Pen-botter product gaps
Razor verification
Robin duplication
Ryosuke Go arch
Takumi concurrency
Zoro shape

🤖 Managed by Coder Agents.

@johnstcn johnstcn changed the title feat(coderd/database/dbpurge): backfill chat_messages.search_tsv feat: backfill chat_messages.search_tsv Jul 8, 2026
coder-agents-review[bot]

This comment was marked as resolved.

@johnstcn
johnstcn force-pushed the cian/codagt-722-dbpurge-sweep-to-backfill-and-maintain branch from 7381fb1 to 2d5bcc9 Compare July 8, 2026 14:51
@johnstcn
johnstcn force-pushed the cian/codagt-721-add-chat_message_search_text-extraction-function branch from e0c2ed2 to fc26db0 Compare July 8, 2026 18:37
@johnstcn
johnstcn force-pushed the cian/codagt-722-dbpurge-sweep-to-backfill-and-maintain branch 2 times, most recently from c6a884b to 7ee0e38 Compare July 8, 2026 19:16
@johnstcn
johnstcn force-pushed the cian/codagt-722-dbpurge-sweep-to-backfill-and-maintain branch from 7ee0e38 to 554ef39 Compare July 8, 2026 19:33
@johnstcn

johnstcn commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All six R1 findings fixed cleanly in 7381fb1. Six reviewers verified the fixes; all confirmed. No regression from the fix commit.

The code is solid. 5 Nits remain (all comment verbosity, same pattern as R1 but milder, 3-10 words each). These are marginal trims that don't affect correctness or readability.

"I tried to build a simpler alternative and couldn't find one that doesn't introduce its own complexity. The solution is proportional." (Pariston R2)

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/database/dbpurge/dbpurge.go Outdated
return func(i *instance) { i.clk = clk }
}

// WithChatSearchBackfillLimits overrides the per-batch row limit and the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit [CRF-7] batchSize int32 = "per-batch row limit", maxBatches int = "per-tick batch cap", function name = "chat message search backfill". The signature says it all.

Trim to: // WithChatSearchBackfillLimits overrides backfill batch size and cap. For tests.

(Gon P2, downgraded: standard Go doc comment, slight verbosity doesn't harm readability)

🤖

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: Adopted verbatim in 01a47d9.

🤖 Posted by Coder Agents on behalf of @johnstcn

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified fixed in 01a47d9975. Adopted verbatim.

🤖

Comment thread coderd/database/dbpurge/dbpurge.go Outdated
}, []string{"record_type"})
reg.MustRegister(recordsPurged)

// The backfill updates rows rather than purging them, so it gets its own

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit [CRF-8] "so it gets its own counter instead of a records_purged_total label" is visible from the code (separate prometheus.Counter vs label on CounterVec).

Trim to: // Separate counter: the backfill updates rows, not purges them.

(Gon P2, downgraded: the why ("updates, not purges") is the real content, but the restatement doesn't confuse)

🤖

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: Adopted verbatim in 01a47d9.

🤖 Posted by Coder Agents on behalf of @johnstcn

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified fixed in 01a47d9975. Adopted verbatim.

🤖

Comment thread coderd/database/dbpurge/dbpurge_test.go Outdated
defer trapReset.Close()
defer trapStop.Close()
defer trapNow.Close()
// Wait for the initial tick signified by a call to Now(), then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit [CRF-9] "signified by a call to" and "that signifies" are 6 extra words. Reads fine, but can be tighter.

Trim to: // Initial tick: Now() trap. Completion: TickerReset trap.

(Gon P2, downgraded: test-internal comment, readability benefits from the connectors)

🤖

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: Adopted verbatim in 01a47d9.

🤖 Posted by Coder Agents on behalf of @johnstcn

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified fixed in 01a47d9975. Adopted verbatim.

🤖

Comment thread coderd/database/dbpurge/dbpurge_test.go Outdated
_, err := rawDB.ExecContext(ctx, "UPDATE chat_messages SET deleted = true WHERE id = $1", id)
require.NoError(t, err)
}
// countPending repeats the predicate of

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit [CRF-10] "countPending" restates the function name. The trap (index predicate coupling) is the value.

Trim to: // Repeats the predicate of idx_chat_messages_search_tsv_pending.

(Gon P2, downgraded: the clarification aids readers unfamiliar with the index)

🤖

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: Adopted verbatim in 01a47d9.

🤖 Posted by Coder Agents on behalf of @johnstcn

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified fixed in 01a47d9975. Adopted verbatim.

🤖

Comment thread coderd/database/dbpurge/dbpurge_test.go Outdated
isNull, _ := searchTsv(ctx, t, rawDB, id)
require.False(t, isNull, msg)
}
// requireTsvFor asserts the row was backfilled with the tsvector of

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit [CRF-11] "requireTsvFor" restates the function name. "such as the sentinel" implied by "not just non-NULL."

Trim to: // Asserts the row's tsvector matches expectedText, not just non-NULL.

(Gon P2, downgraded: new helper, slightly verbose doc aids first-time readers)

🤖

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: Adopted verbatim in 01a47d9.

🤖 Posted by Coder Agents on behalf of @johnstcn

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified fixed in 01a47d9975. Adopted verbatim.

🤖

johnstcn commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

/coder-agents-review

All prior findings addressed and replied to inline. Requesting a convergence pass.

🤖 Requested via Coder Agents on behalf of @johnstcn.

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All 11 findings across 3 rounds resolved. The R2 nits (CRF-7 through CRF-11) were adopted verbatim in 01a47d9. Netero R3 confirmed no regressions.

11/11 findings fixed, 0 open. Clean convergence.

🤖 This review was automatically generated with Coder Agents.

@johnstcn johnstcn changed the title feat: backfill chat_messages.search_tsv DNM: feat: backfill chat_messages.search_tsv Jul 9, 2026
@github-actions github-actions Bot added the stale This issue is like stale bread. label Jul 19, 2026
@github-actions github-actions Bot closed this Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale This issue is like stale bread.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant