mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 09:26:50 +08:00
* 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]>
207 lines
6.0 KiB
Go
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
|
|
}
|