Files
multica/server/pkg/db/generated/activity.sql.go
b4bfe083be MUL-6407: mint UUIDv7 primary keys for append-heavy tables (#7230)
* perf(db): mint UUIDv7 primary keys for the append-heavy tables

Random v4 primary keys scatter every insert across the whole primary-key
B-tree, so each row dirties a different page. On the tables that grow
continuously — the task queue, messages, comments, activity and audit rows —
that is pure write amplification. UUIDv7 puts a big-endian millisecond
timestamp in the leading 48 bits, so consecutive inserts share a page.

The 12 tables converted here are the append-only hot paths:

  agent_task_queue, activity_log, inbox_item, comment, issue, chat_session,
  chat_message, autopilot_run, webhook_delivery, task_token,
  channel_inbound_audit, sys_cron_executions

Ids come from the new pkg/dbid, which returns a pgtype.UUID ready to assign to
a sqlc params struct. Upsert-dominated tables (task_usage, agent_runtime, ...)
are deliberately left alone: v7 only buys locality for rows that are actually
inserted. Lease and claim tokens keep gen_random_uuid() — they must stay
unpredictable and carry no timestamp.

No migration, and no backfill. Every converted INSERT binds its id as
COALESCE(sqlc.narg('id')::uuid, gen_random_uuid()), so the column default stays
reachable: a caller that passes no id inserts exactly as it does today. Tables
will hold a mix of v4 and v7 ids permanently, which the uuid type and every
parser in the repo already accept.

Verification: - pkg/dbid: version/variant, uniqueness, and a non-decreasing timestamp
    prefix; a shape test asserting every converted query keeps the COALESCE
    fallback; and a source scan asserting every production params literal mints
    an id, so a call site added later cannot silently fall back to v4.
  - internal/handler: round-trips issue, comment, activity_log, inbox_item,
    chat_session, chat_message, agent_task_queue, task_token and
    channel_inbound_audit against Postgres and asserts the stored id is v7,
    plus one case proving an unset id still inserts (as v4).
  - internal/scheduler: covers the one hand-written INSERT, asserting the
    execution id is v7 while lease_token stays v4.
Co-authored-by: multica-agent <[email protected]>

* fix(db): address UUIDv7 review feedback

Co-authored-by: multica-agent <[email protected]>

* fix(db): cover Telegram audit UUIDv7

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-20 13:42:44 +08:00

207 lines
6.0 KiB
Go

// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: activity.sql
package db
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const countAssigneeChangesByActor = `-- name: CountAssigneeChangesByActor :many
SELECT
details->>'to_type' as assignee_type,
details->>'to_id' as assignee_id,
COUNT(*)::bigint as frequency
FROM activity_log
WHERE workspace_id = $1
AND actor_id = $2
AND actor_type = 'member'
AND action = 'assignee_changed'
AND details->>'to_type' IS NOT NULL
AND details->>'to_id' IS NOT NULL
GROUP BY details->>'to_type', details->>'to_id'
`
type CountAssigneeChangesByActorParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
ActorID pgtype.UUID `json:"actor_id"`
}
type CountAssigneeChangesByActorRow struct {
AssigneeType interface{} `json:"assignee_type"`
AssigneeID interface{} `json:"assignee_id"`
Frequency int64 `json:"frequency"`
}
// Count how many times a user assigned each target via assignee_changed activities.
func (q *Queries) CountAssigneeChangesByActor(ctx context.Context, arg CountAssigneeChangesByActorParams) ([]CountAssigneeChangesByActorRow, error) {
rows, err := q.db.Query(ctx, countAssigneeChangesByActor, arg.WorkspaceID, arg.ActorID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []CountAssigneeChangesByActorRow{}
for rows.Next() {
var i CountAssigneeChangesByActorRow
if err := rows.Scan(&i.AssigneeType, &i.AssigneeID, &i.Frequency); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const createActivity = `-- name: CreateActivity :one
INSERT INTO activity_log (
workspace_id, issue_id, actor_type, actor_id, action, details, id
) VALUES ($1, $2, $3, $4, $5, $6, COALESCE($7::uuid, gen_random_uuid()))
RETURNING id, workspace_id, issue_id, actor_type, actor_id, action, details, created_at
`
type CreateActivityParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
IssueID pgtype.UUID `json:"issue_id"`
ActorType pgtype.Text `json:"actor_type"`
ActorID pgtype.UUID `json:"actor_id"`
Action string `json:"action"`
Details []byte `json:"details"`
ID pgtype.UUID `json:"id"`
}
func (q *Queries) CreateActivity(ctx context.Context, arg CreateActivityParams) (ActivityLog, error) {
row := q.db.QueryRow(ctx, createActivity,
arg.WorkspaceID,
arg.IssueID,
arg.ActorType,
arg.ActorID,
arg.Action,
arg.Details,
arg.ID,
)
var i ActivityLog
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.IssueID,
&i.ActorType,
&i.ActorID,
&i.Action,
&i.Details,
&i.CreatedAt,
)
return i, err
}
const getActivity = `-- name: GetActivity :one
SELECT id, workspace_id, issue_id, actor_type, actor_id, action, details, created_at FROM activity_log
WHERE id = $1
`
func (q *Queries) GetActivity(ctx context.Context, id pgtype.UUID) (ActivityLog, error) {
row := q.db.QueryRow(ctx, getActivity, id)
var i ActivityLog
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.IssueID,
&i.ActorType,
&i.ActorID,
&i.Action,
&i.Details,
&i.CreatedAt,
)
return i, err
}
const hasSquadLeaderNoActionEvaluationForTask = `-- name: HasSquadLeaderNoActionEvaluationForTask :one
SELECT EXISTS (
SELECT 1
FROM activity_log
WHERE issue_id = $1
AND actor_type = 'agent'
AND actor_id = $2
AND action = 'squad_leader_evaluated'
AND details->>'outcome' = 'no_action'
AND details->>'task_id' = $3::text
) AS exists
`
type HasSquadLeaderNoActionEvaluationForTaskParams struct {
IssueID pgtype.UUID `json:"issue_id"`
AgentID pgtype.UUID `json:"agent_id"`
TaskID string `json:"task_id"`
}
func (q *Queries) HasSquadLeaderNoActionEvaluationForTask(ctx context.Context, arg HasSquadLeaderNoActionEvaluationForTaskParams) (bool, error) {
row := q.db.QueryRow(ctx, hasSquadLeaderNoActionEvaluationForTask, arg.IssueID, arg.AgentID, arg.TaskID)
var exists bool
err := row.Scan(&exists)
return exists, err
}
const listActivitiesForIssue = `-- name: ListActivitiesForIssue :many
SELECT id, workspace_id, issue_id, actor_type, actor_id, action, details, created_at FROM (
SELECT id, workspace_id, issue_id, actor_type, actor_id, action, details, created_at FROM activity_log
WHERE issue_id = $1
ORDER BY created_at DESC, id DESC
LIMIT $2
) AS recent
ORDER BY created_at ASC, id ASC
`
type ListActivitiesForIssueParams struct {
IssueID pgtype.UUID `json:"issue_id"`
Limit int32 `json:"limit"`
}
// The NEWEST $2 activities for an issue, returned in chronological order.
//
// The cap has to bite at the OLD end, not the new one. The inner query takes
// the window with the keyset ordering (created_at DESC, id DESC), which
// idx_activity_log_issue_keyset (migration 068) satisfies without a sort step —
// it is not an index-only scan, since the index does not cover the columns
// SELECT * needs, so the heap is still read for the rows in the window. The
// outer query re-sorts ascending so every caller keeps the chronological
// contract it already had.
//
// Capping with ORDER BY created_at ASC instead discards the newest rows, which
// made a busy issue's timeline appear to stop at some point in the past with no
// indication anything was missing. Activity is machine-paced (description
// autosave, every agent run, status/assignee changes), so this was reachable in
// normal use, not only on pathological issues (MUL-5492).
func (q *Queries) ListActivitiesForIssue(ctx context.Context, arg ListActivitiesForIssueParams) ([]ActivityLog, error) {
rows, err := q.db.Query(ctx, listActivitiesForIssue, arg.IssueID, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ActivityLog{}
for rows.Next() {
var i ActivityLog
if err := rows.Scan(
&i.ID,
&i.WorkspaceID,
&i.IssueID,
&i.ActorType,
&i.ActorID,
&i.Action,
&i.Details,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}