Files
coder/coderd/database/queries.sql.go
T
Michael Suchacz 521c383f6b fix: repair stale chat agent bindings after workspace rebuild (#28152)
## Problem

When a chat is bound to a workspace, chatd persists `chats.agent_id`
pointing at a specific workspace agent, and it only rebinds on the next
chat turn. A workspace stop/start creates a new agent with a new ID in
the latest build, so the chat page resolves the stale agent ID to
`undefined` and the right sidebar silently drops Terminal, Desktop,
Browser, apps, and ports even though the workspace is running. The
existing read-time enrichment only filled nil agent IDs and skipped
stale non-nil ones, so refreshing did not help until the user sent
another message.

## Fix

- `coderd/exp_chats.go`: single-chat reads now repair agent IDs that no
longer resolve in the workspace's latest build, using the same
`agentselect.FindChatAgent` selection chatd uses. A repaired binding
also carries the latest build's ID so the response never pairs the new
agent with the previous build. Bindings that still resolve are
preserved, and repair stays best-effort and response-only (no
write-on-read). List reads keep the previous nil-fill-only behavior
because validating existing bindings would cost a per-workspace
authorization lookup per listed chat.
- `site/src/pages/AgentsPage/AgentChatPage.tsx`: the workspace watch
update handler detects when a running workspace's latest build no longer
contains the chat's bound agent and invalidates the chat query once per
chat/build/binding key for immediate recovery, and the chat query polls
every 30 seconds while the binding remains unresolved so a transiently
failed repair retries even when an idle workspace publishes no further
watch events. The watch stream replays the current workspace on every
(re)connect, so this covers rebuilds that happen while the page is open
or disconnected; page loads are covered by the server-side repair. The
workspace-watcher bailout now also keys on `latest_build.id` so a
rebuild propagates while the page is open.
- `site/src/api/queries/chats.ts`: chat watch events replay the
persisted (pre-repair) binding, so the summary merge adopts a snapshot's
`build_id` only when the snapshot agrees on `agent_id`, keeping the
repaired agent/build pair atomic in the caches.

## Testing

- `go test ./coderd -run TestEnrichChatAgentIDs` covering repair,
keep-valid, selection-error, list-mode-skips-bound, and no-workspaces
cases.
- Storybook interaction story `RecoversSidebarAfterWorkspaceRebuild`
exercising the watch-event to chat-refetch to sidebar-recovery flow
(verified red without the invalidation, green with it).
- `pnpm test AgentChatPage.test.ts` covering the binding-resolution
predicate.

> Mux created this PR on Mike's behalf.
2026-08-16 20:30:32 +02:00

40364 lines
1.3 MiB
Plaintext
Generated

// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
package database
import (
"context"
"database/sql"
"encoding/json"
"time"
"github.com/google/uuid"
"github.com/lib/pq"
"github.com/sqlc-dev/pqtype"
)
const activityBumpWorkspace = `-- name: ActivityBumpWorkspace :exec
WITH latest AS (
SELECT
workspace_builds.id::uuid AS build_id,
workspace_builds.deadline::timestamp with time zone AS build_deadline,
workspace_builds.max_deadline::timestamp with time zone AS build_max_deadline,
workspace_builds.transition AS build_transition,
provisioner_jobs.completed_at::timestamp with time zone AS job_completed_at,
templates.activity_bump AS activity_bump,
(
CASE
-- If the extension would push us over the next_autostart
-- interval, then extend the deadline by the full TTL (NOT
-- activity bump) from the autostart time. This will essentially
-- be as if the workspace auto started at the given time and the
-- original TTL was applied.
--
-- Sadly we can't define 'activity_bump_interval' above since
-- it won't be available for this CASE statement, so we have to
-- copy the cast twice.
WHEN NOW() + (templates.activity_bump / 1000 / 1000 / 1000 || ' seconds')::interval > $1 :: timestamptz
-- If the autostart is behind now(), then the
-- autostart schedule is either the 0 time and not provided,
-- or it was the autostart in the past, which is no longer
-- relevant. If autostart is > 0 and in the past, then
-- that is a mistake by the caller.
AND $1 > NOW()
THEN
-- Extend to the autostart, then add the activity bump
(($1 :: timestamptz) - NOW()) + CASE
WHEN templates.allow_user_autostop
THEN (workspaces.ttl / 1000 / 1000 / 1000 || ' seconds')::interval
ELSE (templates.default_ttl / 1000 / 1000 / 1000 || ' seconds')::interval
END
-- Default to the activity bump duration.
ELSE
(templates.activity_bump / 1000 / 1000 / 1000 || ' seconds')::interval
END
) AS ttl_interval
FROM workspace_builds
JOIN provisioner_jobs
ON provisioner_jobs.id = workspace_builds.job_id
JOIN workspaces
ON workspaces.id = workspace_builds.workspace_id
JOIN templates
ON templates.id = workspaces.template_id
WHERE
workspace_builds.workspace_id = $2::uuid
-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
-- are managed by the reconciliation loop and not subject to activity bumping
AND workspaces.owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID
ORDER BY workspace_builds.build_number DESC
LIMIT 1
)
UPDATE
workspace_builds wb
SET
updated_at = NOW(),
deadline = CASE
WHEN l.build_max_deadline = '0001-01-01 00:00:00+00'
-- Never reduce the deadline from activity.
THEN GREATEST(wb.deadline, NOW() + l.ttl_interval)
ELSE LEAST(GREATEST(wb.deadline, NOW() + l.ttl_interval), l.build_max_deadline)
END
FROM latest l
WHERE wb.id = l.build_id
AND l.job_completed_at IS NOT NULL
AND l.activity_bump > 0
AND l.build_transition = 'start'
AND l.ttl_interval > '0 seconds'::interval
AND l.build_deadline != '0001-01-01 00:00:00+00'
AND l.build_deadline - (l.ttl_interval * 0.95) < NOW()
`
type ActivityBumpWorkspaceParams struct {
NextAutostart time.Time `db:"next_autostart" json:"next_autostart"`
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
}
// Bumps the workspace deadline by the template's configured "activity_bump"
// duration (default 1h). If the workspace bump will cross an autostart
// threshold, then the bump is autostart + TTL. This is the deadline behavior if
// the workspace was to autostart from a stopped state.
//
// Max deadline is respected, and the deadline will never be bumped past it.
// The deadline will never decrease.
// We only bump if the template has an activity bump duration set.
// We only bump if the raw interval is positive and non-zero.
// We only bump if workspace shutdown is manual.
// We only bump when 5% of the deadline has elapsed.
func (q *sqlQuerier) ActivityBumpWorkspace(ctx context.Context, arg ActivityBumpWorkspaceParams) error {
_, err := q.db.ExecContext(ctx, activityBumpWorkspace, arg.NextAutostart, arg.WorkspaceID)
return err
}
const deleteAIGatewayKey = `-- name: DeleteAIGatewayKey :one
DELETE FROM ai_gateway_keys WHERE id = $1
RETURNING id, name, secret_prefix, created_at, last_heartbeat_at
`
type DeleteAIGatewayKeyRow struct {
ID uuid.UUID `db:"id" json:"id"`
Name string `db:"name" json:"name"`
SecretPrefix string `db:"secret_prefix" json:"secret_prefix"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
LastHeartbeatAt sql.NullTime `db:"last_heartbeat_at" json:"last_heartbeat_at"`
}
func (q *sqlQuerier) DeleteAIGatewayKey(ctx context.Context, id uuid.UUID) (DeleteAIGatewayKeyRow, error) {
row := q.db.QueryRowContext(ctx, deleteAIGatewayKey, id)
var i DeleteAIGatewayKeyRow
err := row.Scan(
&i.ID,
&i.Name,
&i.SecretPrefix,
&i.CreatedAt,
&i.LastHeartbeatAt,
)
return i, err
}
const getAIGatewayKeyByHashedSecret = `-- name: GetAIGatewayKeyByHashedSecret :one
SELECT id, created_at, name, secret_prefix, hashed_secret, last_heartbeat_at
FROM ai_gateway_keys
WHERE hashed_secret = $1
`
// Authenticates a standalone AI Gateway replica by its hashed key secret,
// returning the matched key. The lookup is an exact match on a unique index,
// so a returned row is itself proof the secret is valid.
func (q *sqlQuerier) GetAIGatewayKeyByHashedSecret(ctx context.Context, hashedSecret []byte) (AIGatewayKey, error) {
row := q.db.QueryRowContext(ctx, getAIGatewayKeyByHashedSecret, hashedSecret)
var i AIGatewayKey
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.Name,
&i.SecretPrefix,
&i.HashedSecret,
&i.LastHeartbeatAt,
)
return i, err
}
const insertAIGatewayKey = `-- name: InsertAIGatewayKey :one
INSERT INTO ai_gateway_keys (id, name, secret_prefix, hashed_secret, created_at)
VALUES ($1, $4, $2, $3, NOW())
RETURNING id, name, secret_prefix, created_at
`
type InsertAIGatewayKeyParams struct {
ID uuid.UUID `db:"id" json:"id"`
SecretPrefix string `db:"secret_prefix" json:"secret_prefix"`
HashedSecret []byte `db:"hashed_secret" json:"hashed_secret"`
Name string `db:"name" json:"name"`
}
type InsertAIGatewayKeyRow struct {
ID uuid.UUID `db:"id" json:"id"`
Name string `db:"name" json:"name"`
SecretPrefix string `db:"secret_prefix" json:"secret_prefix"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
func (q *sqlQuerier) InsertAIGatewayKey(ctx context.Context, arg InsertAIGatewayKeyParams) (InsertAIGatewayKeyRow, error) {
row := q.db.QueryRowContext(ctx, insertAIGatewayKey,
arg.ID,
arg.SecretPrefix,
arg.HashedSecret,
arg.Name,
)
var i InsertAIGatewayKeyRow
err := row.Scan(
&i.ID,
&i.Name,
&i.SecretPrefix,
&i.CreatedAt,
)
return i, err
}
const listAIGatewayKeys = `-- name: ListAIGatewayKeys :many
SELECT id, name, secret_prefix, created_at, last_heartbeat_at
FROM ai_gateway_keys
ORDER BY created_at ASC
`
type ListAIGatewayKeysRow struct {
ID uuid.UUID `db:"id" json:"id"`
Name string `db:"name" json:"name"`
SecretPrefix string `db:"secret_prefix" json:"secret_prefix"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
LastHeartbeatAt sql.NullTime `db:"last_heartbeat_at" json:"last_heartbeat_at"`
}
func (q *sqlQuerier) ListAIGatewayKeys(ctx context.Context) ([]ListAIGatewayKeysRow, error) {
rows, err := q.db.QueryContext(ctx, listAIGatewayKeys)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListAIGatewayKeysRow
for rows.Next() {
var i ListAIGatewayKeysRow
if err := rows.Scan(
&i.ID,
&i.Name,
&i.SecretPrefix,
&i.CreatedAt,
&i.LastHeartbeatAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateAIGatewayKeyLastHeartbeatAt = `-- name: UpdateAIGatewayKeyLastHeartbeatAt :execrows
UPDATE ai_gateway_keys
SET last_heartbeat_at = NOW()
WHERE id = $1
`
// Records heartbeat liveness for an active Gateway DRPC session. The database sets the
// timestamp so it stays consistent regardless of clock drift between API
// replicas.
func (q *sqlQuerier) UpdateAIGatewayKeyLastHeartbeatAt(ctx context.Context, id uuid.UUID) (int64, error) {
result, err := q.db.ExecContext(ctx, updateAIGatewayKeyLastHeartbeatAt, id)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const deleteAIProviderKey = `-- name: DeleteAIProviderKey :exec
DELETE FROM
ai_provider_keys
WHERE
id = $1::uuid
`
func (q *sqlQuerier) DeleteAIProviderKey(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteAIProviderKey, id)
return err
}
const getAIProviderKeyByID = `-- name: GetAIProviderKeyByID :one
SELECT
id, provider_id, api_key, api_key_key_id, created_at, updated_at
FROM
ai_provider_keys
WHERE
id = $1::uuid
`
func (q *sqlQuerier) GetAIProviderKeyByID(ctx context.Context, id uuid.UUID) (AIProviderKey, error) {
row := q.db.QueryRowContext(ctx, getAIProviderKeyByID, id)
var i AIProviderKey
err := row.Scan(
&i.ID,
&i.ProviderID,
&i.APIKey,
&i.ApiKeyKeyID,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getAIProviderKeyPresence = `-- name: GetAIProviderKeyPresence :many
SELECT DISTINCT
provider_id
FROM
ai_provider_keys
WHERE
provider_id = ANY($1::uuid[])
ORDER BY
provider_id ASC
`
// Returns the provider IDs that have at least one provider-scoped key.
func (q *sqlQuerier) GetAIProviderKeyPresence(ctx context.Context, providerIds []uuid.UUID) ([]uuid.UUID, error) {
rows, err := q.db.QueryContext(ctx, getAIProviderKeyPresence, pq.Array(providerIds))
if err != nil {
return nil, err
}
defer rows.Close()
var items []uuid.UUID
for rows.Next() {
var provider_id uuid.UUID
if err := rows.Scan(&provider_id); err != nil {
return nil, err
}
items = append(items, provider_id)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getAIProviderKeys = `-- name: GetAIProviderKeys :many
SELECT
ai_provider_keys.id, ai_provider_keys.provider_id, ai_provider_keys.api_key, ai_provider_keys.api_key_key_id, ai_provider_keys.created_at, ai_provider_keys.updated_at
FROM
ai_provider_keys
JOIN ai_providers ON ai_providers.id = ai_provider_keys.provider_id
WHERE
$1::boolean OR NOT ai_providers.deleted
ORDER BY
ai_provider_keys.provider_id ASC,
ai_provider_keys.created_at ASC,
ai_provider_keys.id ASC
`
// Returns AI provider key rows. By default, only rows whose parent
// provider is live (deleted = FALSE) are returned, so the API list
// handler can fetch every visible provider's keys in a single query.
// The dbcrypt key rotation utility passes include_deleted=TRUE to
// re-encrypt rows that belong to soft-deleted providers as well.
func (q *sqlQuerier) GetAIProviderKeys(ctx context.Context, includeDeleted bool) ([]AIProviderKey, error) {
rows, err := q.db.QueryContext(ctx, getAIProviderKeys, includeDeleted)
if err != nil {
return nil, err
}
defer rows.Close()
var items []AIProviderKey
for rows.Next() {
var i AIProviderKey
if err := rows.Scan(
&i.ID,
&i.ProviderID,
&i.APIKey,
&i.ApiKeyKeyID,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getAIProviderKeysByProviderID = `-- name: GetAIProviderKeysByProviderID :many
SELECT
id, provider_id, api_key, api_key_key_id, created_at, updated_at
FROM
ai_provider_keys
WHERE
provider_id = $1::uuid
ORDER BY
created_at ASC,
id ASC
`
// Returns all keys for a provider, ordered by created_at ASC so the
// oldest key is returned first. AI Bridge currently uses the oldest
// key per provider; multiple keys are stored to support future
// failover and rotation flows.
func (q *sqlQuerier) GetAIProviderKeysByProviderID(ctx context.Context, providerID uuid.UUID) ([]AIProviderKey, error) {
rows, err := q.db.QueryContext(ctx, getAIProviderKeysByProviderID, providerID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []AIProviderKey
for rows.Next() {
var i AIProviderKey
if err := rows.Scan(
&i.ID,
&i.ProviderID,
&i.APIKey,
&i.ApiKeyKeyID,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getAIProviderKeysByProviderIDs = `-- name: GetAIProviderKeysByProviderIDs :many
SELECT
id, provider_id, api_key, api_key_key_id, created_at, updated_at
FROM
ai_provider_keys
WHERE
provider_id = ANY($1::uuid[])
ORDER BY
provider_id ASC,
created_at ASC,
id ASC
`
// Returns all keys for the requested providers, ordered by provider then created_at ASC
// so callers can select the oldest non-empty key per provider without issuing N queries.
func (q *sqlQuerier) GetAIProviderKeysByProviderIDs(ctx context.Context, providerIds []uuid.UUID) ([]AIProviderKey, error) {
rows, err := q.db.QueryContext(ctx, getAIProviderKeysByProviderIDs, pq.Array(providerIds))
if err != nil {
return nil, err
}
defer rows.Close()
var items []AIProviderKey
for rows.Next() {
var i AIProviderKey
if err := rows.Scan(
&i.ID,
&i.ProviderID,
&i.APIKey,
&i.ApiKeyKeyID,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertAIProviderKey = `-- name: InsertAIProviderKey :one
INSERT INTO ai_provider_keys (
id,
provider_id,
api_key,
api_key_key_id,
created_at,
updated_at
) VALUES (
$1::uuid,
$2::uuid,
$3::text,
$4::text,
$5::timestamptz,
$6::timestamptz
)
RETURNING
id, provider_id, api_key, api_key_key_id, created_at, updated_at
`
type InsertAIProviderKeyParams struct {
ID uuid.UUID `db:"id" json:"id"`
ProviderID uuid.UUID `db:"provider_id" json:"provider_id"`
APIKey string `db:"api_key" json:"api_key"`
ApiKeyKeyID sql.NullString `db:"api_key_key_id" json:"api_key_key_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
func (q *sqlQuerier) InsertAIProviderKey(ctx context.Context, arg InsertAIProviderKeyParams) (AIProviderKey, error) {
row := q.db.QueryRowContext(ctx, insertAIProviderKey,
arg.ID,
arg.ProviderID,
arg.APIKey,
arg.ApiKeyKeyID,
arg.CreatedAt,
arg.UpdatedAt,
)
var i AIProviderKey
err := row.Scan(
&i.ID,
&i.ProviderID,
&i.APIKey,
&i.ApiKeyKeyID,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const updateEncryptedAIProviderKey = `-- name: UpdateEncryptedAIProviderKey :one
UPDATE
ai_provider_keys
SET
api_key = $1::text,
api_key_key_id = $2::text,
updated_at = NOW()
WHERE
id = $3::uuid
RETURNING
id, provider_id, api_key, api_key_key_id, created_at, updated_at
`
type UpdateEncryptedAIProviderKeyParams struct {
APIKey string `db:"api_key" json:"api_key"`
ApiKeyKeyID sql.NullString `db:"api_key_key_id" json:"api_key_key_id"`
ID uuid.UUID `db:"id" json:"id"`
}
// Updates only the encrypted columns (api_key, api_key_key_id) and
// the updated_at timestamp on a row. Used by the dbcrypt key
// rotation utility to re-encrypt or decrypt rows in place.
func (q *sqlQuerier) UpdateEncryptedAIProviderKey(ctx context.Context, arg UpdateEncryptedAIProviderKeyParams) (AIProviderKey, error) {
row := q.db.QueryRowContext(ctx, updateEncryptedAIProviderKey, arg.APIKey, arg.ApiKeyKeyID, arg.ID)
var i AIProviderKey
err := row.Scan(
&i.ID,
&i.ProviderID,
&i.APIKey,
&i.ApiKeyKeyID,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const deleteAIProviderByID = `-- name: DeleteAIProviderByID :exec
UPDATE
ai_providers
SET
deleted = TRUE,
enabled = FALSE,
updated_at = NOW()
WHERE
id = $1::uuid AND deleted = FALSE
`
func (q *sqlQuerier) DeleteAIProviderByID(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteAIProviderByID, id)
return err
}
const getAIProviderByID = `-- name: GetAIProviderByID :one
SELECT
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at, icon
FROM
ai_providers
WHERE
id = $1::uuid AND deleted = FALSE
`
func (q *sqlQuerier) GetAIProviderByID(ctx context.Context, id uuid.UUID) (AIProvider, error) {
row := q.db.QueryRowContext(ctx, getAIProviderByID, id)
var i AIProvider
err := row.Scan(
&i.ID,
&i.Type,
&i.Name,
&i.DisplayName,
&i.Enabled,
&i.Deleted,
&i.BaseUrl,
&i.Settings,
&i.SettingsKeyID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Icon,
)
return i, err
}
const getAIProviderByIDForReferenceLock = `-- name: GetAIProviderByIDForReferenceLock :one
SELECT
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at, icon
FROM
ai_providers
WHERE
id = $1::uuid AND deleted = FALSE
FOR SHARE
`
// Lock the provider row until the model-config write completes. The
// transaction alone does not stop a concurrent soft-delete or disable
// between validation and writing the model config reference.
func (q *sqlQuerier) GetAIProviderByIDForReferenceLock(ctx context.Context, id uuid.UUID) (AIProvider, error) {
row := q.db.QueryRowContext(ctx, getAIProviderByIDForReferenceLock, id)
var i AIProvider
err := row.Scan(
&i.ID,
&i.Type,
&i.Name,
&i.DisplayName,
&i.Enabled,
&i.Deleted,
&i.BaseUrl,
&i.Settings,
&i.SettingsKeyID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Icon,
)
return i, err
}
const getAIProviderByName = `-- name: GetAIProviderByName :one
SELECT
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at, icon
FROM
ai_providers
WHERE
name = $1::text AND deleted = FALSE
`
func (q *sqlQuerier) GetAIProviderByName(ctx context.Context, name string) (AIProvider, error) {
row := q.db.QueryRowContext(ctx, getAIProviderByName, name)
var i AIProvider
err := row.Scan(
&i.ID,
&i.Type,
&i.Name,
&i.DisplayName,
&i.Enabled,
&i.Deleted,
&i.BaseUrl,
&i.Settings,
&i.SettingsKeyID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Icon,
)
return i, err
}
const getAIProviders = `-- name: GetAIProviders :many
SELECT
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at, icon
FROM
ai_providers
WHERE
($1::boolean OR NOT deleted)
AND ($2::boolean OR enabled)
ORDER BY
name ASC
`
type GetAIProvidersParams struct {
IncludeDeleted bool `db:"include_deleted" json:"include_deleted"`
IncludeDisabled bool `db:"include_disabled" json:"include_disabled"`
}
// Returns AI provider rows. Soft-deleted and disabled rows are excluded
// unless include_deleted or include_disabled is set.
func (q *sqlQuerier) GetAIProviders(ctx context.Context, arg GetAIProvidersParams) ([]AIProvider, error) {
rows, err := q.db.QueryContext(ctx, getAIProviders, arg.IncludeDeleted, arg.IncludeDisabled)
if err != nil {
return nil, err
}
defer rows.Close()
var items []AIProvider
for rows.Next() {
var i AIProvider
if err := rows.Scan(
&i.ID,
&i.Type,
&i.Name,
&i.DisplayName,
&i.Enabled,
&i.Deleted,
&i.BaseUrl,
&i.Settings,
&i.SettingsKeyID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Icon,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertAIProvider = `-- name: InsertAIProvider :one
INSERT INTO ai_providers (
id,
type,
name,
display_name,
icon,
enabled,
base_url,
settings,
settings_key_id
) VALUES (
$1::uuid,
$2::ai_provider_type,
$3::text,
$4::text,
$5::text,
$6::boolean,
$7::text,
$8::text,
$9::text
)
RETURNING
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at, icon
`
type InsertAIProviderParams struct {
ID uuid.UUID `db:"id" json:"id"`
Type AIProviderType `db:"type" json:"type"`
Name string `db:"name" json:"name"`
DisplayName sql.NullString `db:"display_name" json:"display_name"`
Icon string `db:"icon" json:"icon"`
Enabled bool `db:"enabled" json:"enabled"`
BaseUrl string `db:"base_url" json:"base_url"`
Settings sql.NullString `db:"settings" json:"settings"`
SettingsKeyID sql.NullString `db:"settings_key_id" json:"settings_key_id"`
}
func (q *sqlQuerier) InsertAIProvider(ctx context.Context, arg InsertAIProviderParams) (AIProvider, error) {
row := q.db.QueryRowContext(ctx, insertAIProvider,
arg.ID,
arg.Type,
arg.Name,
arg.DisplayName,
arg.Icon,
arg.Enabled,
arg.BaseUrl,
arg.Settings,
arg.SettingsKeyID,
)
var i AIProvider
err := row.Scan(
&i.ID,
&i.Type,
&i.Name,
&i.DisplayName,
&i.Enabled,
&i.Deleted,
&i.BaseUrl,
&i.Settings,
&i.SettingsKeyID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Icon,
)
return i, err
}
const updateAIProvider = `-- name: UpdateAIProvider :one
UPDATE
ai_providers
SET
type = $1::ai_provider_type,
display_name = $2::text,
icon = $3::text,
enabled = $4::boolean,
base_url = $5::text,
settings = $6::text,
settings_key_id = $7::text,
updated_at = NOW()
WHERE
id = $8::uuid AND deleted = FALSE
RETURNING
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at, icon
`
type UpdateAIProviderParams struct {
Type AIProviderType `db:"type" json:"type"`
DisplayName sql.NullString `db:"display_name" json:"display_name"`
Icon string `db:"icon" json:"icon"`
Enabled bool `db:"enabled" json:"enabled"`
BaseUrl string `db:"base_url" json:"base_url"`
Settings sql.NullString `db:"settings" json:"settings"`
SettingsKeyID sql.NullString `db:"settings_key_id" json:"settings_key_id"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateAIProvider(ctx context.Context, arg UpdateAIProviderParams) (AIProvider, error) {
row := q.db.QueryRowContext(ctx, updateAIProvider,
arg.Type,
arg.DisplayName,
arg.Icon,
arg.Enabled,
arg.BaseUrl,
arg.Settings,
arg.SettingsKeyID,
arg.ID,
)
var i AIProvider
err := row.Scan(
&i.ID,
&i.Type,
&i.Name,
&i.DisplayName,
&i.Enabled,
&i.Deleted,
&i.BaseUrl,
&i.Settings,
&i.SettingsKeyID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Icon,
)
return i, err
}
const updateEncryptedAIProviderSettings = `-- name: UpdateEncryptedAIProviderSettings :one
UPDATE
ai_providers
SET
settings = $1::text,
settings_key_id = $2::text,
updated_at = NOW()
WHERE
id = $3::uuid
RETURNING
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at, icon
`
type UpdateEncryptedAIProviderSettingsParams struct {
Settings sql.NullString `db:"settings" json:"settings"`
SettingsKeyID sql.NullString `db:"settings_key_id" json:"settings_key_id"`
ID uuid.UUID `db:"id" json:"id"`
}
// Updates only the encrypted columns (settings, settings_key_id) and
// the updated_at timestamp on a row, regardless of its deleted flag.
// Used by the dbcrypt key rotation utility to re-encrypt or decrypt
// rows in place.
func (q *sqlQuerier) UpdateEncryptedAIProviderSettings(ctx context.Context, arg UpdateEncryptedAIProviderSettingsParams) (AIProvider, error) {
row := q.db.QueryRowContext(ctx, updateEncryptedAIProviderSettings, arg.Settings, arg.SettingsKeyID, arg.ID)
var i AIProvider
err := row.Scan(
&i.ID,
&i.Type,
&i.Name,
&i.DisplayName,
&i.Enabled,
&i.Deleted,
&i.BaseUrl,
&i.Settings,
&i.SettingsKeyID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Icon,
)
return i, err
}
const calculateAIBridgeInterceptionsTelemetrySummary = `-- name: CalculateAIBridgeInterceptionsTelemetrySummary :one
WITH interceptions_in_range AS (
-- Get all matching interceptions in the given timeframe.
SELECT
id,
initiator_id,
(ended_at - started_at) AS duration
FROM
aibridge_interceptions
WHERE
provider = $1::text
AND model = $2::text
AND COALESCE(client, 'Unknown') = $3::text
AND ended_at IS NOT NULL -- incomplete interceptions are not included in summaries
AND ended_at >= $4::timestamptz
AND ended_at < $5::timestamptz
),
interception_counts AS (
SELECT
COUNT(id) AS interception_count,
COUNT(DISTINCT initiator_id) AS unique_initiator_count
FROM
interceptions_in_range
),
duration_percentiles AS (
SELECT
(COALESCE(PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY EXTRACT(EPOCH FROM duration)), 0) * 1000)::bigint AS interception_duration_p50_millis,
(COALESCE(PERCENTILE_CONT(0.90) WITHIN GROUP (ORDER BY EXTRACT(EPOCH FROM duration)), 0) * 1000)::bigint AS interception_duration_p90_millis,
(COALESCE(PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY EXTRACT(EPOCH FROM duration)), 0) * 1000)::bigint AS interception_duration_p95_millis,
(COALESCE(PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY EXTRACT(EPOCH FROM duration)), 0) * 1000)::bigint AS interception_duration_p99_millis
FROM
interceptions_in_range
),
token_aggregates AS (
SELECT
COALESCE(SUM(tu.input_tokens), 0) AS token_count_input,
COALESCE(SUM(tu.output_tokens), 0) AS token_count_output,
COALESCE(SUM(tu.cache_read_input_tokens), 0) AS token_count_cached_read,
COALESCE(SUM(tu.cache_write_input_tokens), 0) AS token_count_cached_written,
COUNT(tu.id) AS token_usages_count
FROM
interceptions_in_range i
LEFT JOIN
aibridge_token_usages tu ON i.id = tu.interception_id
),
prompt_aggregates AS (
SELECT
COUNT(up.id) AS user_prompts_count
FROM
interceptions_in_range i
LEFT JOIN
aibridge_user_prompts up ON i.id = up.interception_id
),
tool_aggregates AS (
SELECT
COUNT(tu.id) FILTER (WHERE tu.injected = true) AS tool_calls_count_injected,
COUNT(tu.id) FILTER (WHERE tu.injected = false) AS tool_calls_count_non_injected,
COUNT(tu.id) FILTER (WHERE tu.injected = true AND tu.invocation_error IS NOT NULL) AS injected_tool_call_error_count
FROM
interceptions_in_range i
LEFT JOIN
aibridge_tool_usages tu ON i.id = tu.interception_id
)
SELECT
ic.interception_count::bigint AS interception_count,
dp.interception_duration_p50_millis::bigint AS interception_duration_p50_millis,
dp.interception_duration_p90_millis::bigint AS interception_duration_p90_millis,
dp.interception_duration_p95_millis::bigint AS interception_duration_p95_millis,
dp.interception_duration_p99_millis::bigint AS interception_duration_p99_millis,
ic.unique_initiator_count::bigint AS unique_initiator_count,
pa.user_prompts_count::bigint AS user_prompts_count,
tok_agg.token_usages_count::bigint AS token_usages_count,
tok_agg.token_count_input::bigint AS token_count_input,
tok_agg.token_count_output::bigint AS token_count_output,
tok_agg.token_count_cached_read::bigint AS token_count_cached_read,
tok_agg.token_count_cached_written::bigint AS token_count_cached_written,
tool_agg.tool_calls_count_injected::bigint AS tool_calls_count_injected,
tool_agg.tool_calls_count_non_injected::bigint AS tool_calls_count_non_injected,
tool_agg.injected_tool_call_error_count::bigint AS injected_tool_call_error_count
FROM
interception_counts ic,
duration_percentiles dp,
token_aggregates tok_agg,
prompt_aggregates pa,
tool_aggregates tool_agg
`
type CalculateAIBridgeInterceptionsTelemetrySummaryParams struct {
Provider string `db:"provider" json:"provider"`
Model string `db:"model" json:"model"`
Client string `db:"client" json:"client"`
EndedAtAfter time.Time `db:"ended_at_after" json:"ended_at_after"`
EndedAtBefore time.Time `db:"ended_at_before" json:"ended_at_before"`
}
type CalculateAIBridgeInterceptionsTelemetrySummaryRow struct {
InterceptionCount int64 `db:"interception_count" json:"interception_count"`
InterceptionDurationP50Millis int64 `db:"interception_duration_p50_millis" json:"interception_duration_p50_millis"`
InterceptionDurationP90Millis int64 `db:"interception_duration_p90_millis" json:"interception_duration_p90_millis"`
InterceptionDurationP95Millis int64 `db:"interception_duration_p95_millis" json:"interception_duration_p95_millis"`
InterceptionDurationP99Millis int64 `db:"interception_duration_p99_millis" json:"interception_duration_p99_millis"`
UniqueInitiatorCount int64 `db:"unique_initiator_count" json:"unique_initiator_count"`
UserPromptsCount int64 `db:"user_prompts_count" json:"user_prompts_count"`
TokenUsagesCount int64 `db:"token_usages_count" json:"token_usages_count"`
TokenCountInput int64 `db:"token_count_input" json:"token_count_input"`
TokenCountOutput int64 `db:"token_count_output" json:"token_count_output"`
TokenCountCachedRead int64 `db:"token_count_cached_read" json:"token_count_cached_read"`
TokenCountCachedWritten int64 `db:"token_count_cached_written" json:"token_count_cached_written"`
ToolCallsCountInjected int64 `db:"tool_calls_count_injected" json:"tool_calls_count_injected"`
ToolCallsCountNonInjected int64 `db:"tool_calls_count_non_injected" json:"tool_calls_count_non_injected"`
InjectedToolCallErrorCount int64 `db:"injected_tool_call_error_count" json:"injected_tool_call_error_count"`
}
// Calculates the telemetry summary for a given provider, model, and client
// combination for telemetry reporting.
func (q *sqlQuerier) CalculateAIBridgeInterceptionsTelemetrySummary(ctx context.Context, arg CalculateAIBridgeInterceptionsTelemetrySummaryParams) (CalculateAIBridgeInterceptionsTelemetrySummaryRow, error) {
row := q.db.QueryRowContext(ctx, calculateAIBridgeInterceptionsTelemetrySummary,
arg.Provider,
arg.Model,
arg.Client,
arg.EndedAtAfter,
arg.EndedAtBefore,
)
var i CalculateAIBridgeInterceptionsTelemetrySummaryRow
err := row.Scan(
&i.InterceptionCount,
&i.InterceptionDurationP50Millis,
&i.InterceptionDurationP90Millis,
&i.InterceptionDurationP95Millis,
&i.InterceptionDurationP99Millis,
&i.UniqueInitiatorCount,
&i.UserPromptsCount,
&i.TokenUsagesCount,
&i.TokenCountInput,
&i.TokenCountOutput,
&i.TokenCountCachedRead,
&i.TokenCountCachedWritten,
&i.ToolCallsCountInjected,
&i.ToolCallsCountNonInjected,
&i.InjectedToolCallErrorCount,
)
return i, err
}
const countAIBridgeSessions = `-- name: CountAIBridgeSessions :one
SELECT
COUNT(DISTINCT (aibridge_interceptions.session_id, aibridge_interceptions.initiator_id))
FROM
aibridge_interceptions
WHERE
-- Remove inflight interceptions (ones which lack an ended_at value).
aibridge_interceptions.ended_at IS NOT NULL
-- Filter by time frame
AND CASE
WHEN $1::timestamptz != '0001-01-01 00:00:00+00'::timestamptz THEN aibridge_interceptions.started_at >= $1::timestamptz
ELSE true
END
AND CASE
WHEN $2::timestamptz != '0001-01-01 00:00:00+00'::timestamptz THEN aibridge_interceptions.started_at <= $2::timestamptz
ELSE true
END
-- Filter initiator_id
AND CASE
WHEN $3::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN aibridge_interceptions.initiator_id = $3::uuid
ELSE true
END
-- Filter provider
AND CASE
WHEN $4::text != '' THEN aibridge_interceptions.provider = $4::text
ELSE true
END
-- Filter provider_name
AND CASE
WHEN $5::text != '' THEN aibridge_interceptions.provider_name = $5::text
ELSE true
END
-- Filter model
AND CASE
WHEN $6::text != '' THEN aibridge_interceptions.model = $6::text
ELSE true
END
-- Filter client
AND CASE
WHEN $7::text != '' THEN COALESCE(aibridge_interceptions.client, 'Unknown') = $7::text
ELSE true
END
-- Filter session_id
AND CASE
WHEN $8::text != '' THEN aibridge_interceptions.session_id = $8::text
ELSE true
END
-- Authorize Filter clause will be injected below in CountAuthorizedAIBridgeSessions
-- @authorize_filter
`
type CountAIBridgeSessionsParams struct {
StartedAfter time.Time `db:"started_after" json:"started_after"`
StartedBefore time.Time `db:"started_before" json:"started_before"`
InitiatorID uuid.UUID `db:"initiator_id" json:"initiator_id"`
Provider string `db:"provider" json:"provider"`
ProviderName string `db:"provider_name" json:"provider_name"`
Model string `db:"model" json:"model"`
Client string `db:"client" json:"client"`
SessionID string `db:"session_id" json:"session_id"`
}
func (q *sqlQuerier) CountAIBridgeSessions(ctx context.Context, arg CountAIBridgeSessionsParams) (int64, error) {
row := q.db.QueryRowContext(ctx, countAIBridgeSessions,
arg.StartedAfter,
arg.StartedBefore,
arg.InitiatorID,
arg.Provider,
arg.ProviderName,
arg.Model,
arg.Client,
arg.SessionID,
)
var count int64
err := row.Scan(&count)
return count, err
}
const deleteOldAIBridgeRecords = `-- name: DeleteOldAIBridgeRecords :one
WITH
-- We don't have FK relationships between the dependent tables and aibridge_interceptions, so we can't rely on DELETE CASCADE.
to_delete AS (
SELECT id FROM aibridge_interceptions
WHERE started_at < $1::timestamp with time zone
),
-- CTEs are executed in order.
model_thoughts AS (
DELETE FROM aibridge_model_thoughts
WHERE interception_id IN (SELECT id FROM to_delete)
RETURNING 1
),
tool_usages AS (
DELETE FROM aibridge_tool_usages
WHERE interception_id IN (SELECT id FROM to_delete)
RETURNING 1
),
token_usages AS (
DELETE FROM aibridge_token_usages
WHERE interception_id IN (SELECT id FROM to_delete)
RETURNING 1
),
user_prompts AS (
DELETE FROM aibridge_user_prompts
WHERE interception_id IN (SELECT id FROM to_delete)
RETURNING 1
),
interceptions AS (
DELETE FROM aibridge_interceptions
WHERE id IN (SELECT id FROM to_delete)
RETURNING 1
)
SELECT (
(SELECT COUNT(*) FROM model_thoughts) +
(SELECT COUNT(*) FROM tool_usages) +
(SELECT COUNT(*) FROM token_usages) +
(SELECT COUNT(*) FROM user_prompts) +
(SELECT COUNT(*) FROM interceptions)
)::bigint as total_deleted
`
// Cumulative count.
func (q *sqlQuerier) DeleteOldAIBridgeRecords(ctx context.Context, beforeTime time.Time) (int64, error) {
row := q.db.QueryRowContext(ctx, deleteOldAIBridgeRecords, beforeTime)
var total_deleted int64
err := row.Scan(&total_deleted)
return total_deleted, err
}
const getAIBridgeChatCost = `-- name: GetAIBridgeChatCost :one
WITH per_request AS (
-- One row per interception. A request records one token usage per provider
-- response, so aggregating here keeps the outer counts per request and
-- flags a request whose cost is partial because some usage was unpriced.
-- The usage join is a LEFT JOIN so a request that ended without eligible
-- usage, such as one that failed upstream, still counts as a request. The
-- tu.id guard keeps that row from reading as unpriced usage, since the
-- unmatched side is all NULL.
SELECT
SUM(tu.cost_micros) AS cost_micros,
BOOL_OR(tu.id IS NOT NULL AND tu.cost_micros IS NULL) AS has_unpriced_usage
FROM aibridge_interceptions i
JOIN chats c ON c.id::text = i.session_id AND c.owner_id = i.initiator_id
LEFT JOIN aibridge_token_usages tu ON tu.interception_id = i.id AND tu.effective_group_id IS NOT NULL
WHERE (
-- Spelled out instead of COALESCE(c.root_chat_id, c.id) so each branch
-- stays a plain comparison against an indexed column.
c.root_chat_id = $1::uuid
OR (c.root_chat_id IS NULL AND c.id = $1::uuid)
)
-- Restrict to aibridge.ClientCoderAgents so another client's session
-- reference cannot match a chat ID.
AND i.client = 'Coder Agents'
AND i.ended_at IS NOT NULL
GROUP BY i.id
)
SELECT
COALESCE(SUM(cost_micros), 0)::bigint AS total_cost_micros,
COUNT(*)::bigint AS request_count,
COUNT(*) FILTER (WHERE has_unpriced_usage)::bigint AS unpriced_request_count
FROM per_request
`
type GetAIBridgeChatCostRow struct {
TotalCostMicros int64 `db:"total_cost_micros" json:"total_cost_micros"`
RequestCount int64 `db:"request_count" json:"request_count"`
UnpricedRequestCount int64 `db:"unpriced_request_count" json:"unpriced_request_count"`
}
// AI Gateway cost for one chat tree: the root chat plus every subagent
// beneath it. The spawning chat's ID is recorded as the interception session
// ID (see chatprovider.CoderHeaders), so a subagent's requests are attributed
// to its parent rather than the root, and only whole trees can be summed. The
// owner check guards against session-id collisions. Usage without an
// effective group never reaches ai_user_daily_spend.
func (q *sqlQuerier) GetAIBridgeChatCost(ctx context.Context, rootChatID uuid.UUID) (GetAIBridgeChatCostRow, error) {
row := q.db.QueryRowContext(ctx, getAIBridgeChatCost, rootChatID)
var i GetAIBridgeChatCostRow
err := row.Scan(&i.TotalCostMicros, &i.RequestCount, &i.UnpricedRequestCount)
return i, err
}
const getAIBridgeInterceptionByID = `-- name: GetAIBridgeInterceptionByID :one
SELECT
id, initiator_id, provider, model, started_at, metadata, ended_at, api_key_id, client, thread_parent_id, thread_root_id, client_session_id, session_id, provider_name, credential_kind, credential_hint, agent_firewall_session_id, agent_firewall_sequence_number, error_type, error_message
FROM
aibridge_interceptions
WHERE
id = $1::uuid
`
func (q *sqlQuerier) GetAIBridgeInterceptionByID(ctx context.Context, id uuid.UUID) (AIBridgeInterception, error) {
row := q.db.QueryRowContext(ctx, getAIBridgeInterceptionByID, id)
var i AIBridgeInterception
err := row.Scan(
&i.ID,
&i.InitiatorID,
&i.Provider,
&i.Model,
&i.StartedAt,
&i.Metadata,
&i.EndedAt,
&i.APIKeyID,
&i.Client,
&i.ThreadParentID,
&i.ThreadRootID,
&i.ClientSessionID,
&i.SessionID,
&i.ProviderName,
&i.CredentialKind,
&i.CredentialHint,
&i.AgentFirewallSessionID,
&i.AgentFirewallSequenceNumber,
&i.ErrorType,
&i.ErrorMessage,
)
return i, err
}
const getAIBridgeInterceptionLineageByToolCallID = `-- name: GetAIBridgeInterceptionLineageByToolCallID :one
SELECT aibridge_interceptions.id AS thread_parent_id,
COALESCE(aibridge_interceptions.thread_root_id, aibridge_interceptions.id) AS thread_root_id
FROM aibridge_interceptions
WHERE aibridge_interceptions.id = (
SELECT interception_id FROM aibridge_tool_usages
WHERE provider_tool_call_id = $1::text
ORDER BY created_at DESC
LIMIT 1
)
`
type GetAIBridgeInterceptionLineageByToolCallIDRow struct {
ThreadParentID uuid.UUID `db:"thread_parent_id" json:"thread_parent_id"`
ThreadRootID uuid.UUID `db:"thread_root_id" json:"thread_root_id"`
}
// Look up the parent interception and the root of the thread by finding
// which interception recorded a tool usage with the given tool call ID.
// COALESCE ensures that if the parent has no thread_root_id (i.e. it IS
// the root), we return its own ID as the root.
func (q *sqlQuerier) GetAIBridgeInterceptionLineageByToolCallID(ctx context.Context, toolCallID string) (GetAIBridgeInterceptionLineageByToolCallIDRow, error) {
row := q.db.QueryRowContext(ctx, getAIBridgeInterceptionLineageByToolCallID, toolCallID)
var i GetAIBridgeInterceptionLineageByToolCallIDRow
err := row.Scan(&i.ThreadParentID, &i.ThreadRootID)
return i, err
}
const getAIBridgeInterceptions = `-- name: GetAIBridgeInterceptions :many
SELECT
id, initiator_id, provider, model, started_at, metadata, ended_at, api_key_id, client, thread_parent_id, thread_root_id, client_session_id, session_id, provider_name, credential_kind, credential_hint, agent_firewall_session_id, agent_firewall_sequence_number, error_type, error_message
FROM
aibridge_interceptions
`
func (q *sqlQuerier) GetAIBridgeInterceptions(ctx context.Context) ([]AIBridgeInterception, error) {
rows, err := q.db.QueryContext(ctx, getAIBridgeInterceptions)
if err != nil {
return nil, err
}
defer rows.Close()
var items []AIBridgeInterception
for rows.Next() {
var i AIBridgeInterception
if err := rows.Scan(
&i.ID,
&i.InitiatorID,
&i.Provider,
&i.Model,
&i.StartedAt,
&i.Metadata,
&i.EndedAt,
&i.APIKeyID,
&i.Client,
&i.ThreadParentID,
&i.ThreadRootID,
&i.ClientSessionID,
&i.SessionID,
&i.ProviderName,
&i.CredentialKind,
&i.CredentialHint,
&i.AgentFirewallSessionID,
&i.AgentFirewallSequenceNumber,
&i.ErrorType,
&i.ErrorMessage,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getAIBridgeSessionTopDomains = `-- name: GetAIBridgeSessionTopDomains :many
WITH session_boundary_logs AS (
SELECT bl.detail
FROM aibridge_interceptions afi
LEFT JOIN LATERAL (
SELECT COALESCE(MIN(nxt.agent_firewall_sequence_number), 2147483647) AS next_seq
FROM aibridge_interceptions nxt
WHERE nxt.agent_firewall_session_id = afi.agent_firewall_session_id
AND nxt.agent_firewall_sequence_number > afi.agent_firewall_sequence_number
) w ON true
JOIN boundary_logs bl
ON bl.session_id = afi.agent_firewall_session_id
AND bl.sequence_number > afi.agent_firewall_sequence_number
AND bl.sequence_number < w.next_seq
WHERE afi.session_id = $2::text
AND afi.ended_at IS NOT NULL
AND afi.agent_firewall_session_id IS NOT NULL
AND afi.agent_firewall_sequence_number IS NOT NULL
AND bl.proto = 'http'
),
extracted AS (
-- Strip an optional scheme, then keep the host up to the first port, path,
-- query, or fragment delimiter. This assumes HTTP egress detail is a plain
-- scheme+host(+port) URL: it does not handle userinfo (user@host, which
-- would be captured into the host) or IPv6 literal hosts ([::1], where the
-- leading '[' is captured and the ':' terminates early). Boundary HTTP logs
-- do not currently emit those forms; revisit this extraction if they do.
SELECT substring(detail from '^(?:[A-Za-z][A-Za-z0-9+.-]*://)?([^/:?#]+)') AS domain
FROM session_boundary_logs
),
domains AS (
SELECT domain, COUNT(*)::bigint AS count
FROM extracted
WHERE domain IS NOT NULL AND domain != ''
GROUP BY domain
)
SELECT
-- COALESCE keeps sqlc from typing the grouped column as nullable; the
-- domains CTE already filters out NULL/empty hosts.
COALESCE(domain, '')::text AS domain,
count,
COUNT(*) OVER ()::bigint AS total_domains
FROM domains
ORDER BY count DESC, domain ASC
LIMIT COALESCE(NULLIF($1::integer, 0), 5)
`
type GetAIBridgeSessionTopDomainsParams struct {
Limit int32 `db:"limit_" json:"limit_"`
SessionID string `db:"session_id" json:"session_id"`
}
type GetAIBridgeSessionTopDomainsRow struct {
Domain string `db:"domain" json:"domain"`
Count int64 `db:"count" json:"count"`
TotalDomains int64 `db:"total_domains" json:"total_domains"`
}
// Returns the most contacted destination hosts for an AI session, ordered by
// call count descending and limited to the top @limit_ rows. total_domains is
// the number of distinct domains across the whole session, used to render a
// "+N more" overflow beyond the returned rows. Only HTTP egress is considered;
// dns/git/fs boundary logs do not carry a domain in the same shape.
//
// Windowing mirrors the network_calls aggregation in ListAIBridgeSessions:
// each interception's boundary logs fall in the open interval (this seq, next
// interception's seq) within the same firewall session. The exclusive lower
// bound drops the interception's own LLM-provider call. next_seq considers all
// interceptions in the firewall session so windows never bleed across AI
// sessions that share one firewall session, and falls back to the maximum
// sequence_number for the last interception so the window stays an
// index-satisfiable range.
func (q *sqlQuerier) GetAIBridgeSessionTopDomains(ctx context.Context, arg GetAIBridgeSessionTopDomainsParams) ([]GetAIBridgeSessionTopDomainsRow, error) {
rows, err := q.db.QueryContext(ctx, getAIBridgeSessionTopDomains, arg.Limit, arg.SessionID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetAIBridgeSessionTopDomainsRow
for rows.Next() {
var i GetAIBridgeSessionTopDomainsRow
if err := rows.Scan(&i.Domain, &i.Count, &i.TotalDomains); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getAIBridgeTokenUsagesByInterceptionID = `-- name: GetAIBridgeTokenUsagesByInterceptionID :many
SELECT
id, interception_id, provider_response_id, input_tokens, output_tokens, metadata, created_at, cache_read_input_tokens, cache_write_input_tokens, effective_group_id, input_price_micros, output_price_micros, cache_read_price_micros, cache_write_price_micros, cost_micros
FROM
aibridge_token_usages WHERE interception_id = $1::uuid
ORDER BY
created_at ASC,
id ASC
`
func (q *sqlQuerier) GetAIBridgeTokenUsagesByInterceptionID(ctx context.Context, interceptionID uuid.UUID) ([]AIBridgeTokenUsage, error) {
rows, err := q.db.QueryContext(ctx, getAIBridgeTokenUsagesByInterceptionID, interceptionID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []AIBridgeTokenUsage
for rows.Next() {
var i AIBridgeTokenUsage
if err := rows.Scan(
&i.ID,
&i.InterceptionID,
&i.ProviderResponseID,
&i.InputTokens,
&i.OutputTokens,
&i.Metadata,
&i.CreatedAt,
&i.CacheReadInputTokens,
&i.CacheWriteInputTokens,
&i.EffectiveGroupID,
&i.InputPriceMicros,
&i.OutputPriceMicros,
&i.CacheReadPriceMicros,
&i.CacheWritePriceMicros,
&i.CostMicros,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getAIBridgeToolUsagesByInterceptionID = `-- name: GetAIBridgeToolUsagesByInterceptionID :many
SELECT
id, interception_id, provider_response_id, server_url, tool, input, injected, invocation_error, metadata, created_at, provider_tool_call_id, provider_item_id
FROM
aibridge_tool_usages
WHERE
interception_id = $1::uuid
ORDER BY
created_at ASC,
id ASC
`
func (q *sqlQuerier) GetAIBridgeToolUsagesByInterceptionID(ctx context.Context, interceptionID uuid.UUID) ([]AIBridgeToolUsage, error) {
rows, err := q.db.QueryContext(ctx, getAIBridgeToolUsagesByInterceptionID, interceptionID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []AIBridgeToolUsage
for rows.Next() {
var i AIBridgeToolUsage
if err := rows.Scan(
&i.ID,
&i.InterceptionID,
&i.ProviderResponseID,
&i.ServerUrl,
&i.Tool,
&i.Input,
&i.Injected,
&i.InvocationError,
&i.Metadata,
&i.CreatedAt,
&i.ProviderToolCallID,
&i.ProviderItemID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getAIBridgeUserPromptsByInterceptionID = `-- name: GetAIBridgeUserPromptsByInterceptionID :many
SELECT
id, interception_id, provider_response_id, prompt, metadata, created_at
FROM
aibridge_user_prompts
WHERE
interception_id = $1::uuid
ORDER BY
created_at ASC,
id ASC
`
func (q *sqlQuerier) GetAIBridgeUserPromptsByInterceptionID(ctx context.Context, interceptionID uuid.UUID) ([]AIBridgeUserPrompt, error) {
rows, err := q.db.QueryContext(ctx, getAIBridgeUserPromptsByInterceptionID, interceptionID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []AIBridgeUserPrompt
for rows.Next() {
var i AIBridgeUserPrompt
if err := rows.Scan(
&i.ID,
&i.InterceptionID,
&i.ProviderResponseID,
&i.Prompt,
&i.Metadata,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertAIBridgeInterception = `-- name: InsertAIBridgeInterception :one
INSERT INTO aibridge_interceptions (
id, api_key_id, initiator_id, provider, provider_name, model, metadata, started_at, client, client_session_id, thread_parent_id, thread_root_id, credential_kind, credential_hint, agent_firewall_session_id, agent_firewall_sequence_number
) VALUES (
$1, $2, $3, $4, $5, $6, COALESCE($7::jsonb, '{}'::jsonb), $8, $9, $10, $11::uuid, $12::uuid, $13, $14, $15::uuid, $16
)
RETURNING id, initiator_id, provider, model, started_at, metadata, ended_at, api_key_id, client, thread_parent_id, thread_root_id, client_session_id, session_id, provider_name, credential_kind, credential_hint, agent_firewall_session_id, agent_firewall_sequence_number, error_type, error_message
`
type InsertAIBridgeInterceptionParams struct {
ID uuid.UUID `db:"id" json:"id"`
APIKeyID sql.NullString `db:"api_key_id" json:"api_key_id"`
InitiatorID uuid.UUID `db:"initiator_id" json:"initiator_id"`
Provider string `db:"provider" json:"provider"`
ProviderName string `db:"provider_name" json:"provider_name"`
Model string `db:"model" json:"model"`
Metadata json.RawMessage `db:"metadata" json:"metadata"`
StartedAt time.Time `db:"started_at" json:"started_at"`
Client sql.NullString `db:"client" json:"client"`
ClientSessionID sql.NullString `db:"client_session_id" json:"client_session_id"`
ThreadParentInterceptionID uuid.NullUUID `db:"thread_parent_interception_id" json:"thread_parent_interception_id"`
ThreadRootInterceptionID uuid.NullUUID `db:"thread_root_interception_id" json:"thread_root_interception_id"`
CredentialKind CredentialKind `db:"credential_kind" json:"credential_kind"`
CredentialHint string `db:"credential_hint" json:"credential_hint"`
AgentFirewallSessionID uuid.NullUUID `db:"agent_firewall_session_id" json:"agent_firewall_session_id"`
AgentFirewallSequenceNumber sql.NullInt32 `db:"agent_firewall_sequence_number" json:"agent_firewall_sequence_number"`
}
func (q *sqlQuerier) InsertAIBridgeInterception(ctx context.Context, arg InsertAIBridgeInterceptionParams) (AIBridgeInterception, error) {
row := q.db.QueryRowContext(ctx, insertAIBridgeInterception,
arg.ID,
arg.APIKeyID,
arg.InitiatorID,
arg.Provider,
arg.ProviderName,
arg.Model,
arg.Metadata,
arg.StartedAt,
arg.Client,
arg.ClientSessionID,
arg.ThreadParentInterceptionID,
arg.ThreadRootInterceptionID,
arg.CredentialKind,
arg.CredentialHint,
arg.AgentFirewallSessionID,
arg.AgentFirewallSequenceNumber,
)
var i AIBridgeInterception
err := row.Scan(
&i.ID,
&i.InitiatorID,
&i.Provider,
&i.Model,
&i.StartedAt,
&i.Metadata,
&i.EndedAt,
&i.APIKeyID,
&i.Client,
&i.ThreadParentID,
&i.ThreadRootID,
&i.ClientSessionID,
&i.SessionID,
&i.ProviderName,
&i.CredentialKind,
&i.CredentialHint,
&i.AgentFirewallSessionID,
&i.AgentFirewallSequenceNumber,
&i.ErrorType,
&i.ErrorMessage,
)
return i, err
}
const insertAIBridgeModelThought = `-- name: InsertAIBridgeModelThought :one
INSERT INTO aibridge_model_thoughts (
interception_id, content, metadata, created_at
) VALUES (
$1, $2, COALESCE($3::jsonb, '{}'::jsonb), $4
)
RETURNING interception_id, content, metadata, created_at
`
type InsertAIBridgeModelThoughtParams struct {
InterceptionID uuid.UUID `db:"interception_id" json:"interception_id"`
Content string `db:"content" json:"content"`
Metadata json.RawMessage `db:"metadata" json:"metadata"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
func (q *sqlQuerier) InsertAIBridgeModelThought(ctx context.Context, arg InsertAIBridgeModelThoughtParams) (AIBridgeModelThought, error) {
row := q.db.QueryRowContext(ctx, insertAIBridgeModelThought,
arg.InterceptionID,
arg.Content,
arg.Metadata,
arg.CreatedAt,
)
var i AIBridgeModelThought
err := row.Scan(
&i.InterceptionID,
&i.Content,
&i.Metadata,
&i.CreatedAt,
)
return i, err
}
const insertAIBridgeTokenUsage = `-- name: InsertAIBridgeTokenUsage :one
INSERT INTO aibridge_token_usages (
id, interception_id, provider_response_id, input_tokens, output_tokens, cache_read_input_tokens, cache_write_input_tokens, metadata, created_at,
effective_group_id, input_price_micros, output_price_micros, cache_read_price_micros, cache_write_price_micros, cost_micros
) VALUES (
$1, $2, $3, $4, $5, $6, $7, COALESCE($8::jsonb, '{}'::jsonb), $9,
$10, $11, $12, $13, $14, $15
)
RETURNING id, interception_id, provider_response_id, input_tokens, output_tokens, metadata, created_at, cache_read_input_tokens, cache_write_input_tokens, effective_group_id, input_price_micros, output_price_micros, cache_read_price_micros, cache_write_price_micros, cost_micros
`
type InsertAIBridgeTokenUsageParams struct {
ID uuid.UUID `db:"id" json:"id"`
InterceptionID uuid.UUID `db:"interception_id" json:"interception_id"`
ProviderResponseID string `db:"provider_response_id" json:"provider_response_id"`
InputTokens int64 `db:"input_tokens" json:"input_tokens"`
OutputTokens int64 `db:"output_tokens" json:"output_tokens"`
CacheReadInputTokens int64 `db:"cache_read_input_tokens" json:"cache_read_input_tokens"`
CacheWriteInputTokens int64 `db:"cache_write_input_tokens" json:"cache_write_input_tokens"`
Metadata json.RawMessage `db:"metadata" json:"metadata"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
EffectiveGroupID uuid.NullUUID `db:"effective_group_id" json:"effective_group_id"`
InputPriceMicros sql.NullInt64 `db:"input_price_micros" json:"input_price_micros"`
OutputPriceMicros sql.NullInt64 `db:"output_price_micros" json:"output_price_micros"`
CacheReadPriceMicros sql.NullInt64 `db:"cache_read_price_micros" json:"cache_read_price_micros"`
CacheWritePriceMicros sql.NullInt64 `db:"cache_write_price_micros" json:"cache_write_price_micros"`
CostMicros sql.NullInt64 `db:"cost_micros" json:"cost_micros"`
}
func (q *sqlQuerier) InsertAIBridgeTokenUsage(ctx context.Context, arg InsertAIBridgeTokenUsageParams) (AIBridgeTokenUsage, error) {
row := q.db.QueryRowContext(ctx, insertAIBridgeTokenUsage,
arg.ID,
arg.InterceptionID,
arg.ProviderResponseID,
arg.InputTokens,
arg.OutputTokens,
arg.CacheReadInputTokens,
arg.CacheWriteInputTokens,
arg.Metadata,
arg.CreatedAt,
arg.EffectiveGroupID,
arg.InputPriceMicros,
arg.OutputPriceMicros,
arg.CacheReadPriceMicros,
arg.CacheWritePriceMicros,
arg.CostMicros,
)
var i AIBridgeTokenUsage
err := row.Scan(
&i.ID,
&i.InterceptionID,
&i.ProviderResponseID,
&i.InputTokens,
&i.OutputTokens,
&i.Metadata,
&i.CreatedAt,
&i.CacheReadInputTokens,
&i.CacheWriteInputTokens,
&i.EffectiveGroupID,
&i.InputPriceMicros,
&i.OutputPriceMicros,
&i.CacheReadPriceMicros,
&i.CacheWritePriceMicros,
&i.CostMicros,
)
return i, err
}
const insertAIBridgeToolUsage = `-- name: InsertAIBridgeToolUsage :one
INSERT INTO aibridge_tool_usages (
id, interception_id, provider_response_id, provider_tool_call_id, provider_item_id, tool, server_url, input, injected, invocation_error, metadata, created_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, COALESCE($11::jsonb, '{}'::jsonb), $12
)
RETURNING id, interception_id, provider_response_id, server_url, tool, input, injected, invocation_error, metadata, created_at, provider_tool_call_id, provider_item_id
`
type InsertAIBridgeToolUsageParams struct {
ID uuid.UUID `db:"id" json:"id"`
InterceptionID uuid.UUID `db:"interception_id" json:"interception_id"`
ProviderResponseID string `db:"provider_response_id" json:"provider_response_id"`
ProviderToolCallID sql.NullString `db:"provider_tool_call_id" json:"provider_tool_call_id"`
ProviderItemID sql.NullString `db:"provider_item_id" json:"provider_item_id"`
Tool string `db:"tool" json:"tool"`
ServerUrl sql.NullString `db:"server_url" json:"server_url"`
Input string `db:"input" json:"input"`
Injected bool `db:"injected" json:"injected"`
InvocationError sql.NullString `db:"invocation_error" json:"invocation_error"`
Metadata json.RawMessage `db:"metadata" json:"metadata"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
func (q *sqlQuerier) InsertAIBridgeToolUsage(ctx context.Context, arg InsertAIBridgeToolUsageParams) (AIBridgeToolUsage, error) {
row := q.db.QueryRowContext(ctx, insertAIBridgeToolUsage,
arg.ID,
arg.InterceptionID,
arg.ProviderResponseID,
arg.ProviderToolCallID,
arg.ProviderItemID,
arg.Tool,
arg.ServerUrl,
arg.Input,
arg.Injected,
arg.InvocationError,
arg.Metadata,
arg.CreatedAt,
)
var i AIBridgeToolUsage
err := row.Scan(
&i.ID,
&i.InterceptionID,
&i.ProviderResponseID,
&i.ServerUrl,
&i.Tool,
&i.Input,
&i.Injected,
&i.InvocationError,
&i.Metadata,
&i.CreatedAt,
&i.ProviderToolCallID,
&i.ProviderItemID,
)
return i, err
}
const insertAIBridgeUserPrompt = `-- name: InsertAIBridgeUserPrompt :one
INSERT INTO aibridge_user_prompts (
id, interception_id, provider_response_id, prompt, metadata, created_at
) VALUES (
$1, $2, $3, $4, COALESCE($5::jsonb, '{}'::jsonb), $6
)
RETURNING id, interception_id, provider_response_id, prompt, metadata, created_at
`
type InsertAIBridgeUserPromptParams struct {
ID uuid.UUID `db:"id" json:"id"`
InterceptionID uuid.UUID `db:"interception_id" json:"interception_id"`
ProviderResponseID string `db:"provider_response_id" json:"provider_response_id"`
Prompt string `db:"prompt" json:"prompt"`
Metadata json.RawMessage `db:"metadata" json:"metadata"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
func (q *sqlQuerier) InsertAIBridgeUserPrompt(ctx context.Context, arg InsertAIBridgeUserPromptParams) (AIBridgeUserPrompt, error) {
row := q.db.QueryRowContext(ctx, insertAIBridgeUserPrompt,
arg.ID,
arg.InterceptionID,
arg.ProviderResponseID,
arg.Prompt,
arg.Metadata,
arg.CreatedAt,
)
var i AIBridgeUserPrompt
err := row.Scan(
&i.ID,
&i.InterceptionID,
&i.ProviderResponseID,
&i.Prompt,
&i.Metadata,
&i.CreatedAt,
)
return i, err
}
const listAIBridgeClients = `-- name: ListAIBridgeClients :many
SELECT
COALESCE(client, 'Unknown') AS client
FROM
aibridge_interceptions
WHERE
ended_at IS NOT NULL
-- Filter client (prefix match to allow B-tree index usage).
AND CASE
WHEN $1::text != '' THEN COALESCE(aibridge_interceptions.client, 'Unknown') LIKE $1::text || '%'
ELSE true
END
-- We use an ` + "`" + `@authorize_filter` + "`" + ` as we are attempting to list clients
-- that are relevant to the user and what they are allowed to see.
-- Authorize Filter clause will be injected below in
-- ListAIBridgeClientsAuthorized.
-- @authorize_filter
GROUP BY
client
LIMIT COALESCE(NULLIF($3::integer, 0), 100)
OFFSET $2
`
type ListAIBridgeClientsParams struct {
Client string `db:"client" json:"client"`
Offset int32 `db:"offset_" json:"offset_"`
Limit int32 `db:"limit_" json:"limit_"`
}
func (q *sqlQuerier) ListAIBridgeClients(ctx context.Context, arg ListAIBridgeClientsParams) ([]string, error) {
rows, err := q.db.QueryContext(ctx, listAIBridgeClients, arg.Client, arg.Offset, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []string
for rows.Next() {
var client string
if err := rows.Scan(&client); err != nil {
return nil, err
}
items = append(items, client)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listAIBridgeInterceptionsTelemetrySummaries = `-- name: ListAIBridgeInterceptionsTelemetrySummaries :many
SELECT
DISTINCT ON (provider, model, client)
provider,
model,
COALESCE(client, 'Unknown') AS client
FROM
aibridge_interceptions
WHERE
ended_at IS NOT NULL -- incomplete interceptions are not included in summaries
AND ended_at >= $1::timestamptz
AND ended_at < $2::timestamptz
`
type ListAIBridgeInterceptionsTelemetrySummariesParams struct {
EndedAtAfter time.Time `db:"ended_at_after" json:"ended_at_after"`
EndedAtBefore time.Time `db:"ended_at_before" json:"ended_at_before"`
}
type ListAIBridgeInterceptionsTelemetrySummariesRow struct {
Provider string `db:"provider" json:"provider"`
Model string `db:"model" json:"model"`
Client string `db:"client" json:"client"`
}
// Finds all unique AI Bridge interception telemetry summaries combinations
// (provider, model, client) in the given timeframe for telemetry reporting.
func (q *sqlQuerier) ListAIBridgeInterceptionsTelemetrySummaries(ctx context.Context, arg ListAIBridgeInterceptionsTelemetrySummariesParams) ([]ListAIBridgeInterceptionsTelemetrySummariesRow, error) {
rows, err := q.db.QueryContext(ctx, listAIBridgeInterceptionsTelemetrySummaries, arg.EndedAtAfter, arg.EndedAtBefore)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListAIBridgeInterceptionsTelemetrySummariesRow
for rows.Next() {
var i ListAIBridgeInterceptionsTelemetrySummariesRow
if err := rows.Scan(&i.Provider, &i.Model, &i.Client); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listAIBridgeModelThoughtsByInterceptionIDs = `-- name: ListAIBridgeModelThoughtsByInterceptionIDs :many
SELECT
interception_id, content, metadata, created_at
FROM
aibridge_model_thoughts
WHERE
interception_id = ANY($1::uuid[])
ORDER BY
created_at ASC
`
func (q *sqlQuerier) ListAIBridgeModelThoughtsByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]AIBridgeModelThought, error) {
rows, err := q.db.QueryContext(ctx, listAIBridgeModelThoughtsByInterceptionIDs, pq.Array(interceptionIds))
if err != nil {
return nil, err
}
defer rows.Close()
var items []AIBridgeModelThought
for rows.Next() {
var i AIBridgeModelThought
if err := rows.Scan(
&i.InterceptionID,
&i.Content,
&i.Metadata,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listAIBridgeModels = `-- name: ListAIBridgeModels :many
SELECT
model
FROM
aibridge_interceptions
WHERE
-- Remove inflight interceptions (ones which lack an ended_at value).
aibridge_interceptions.ended_at IS NOT NULL
-- Filter model
AND CASE
WHEN $1::text != '' THEN aibridge_interceptions.model LIKE $1::text || '%'
ELSE true
END
-- We use an ` + "`" + `@authorize_filter` + "`" + ` as we are attempting to list models that are relevant
-- to the user and what they are allowed to see.
-- Authorize Filter clause will be injected below in ListAIBridgeModelsAuthorized
-- @authorize_filter
GROUP BY
model
ORDER BY
model ASC
LIMIT COALESCE(NULLIF($3::integer, 0), 100)
OFFSET $2
`
type ListAIBridgeModelsParams struct {
Model string `db:"model" json:"model"`
Offset int32 `db:"offset_" json:"offset_"`
Limit int32 `db:"limit_" json:"limit_"`
}
func (q *sqlQuerier) ListAIBridgeModels(ctx context.Context, arg ListAIBridgeModelsParams) ([]string, error) {
rows, err := q.db.QueryContext(ctx, listAIBridgeModels, arg.Model, arg.Offset, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []string
for rows.Next() {
var model string
if err := rows.Scan(&model); err != nil {
return nil, err
}
items = append(items, model)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listAIBridgeSessionNetworkCalls = `-- name: ListAIBridgeSessionNetworkCalls :many
SELECT bl.id, bl.session_id, bl.sequence_number, bl.captured_at, bl.created_at, bl.proto, bl.method, bl.detail, bl.matched_rule, bl.owner_id
FROM aibridge_interceptions afi
LEFT JOIN LATERAL (
SELECT COALESCE(MIN(nxt.agent_firewall_sequence_number), 2147483647) AS next_seq
FROM aibridge_interceptions nxt
WHERE nxt.agent_firewall_session_id = afi.agent_firewall_session_id
AND nxt.agent_firewall_sequence_number > afi.agent_firewall_sequence_number
) w ON true
JOIN boundary_logs bl
ON bl.session_id = afi.agent_firewall_session_id
AND bl.sequence_number > afi.agent_firewall_sequence_number
AND bl.sequence_number < w.next_seq
WHERE afi.session_id = $1::text
AND afi.ended_at IS NOT NULL
AND afi.agent_firewall_session_id IS NOT NULL
AND afi.agent_firewall_sequence_number IS NOT NULL
ORDER BY bl.created_at ASC, bl.sequence_number ASC, bl.id ASC
LIMIT COALESCE(NULLIF($2::integer, 0), 1000)
`
type ListAIBridgeSessionNetworkCallsParams struct {
SessionID string `db:"session_id" json:"session_id"`
Limit int32 `db:"limit_" json:"limit_"`
}
// Returns the individual Agent Firewall network calls made during an AI
// session, ordered chronologically. All protocols are included, unlike
// GetAIBridgeSessionTopDomains which considers only HTTP egress, so the list
// covers the same events the network_calls summary in ListAIBridgeSessions
// counts. The list is capped at @limit_ rows, so its length equals the summary
// total only for sessions at or below the cap. The summary stays authoritative
// for whole-session totals.
//
// Windowing mirrors that summary and GetAIBridgeSessionTopDomains: each
// interception's boundary logs fall in the open interval (this seq, next
// interception's seq) within the same firewall session. The exclusive lower
// bound drops the interception's own LLM-provider call. next_seq considers all
// interceptions in the firewall session so windows never bleed across AI
// sessions that share one firewall session, and falls back to the maximum
// sequence_number for the last interception so the window stays an
// index-satisfiable range.
// created_at leads because a session can span several firewall sessions, whose
// sequence numbers are independent streams. id breaks remaining ties so the row
// that lands on the limit boundary is stable across identical requests.
func (q *sqlQuerier) ListAIBridgeSessionNetworkCalls(ctx context.Context, arg ListAIBridgeSessionNetworkCallsParams) ([]BoundaryLog, error) {
rows, err := q.db.QueryContext(ctx, listAIBridgeSessionNetworkCalls, arg.SessionID, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []BoundaryLog
for rows.Next() {
var i BoundaryLog
if err := rows.Scan(
&i.ID,
&i.SessionID,
&i.SequenceNumber,
&i.CapturedAt,
&i.CreatedAt,
&i.Proto,
&i.Method,
&i.Detail,
&i.MatchedRule,
&i.OwnerID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listAIBridgeSessionThreads = `-- name: ListAIBridgeSessionThreads :many
WITH paginated_threads AS (
SELECT
-- Find thread root interceptions (thread_root_id IS NULL), apply cursor
-- pagination, and return the page.
aibridge_interceptions.id AS thread_id,
aibridge_interceptions.started_at
FROM
aibridge_interceptions
WHERE
aibridge_interceptions.session_id = $1::text
AND aibridge_interceptions.ended_at IS NOT NULL
AND aibridge_interceptions.thread_root_id IS NULL
-- Pagination cursor.
AND ($2::uuid = '00000000-0000-0000-0000-000000000000'::uuid OR
(aibridge_interceptions.started_at, aibridge_interceptions.id) > (
(SELECT started_at FROM aibridge_interceptions ai2 WHERE ai2.id = $2),
$2::uuid
)
)
AND ($3::uuid = '00000000-0000-0000-0000-000000000000'::uuid OR
(aibridge_interceptions.started_at, aibridge_interceptions.id) < (
(SELECT started_at FROM aibridge_interceptions ai2 WHERE ai2.id = $3),
$3::uuid
)
)
-- @authorize_filter
ORDER BY
aibridge_interceptions.started_at ASC,
aibridge_interceptions.id ASC
LIMIT COALESCE(NULLIF($4::integer, 0), 50)
)
SELECT
COALESCE(aibridge_interceptions.thread_root_id, aibridge_interceptions.id) AS thread_id,
aibridge_interceptions.id, aibridge_interceptions.initiator_id, aibridge_interceptions.provider, aibridge_interceptions.model, aibridge_interceptions.started_at, aibridge_interceptions.metadata, aibridge_interceptions.ended_at, aibridge_interceptions.api_key_id, aibridge_interceptions.client, aibridge_interceptions.thread_parent_id, aibridge_interceptions.thread_root_id, aibridge_interceptions.client_session_id, aibridge_interceptions.session_id, aibridge_interceptions.provider_name, aibridge_interceptions.credential_kind, aibridge_interceptions.credential_hint, aibridge_interceptions.agent_firewall_session_id, aibridge_interceptions.agent_firewall_sequence_number, aibridge_interceptions.error_type, aibridge_interceptions.error_message
FROM
aibridge_interceptions
JOIN
paginated_threads pt
ON pt.thread_id = COALESCE(aibridge_interceptions.thread_root_id, aibridge_interceptions.id)
WHERE
aibridge_interceptions.session_id = $1::text
AND aibridge_interceptions.ended_at IS NOT NULL
-- @authorize_filter
ORDER BY
-- Ensure threads and their associated interceptions (agentic loops) are sorted chronologically.
pt.started_at ASC,
pt.thread_id ASC,
aibridge_interceptions.started_at ASC,
aibridge_interceptions.id ASC
`
type ListAIBridgeSessionThreadsParams struct {
SessionID string `db:"session_id" json:"session_id"`
AfterID uuid.UUID `db:"after_id" json:"after_id"`
BeforeID uuid.UUID `db:"before_id" json:"before_id"`
Limit int32 `db:"limit_" json:"limit_"`
}
type ListAIBridgeSessionThreadsRow struct {
ThreadID uuid.UUID `db:"thread_id" json:"thread_id"`
AIBridgeInterception AIBridgeInterception `db:"aibridge_interception" json:"aibridge_interception"`
}
// Returns all interceptions belonging to paginated threads within a session.
// Threads are paginated by (started_at, thread_id) cursor.
func (q *sqlQuerier) ListAIBridgeSessionThreads(ctx context.Context, arg ListAIBridgeSessionThreadsParams) ([]ListAIBridgeSessionThreadsRow, error) {
rows, err := q.db.QueryContext(ctx, listAIBridgeSessionThreads,
arg.SessionID,
arg.AfterID,
arg.BeforeID,
arg.Limit,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListAIBridgeSessionThreadsRow
for rows.Next() {
var i ListAIBridgeSessionThreadsRow
if err := rows.Scan(
&i.ThreadID,
&i.AIBridgeInterception.ID,
&i.AIBridgeInterception.InitiatorID,
&i.AIBridgeInterception.Provider,
&i.AIBridgeInterception.Model,
&i.AIBridgeInterception.StartedAt,
&i.AIBridgeInterception.Metadata,
&i.AIBridgeInterception.EndedAt,
&i.AIBridgeInterception.APIKeyID,
&i.AIBridgeInterception.Client,
&i.AIBridgeInterception.ThreadParentID,
&i.AIBridgeInterception.ThreadRootID,
&i.AIBridgeInterception.ClientSessionID,
&i.AIBridgeInterception.SessionID,
&i.AIBridgeInterception.ProviderName,
&i.AIBridgeInterception.CredentialKind,
&i.AIBridgeInterception.CredentialHint,
&i.AIBridgeInterception.AgentFirewallSessionID,
&i.AIBridgeInterception.AgentFirewallSequenceNumber,
&i.AIBridgeInterception.ErrorType,
&i.AIBridgeInterception.ErrorMessage,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listAIBridgeSessions = `-- name: ListAIBridgeSessions :many
WITH cursor_pos AS (
-- Resolve the cursor's last_active_at once, outside the HAVING clause,
-- so the planner cannot accidentally re-evaluate it per group. Direct
-- LEFT JOIN is safe here since we only use MAX/MIN aggregates (no COUNT
-- affected by fan-out from multiple prompts per interception).
-- COALESCE falls back to MIN(ai.started_at) so the cursor value is
-- never NULL, which would silently drop rows from the HAVING comparison.
SELECT COALESCE(MAX(up.created_at), MIN(ai.started_at)) AS last_active_at
FROM aibridge_interceptions ai
LEFT JOIN aibridge_user_prompts up ON up.interception_id = ai.id
WHERE ai.session_id = $1 AND ai.ended_at IS NOT NULL
),
session_page AS (
-- Paginate at the session level first; only cheap aggregates here.
-- A lateral correlated subquery for prompts keeps the join one-to-one
-- with aibridge_interceptions so COUNT(*) for thread tallies is not
-- inflated. LIMIT 1 combined with the (interception_id, created_at DESC)
-- index makes this an index-only lookup per interception row rather than
-- a full-table-scan GROUP BY over all prompts.
-- last_active_at is the latest prompt timestamp, falling back to
-- MIN(started_at) for sessions with no prompts. The COALESCE ensures
-- it is never NULL so the HAVING row-value cursor comparison is safe.
SELECT
ai.session_id,
ai.initiator_id,
MIN(ai.started_at) AS started_at,
MAX(ai.ended_at) AS ended_at,
COUNT(*) FILTER (WHERE ai.thread_root_id IS NULL) AS threads,
COALESCE(MAX(latest_prompt.latest_prompt_at), MIN(ai.started_at))::timestamptz AS last_active_at
FROM
aibridge_interceptions ai
LEFT JOIN LATERAL (
SELECT created_at AS latest_prompt_at
FROM aibridge_user_prompts
WHERE interception_id = ai.id
ORDER BY created_at DESC
LIMIT 1
) latest_prompt ON true
WHERE
-- Remove inflight interceptions (ones which lack an ended_at value).
ai.ended_at IS NOT NULL
-- Filter by time frame
AND CASE
WHEN $2::timestamptz != '0001-01-01 00:00:00+00'::timestamptz THEN ai.started_at >= $2::timestamptz
ELSE true
END
AND CASE
WHEN $3::timestamptz != '0001-01-01 00:00:00+00'::timestamptz THEN ai.started_at <= $3::timestamptz
ELSE true
END
-- Filter initiator_id
AND CASE
WHEN $4::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN ai.initiator_id = $4::uuid
ELSE true
END
-- Filter provider
AND CASE
WHEN $5::text != '' THEN ai.provider = $5::text
ELSE true
END
-- Filter provider_name
AND CASE
WHEN $6::text != '' THEN ai.provider_name = $6::text
ELSE true
END
-- Filter model
AND CASE
WHEN $7::text != '' THEN ai.model = $7::text
ELSE true
END
-- Filter client
AND CASE
WHEN $8::text != '' THEN COALESCE(ai.client, 'Unknown') = $8::text
ELSE true
END
-- Filter session_id
AND CASE
WHEN $9::text != '' THEN ai.session_id = $9::text
ELSE true
END
-- Authorize Filter clause will be injected below in ListAuthorizedAIBridgeSessions
-- @authorize_filter
GROUP BY
ai.session_id, ai.initiator_id
HAVING
-- Cursor pagination: uses a composite (last_active_at, session_id) cursor to
-- support keyset pagination. The less-than comparison matches the DESC
-- sort order so rows after the cursor come later in results. The cursor
-- value comes from cursor_pos to guarantee single evaluation.
CASE
WHEN $1::text != '' THEN (
(COALESCE(MAX(latest_prompt.latest_prompt_at), MIN(ai.started_at)), ai.session_id) < (
(SELECT last_active_at FROM cursor_pos),
$1::text
)
)
ELSE true
END
ORDER BY
last_active_at DESC,
ai.session_id DESC
LIMIT COALESCE(NULLIF($11::integer, 0), 100)
OFFSET $10
)
SELECT
sp.session_id,
visible_users.id AS user_id,
visible_users.username AS user_username,
visible_users.name AS user_name,
visible_users.avatar_url AS user_avatar_url,
sr.providers::text[] AS providers,
sr.models::text[] AS models,
COALESCE(sr.client, '')::varchar(64) AS client,
sr.metadata::jsonb AS metadata,
sp.started_at::timestamptz AS started_at,
sp.ended_at::timestamptz AS ended_at,
sp.threads,
COALESCE(st.input_tokens, 0)::bigint AS input_tokens,
COALESCE(st.output_tokens, 0)::bigint AS output_tokens,
COALESCE(st.cache_read_input_tokens, 0)::bigint AS cache_read_input_tokens,
COALESCE(st.cache_write_input_tokens, 0)::bigint AS cache_write_input_tokens,
COALESCE(slp.prompt, '') AS last_prompt,
sp.last_active_at AS last_active_at,
COALESCE(bnc.total, 0)::bigint AS network_calls_total,
COALESCE(bnc.blocked, 0)::bigint AS network_calls_blocked,
COALESCE(sr.firewall_active, false) AS firewall_active
FROM
session_page sp
JOIN
visible_users ON visible_users.id = sp.initiator_id
LEFT JOIN LATERAL (
SELECT
(ARRAY_AGG(ai.client ORDER BY ai.started_at, ai.id))[1] AS client,
(ARRAY_AGG(ai.metadata ORDER BY ai.started_at, ai.id))[1] AS metadata,
ARRAY_AGG(DISTINCT ai.provider ORDER BY ai.provider) AS providers,
ARRAY_AGG(DISTINCT ai.model ORDER BY ai.model) AS models,
ARRAY_AGG(ai.id) AS interception_ids,
BOOL_OR(ai.agent_firewall_session_id IS NOT NULL) AS firewall_active
FROM aibridge_interceptions ai
WHERE ai.session_id = sp.session_id
AND ai.initiator_id = sp.initiator_id
AND ai.ended_at IS NOT NULL
) sr ON true
LEFT JOIN LATERAL (
-- Aggregate tokens only for this session's interceptions.
SELECT
COALESCE(SUM(tu.input_tokens), 0)::bigint AS input_tokens,
COALESCE(SUM(tu.output_tokens), 0)::bigint AS output_tokens,
COALESCE(SUM(tu.cache_read_input_tokens), 0)::bigint AS cache_read_input_tokens,
COALESCE(SUM(tu.cache_write_input_tokens), 0)::bigint AS cache_write_input_tokens
FROM aibridge_token_usages tu
WHERE tu.interception_id = ANY(sr.interception_ids)
) st ON true
LEFT JOIN LATERAL (
-- Fetch only the most recent user prompt across all interceptions
-- in the session.
SELECT up.prompt
FROM aibridge_user_prompts up
WHERE up.interception_id = ANY(sr.interception_ids)
ORDER BY up.created_at DESC, up.id DESC
LIMIT 1
) slp ON true
LEFT JOIN LATERAL (
-- Count Agent Firewall network calls attributed to this session. Each
-- interception marks a point in its firewall session's monotonic sequence
-- stream; the boundary logs it triggered fall in the open interval
-- (this seq, next interception's seq) within the same firewall session.
-- The exclusive lower bound drops the interception's own LLM-provider call
-- (logged at exactly its sequence number), leaving the agent's other
-- egress. next_seq considers all interceptions in the firewall session so
-- windows never bleed across AI sessions that share one firewall session.
--
SELECT
COUNT(*)::bigint AS total,
COUNT(*) FILTER (WHERE bl.matched_rule IS NULL)::bigint AS blocked
FROM aibridge_interceptions afi
LEFT JOIN LATERAL (
SELECT COALESCE(MIN(nxt.agent_firewall_sequence_number), 2147483647) AS next_seq
FROM aibridge_interceptions nxt
WHERE nxt.agent_firewall_session_id = afi.agent_firewall_session_id
AND nxt.agent_firewall_sequence_number > afi.agent_firewall_sequence_number
) w ON true
JOIN boundary_logs bl
ON bl.session_id = afi.agent_firewall_session_id
AND bl.sequence_number > afi.agent_firewall_sequence_number
AND bl.sequence_number < w.next_seq
WHERE afi.id = ANY(sr.interception_ids)
AND afi.agent_firewall_session_id IS NOT NULL
AND afi.agent_firewall_sequence_number IS NOT NULL
) bnc ON true
ORDER BY
sp.last_active_at DESC,
sp.session_id DESC
`
type ListAIBridgeSessionsParams struct {
AfterSessionID string `db:"after_session_id" json:"after_session_id"`
StartedAfter time.Time `db:"started_after" json:"started_after"`
StartedBefore time.Time `db:"started_before" json:"started_before"`
InitiatorID uuid.UUID `db:"initiator_id" json:"initiator_id"`
Provider string `db:"provider" json:"provider"`
ProviderName string `db:"provider_name" json:"provider_name"`
Model string `db:"model" json:"model"`
Client string `db:"client" json:"client"`
SessionID string `db:"session_id" json:"session_id"`
Offset int32 `db:"offset_" json:"offset_"`
Limit int32 `db:"limit_" json:"limit_"`
}
type ListAIBridgeSessionsRow struct {
SessionID string `db:"session_id" json:"session_id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
UserUsername string `db:"user_username" json:"user_username"`
UserName string `db:"user_name" json:"user_name"`
UserAvatarUrl string `db:"user_avatar_url" json:"user_avatar_url"`
Providers []string `db:"providers" json:"providers"`
Models []string `db:"models" json:"models"`
Client string `db:"client" json:"client"`
Metadata json.RawMessage `db:"metadata" json:"metadata"`
StartedAt time.Time `db:"started_at" json:"started_at"`
EndedAt time.Time `db:"ended_at" json:"ended_at"`
Threads int64 `db:"threads" json:"threads"`
InputTokens int64 `db:"input_tokens" json:"input_tokens"`
OutputTokens int64 `db:"output_tokens" json:"output_tokens"`
CacheReadInputTokens int64 `db:"cache_read_input_tokens" json:"cache_read_input_tokens"`
CacheWriteInputTokens int64 `db:"cache_write_input_tokens" json:"cache_write_input_tokens"`
LastPrompt string `db:"last_prompt" json:"last_prompt"`
LastActiveAt time.Time `db:"last_active_at" json:"last_active_at"`
NetworkCallsTotal int64 `db:"network_calls_total" json:"network_calls_total"`
NetworkCallsBlocked int64 `db:"network_calls_blocked" json:"network_calls_blocked"`
FirewallActive bool `db:"firewall_active" json:"firewall_active"`
}
// Returns paginated sessions with aggregated metadata, token counts, and
// the most recent user prompt. A "session" is a logical grouping of
// interceptions that share the same session_id (set by the client).
//
// Pagination-first strategy: identify the page of sessions cheaply via a
// single GROUP BY scan, then do expensive lateral joins (tokens, prompts,
// first-interception metadata) only for the ~page-size result set.
// The last interception in a session has no next row, so next_seq uses
// the largest sequence_number instead of NULL. The lookup stays a plain
// range, so the (session_id, sequence_number) index answers it alone.
// With NULL and an OR check, the index cannot bound the range: each
// interception reads every log to the end of the session and throws
// most of them away.
func (q *sqlQuerier) ListAIBridgeSessions(ctx context.Context, arg ListAIBridgeSessionsParams) ([]ListAIBridgeSessionsRow, error) {
rows, err := q.db.QueryContext(ctx, listAIBridgeSessions,
arg.AfterSessionID,
arg.StartedAfter,
arg.StartedBefore,
arg.InitiatorID,
arg.Provider,
arg.ProviderName,
arg.Model,
arg.Client,
arg.SessionID,
arg.Offset,
arg.Limit,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListAIBridgeSessionsRow
for rows.Next() {
var i ListAIBridgeSessionsRow
if err := rows.Scan(
&i.SessionID,
&i.UserID,
&i.UserUsername,
&i.UserName,
&i.UserAvatarUrl,
pq.Array(&i.Providers),
pq.Array(&i.Models),
&i.Client,
&i.Metadata,
&i.StartedAt,
&i.EndedAt,
&i.Threads,
&i.InputTokens,
&i.OutputTokens,
&i.CacheReadInputTokens,
&i.CacheWriteInputTokens,
&i.LastPrompt,
&i.LastActiveAt,
&i.NetworkCallsTotal,
&i.NetworkCallsBlocked,
&i.FirewallActive,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listAIBridgeTokenUsagesByInterceptionIDs = `-- name: ListAIBridgeTokenUsagesByInterceptionIDs :many
SELECT
id, interception_id, provider_response_id, input_tokens, output_tokens, metadata, created_at, cache_read_input_tokens, cache_write_input_tokens, effective_group_id, input_price_micros, output_price_micros, cache_read_price_micros, cache_write_price_micros, cost_micros
FROM
aibridge_token_usages
WHERE
interception_id = ANY($1::uuid[])
ORDER BY
created_at ASC,
id ASC
`
func (q *sqlQuerier) ListAIBridgeTokenUsagesByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]AIBridgeTokenUsage, error) {
rows, err := q.db.QueryContext(ctx, listAIBridgeTokenUsagesByInterceptionIDs, pq.Array(interceptionIds))
if err != nil {
return nil, err
}
defer rows.Close()
var items []AIBridgeTokenUsage
for rows.Next() {
var i AIBridgeTokenUsage
if err := rows.Scan(
&i.ID,
&i.InterceptionID,
&i.ProviderResponseID,
&i.InputTokens,
&i.OutputTokens,
&i.Metadata,
&i.CreatedAt,
&i.CacheReadInputTokens,
&i.CacheWriteInputTokens,
&i.EffectiveGroupID,
&i.InputPriceMicros,
&i.OutputPriceMicros,
&i.CacheReadPriceMicros,
&i.CacheWritePriceMicros,
&i.CostMicros,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listAIBridgeToolUsagesByInterceptionIDs = `-- name: ListAIBridgeToolUsagesByInterceptionIDs :many
SELECT
id, interception_id, provider_response_id, server_url, tool, input, injected, invocation_error, metadata, created_at, provider_tool_call_id, provider_item_id
FROM
aibridge_tool_usages
WHERE
interception_id = ANY($1::uuid[])
ORDER BY
created_at ASC,
id ASC
`
func (q *sqlQuerier) ListAIBridgeToolUsagesByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]AIBridgeToolUsage, error) {
rows, err := q.db.QueryContext(ctx, listAIBridgeToolUsagesByInterceptionIDs, pq.Array(interceptionIds))
if err != nil {
return nil, err
}
defer rows.Close()
var items []AIBridgeToolUsage
for rows.Next() {
var i AIBridgeToolUsage
if err := rows.Scan(
&i.ID,
&i.InterceptionID,
&i.ProviderResponseID,
&i.ServerUrl,
&i.Tool,
&i.Input,
&i.Injected,
&i.InvocationError,
&i.Metadata,
&i.CreatedAt,
&i.ProviderToolCallID,
&i.ProviderItemID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listAIBridgeUserPromptsByInterceptionIDs = `-- name: ListAIBridgeUserPromptsByInterceptionIDs :many
SELECT
id, interception_id, provider_response_id, prompt, metadata, created_at
FROM
aibridge_user_prompts
WHERE
interception_id = ANY($1::uuid[])
ORDER BY
created_at ASC,
id ASC
`
func (q *sqlQuerier) ListAIBridgeUserPromptsByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]AIBridgeUserPrompt, error) {
rows, err := q.db.QueryContext(ctx, listAIBridgeUserPromptsByInterceptionIDs, pq.Array(interceptionIds))
if err != nil {
return nil, err
}
defer rows.Close()
var items []AIBridgeUserPrompt
for rows.Next() {
var i AIBridgeUserPrompt
if err := rows.Scan(
&i.ID,
&i.InterceptionID,
&i.ProviderResponseID,
&i.Prompt,
&i.Metadata,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateAIBridgeInterceptionEnded = `-- name: UpdateAIBridgeInterceptionEnded :one
UPDATE aibridge_interceptions
SET ended_at = $1::timestamptz,
-- BYOK records its hint at the start of the interception.
-- Centralized uses key failover, so its hint is only known
-- at end-of-interception.
credential_hint = CASE
WHEN credential_kind = 'centralized' THEN $2::text
ELSE credential_hint
END,
-- Terminal upstream error, only set when the interception failed.
-- NULL leaves the columns empty for successful interceptions.
error_type = $3::aibridge_interception_error_type,
error_message = $4::text
WHERE
id = $5::uuid
AND ended_at IS NULL
RETURNING id, initiator_id, provider, model, started_at, metadata, ended_at, api_key_id, client, thread_parent_id, thread_root_id, client_session_id, session_id, provider_name, credential_kind, credential_hint, agent_firewall_session_id, agent_firewall_sequence_number, error_type, error_message
`
type UpdateAIBridgeInterceptionEndedParams struct {
EndedAt time.Time `db:"ended_at" json:"ended_at"`
CredentialHint string `db:"credential_hint" json:"credential_hint"`
ErrorType NullAIBridgeInterceptionErrorType `db:"error_type" json:"error_type"`
ErrorMessage sql.NullString `db:"error_message" json:"error_message"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateAIBridgeInterceptionEnded(ctx context.Context, arg UpdateAIBridgeInterceptionEndedParams) (AIBridgeInterception, error) {
row := q.db.QueryRowContext(ctx, updateAIBridgeInterceptionEnded,
arg.EndedAt,
arg.CredentialHint,
arg.ErrorType,
arg.ErrorMessage,
arg.ID,
)
var i AIBridgeInterception
err := row.Scan(
&i.ID,
&i.InitiatorID,
&i.Provider,
&i.Model,
&i.StartedAt,
&i.Metadata,
&i.EndedAt,
&i.APIKeyID,
&i.Client,
&i.ThreadParentID,
&i.ThreadRootID,
&i.ClientSessionID,
&i.SessionID,
&i.ProviderName,
&i.CredentialKind,
&i.CredentialHint,
&i.AgentFirewallSessionID,
&i.AgentFirewallSequenceNumber,
&i.ErrorType,
&i.ErrorMessage,
)
return i, err
}
const deleteGroupAIBudget = `-- name: DeleteGroupAIBudget :one
DELETE FROM group_ai_budgets WHERE group_id = $1 RETURNING group_id, spend_limit_micros, created_at, updated_at
`
func (q *sqlQuerier) DeleteGroupAIBudget(ctx context.Context, groupID uuid.UUID) (GroupAIBudget, error) {
row := q.db.QueryRowContext(ctx, deleteGroupAIBudget, groupID)
var i GroupAIBudget
err := row.Scan(
&i.GroupID,
&i.SpendLimitMicros,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const deleteUserAIBudgetOverride = `-- name: DeleteUserAIBudgetOverride :one
DELETE FROM user_ai_budget_overrides WHERE user_id = $1 RETURNING user_id, group_id, spend_limit_micros, created_at, updated_at
`
func (q *sqlQuerier) DeleteUserAIBudgetOverride(ctx context.Context, userID uuid.UUID) (UserAIBudgetOverride, error) {
row := q.db.QueryRowContext(ctx, deleteUserAIBudgetOverride, userID)
var i UserAIBudgetOverride
err := row.Scan(
&i.UserID,
&i.GroupID,
&i.SpendLimitMicros,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const exportOrganizationAISpend = `-- name: ExportOrganizationAISpend :many
SELECT
ai.initiator_id AS user_id,
users.username AS username,
tu.effective_group_id AS group_id,
groups.name AS group_name,
groups.organization_id AS organization_id,
organizations.name AS organization_name,
ai.model AS model,
ai.provider AS provider,
ai.provider_name AS provider_name,
COALESCE(SUM(tu.input_tokens), 0)::BIGINT AS input_tokens,
COALESCE(SUM(tu.output_tokens), 0)::BIGINT AS output_tokens,
COALESCE(SUM(tu.cache_read_input_tokens), 0)::BIGINT AS cache_read_tokens,
COALESCE(SUM(tu.cache_write_input_tokens), 0)::BIGINT AS cache_write_tokens,
COALESCE(SUM(tu.cost_micros), 0)::BIGINT AS cost_micros
FROM aibridge_token_usages tu
JOIN aibridge_interceptions ai ON ai.id = tu.interception_id
JOIN users ON users.id = ai.initiator_id
JOIN groups ON groups.id = tu.effective_group_id
JOIN organizations ON organizations.id = groups.organization_id
WHERE groups.organization_id = $1
AND tu.created_at >= $2::timestamptz
AND tu.created_at < $3::timestamptz
GROUP BY
ai.initiator_id,
users.username,
tu.effective_group_id,
groups.name,
groups.organization_id,
organizations.name,
ai.model,
ai.provider,
ai.provider_name
ORDER BY ai.initiator_id, tu.effective_group_id, ai.provider, ai.provider_name, ai.model
`
type ExportOrganizationAISpendParams struct {
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
PeriodStart time.Time `db:"period_start" json:"period_start"`
PeriodEnd time.Time `db:"period_end" json:"period_end"`
}
type ExportOrganizationAISpendRow struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
Username string `db:"username" json:"username"`
GroupID uuid.NullUUID `db:"group_id" json:"group_id"`
GroupName string `db:"group_name" json:"group_name"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
OrganizationName string `db:"organization_name" json:"organization_name"`
Model string `db:"model" json:"model"`
Provider string `db:"provider" json:"provider"`
ProviderName string `db:"provider_name" json:"provider_name"`
InputTokens int64 `db:"input_tokens" json:"input_tokens"`
OutputTokens int64 `db:"output_tokens" json:"output_tokens"`
CacheReadTokens int64 `db:"cache_read_tokens" json:"cache_read_tokens"`
CacheWriteTokens int64 `db:"cache_write_tokens" json:"cache_write_tokens"`
CostMicros int64 `db:"cost_micros" json:"cost_micros"`
}
// Returns per-user, per-group, per-model, per-provider aggregated AI spend for
// @organization_id over the [period_start, period_end) window. Spend is
// attributed through the token usage's effective group, and rows are bucketed
// by the token usage created_at, matching how ai_user_daily_spend is derived.
func (q *sqlQuerier) ExportOrganizationAISpend(ctx context.Context, arg ExportOrganizationAISpendParams) ([]ExportOrganizationAISpendRow, error) {
rows, err := q.db.QueryContext(ctx, exportOrganizationAISpend, arg.OrganizationID, arg.PeriodStart, arg.PeriodEnd)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ExportOrganizationAISpendRow
for rows.Next() {
var i ExportOrganizationAISpendRow
if err := rows.Scan(
&i.UserID,
&i.Username,
&i.GroupID,
&i.GroupName,
&i.OrganizationID,
&i.OrganizationName,
&i.Model,
&i.Provider,
&i.ProviderName,
&i.InputTokens,
&i.OutputTokens,
&i.CacheReadTokens,
&i.CacheWriteTokens,
&i.CostMicros,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getAIModelPriceByProviderModel = `-- name: GetAIModelPriceByProviderModel :one
SELECT provider, model, input_price, output_price, cache_read_price, cache_write_price, created_at, updated_at
FROM ai_model_prices
WHERE provider = $1 AND model = $2
`
type GetAIModelPriceByProviderModelParams struct {
Provider string `db:"provider" json:"provider"`
Model string `db:"model" json:"model"`
}
func (q *sqlQuerier) GetAIModelPriceByProviderModel(ctx context.Context, arg GetAIModelPriceByProviderModelParams) (AIModelPrice, error) {
row := q.db.QueryRowContext(ctx, getAIModelPriceByProviderModel, arg.Provider, arg.Model)
var i AIModelPrice
err := row.Scan(
&i.Provider,
&i.Model,
&i.InputPrice,
&i.OutputPrice,
&i.CacheReadPrice,
&i.CacheWritePrice,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getAIModelPrices = `-- name: GetAIModelPrices :many
SELECT provider, model, input_price, output_price, cache_read_price, cache_write_price, created_at, updated_at
FROM ai_model_prices
-- Filter by provider
WHERE CASE
WHEN $1::text != '' THEN
provider = $1
ELSE true
END
-- Filter by model
AND CASE
WHEN $2::text != '' THEN
model = $2
ELSE true
END
ORDER BY provider, model
`
type GetAIModelPricesParams struct {
Provider string `db:"provider" json:"provider"`
Model string `db:"model" json:"model"`
}
func (q *sqlQuerier) GetAIModelPrices(ctx context.Context, arg GetAIModelPricesParams) ([]AIModelPrice, error) {
rows, err := q.db.QueryContext(ctx, getAIModelPrices, arg.Provider, arg.Model)
if err != nil {
return nil, err
}
defer rows.Close()
var items []AIModelPrice
for rows.Next() {
var i AIModelPrice
if err := rows.Scan(
&i.Provider,
&i.Model,
&i.InputPrice,
&i.OutputPrice,
&i.CacheReadPrice,
&i.CacheWritePrice,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getGroupAIBudget = `-- name: GetGroupAIBudget :one
SELECT group_id, spend_limit_micros, created_at, updated_at
FROM group_ai_budgets
WHERE group_id = $1
`
func (q *sqlQuerier) GetGroupAIBudget(ctx context.Context, groupID uuid.UUID) (GroupAIBudget, error) {
row := q.db.QueryRowContext(ctx, getGroupAIBudget, groupID)
var i GroupAIBudget
err := row.Scan(
&i.GroupID,
&i.SpendLimitMicros,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getGroupMembersAISpend = `-- name: GetGroupMembersAISpend :many
WITH queried_group AS (
-- The queried group's org, used to detect cross-org effective groups.
SELECT organization_id
FROM groups
WHERE id = $1
),
filtered_users AS (
-- Users from @user_ids that are members of the queried group. Uses
-- group_members_expanded so the implicit Everyone group counts.
SELECT DISTINCT user_id
FROM group_members_expanded
WHERE group_id = $1
AND user_id = ANY($3::uuid[])
),
user_highest_group AS (
-- Per user, the highest-limit group they belong to. Uses
-- group_members_expanded so the implicit Everyone group counts.
SELECT DISTINCT ON (member.user_id)
member.user_id,
budget.group_id,
budget.spend_limit_micros
FROM group_ai_budgets budget
JOIN group_members_expanded member ON member.group_id = budget.group_id
JOIN organizations ON organizations.id = member.organization_id
JOIN organization_members
ON organization_members.user_id = member.user_id
AND organization_members.organization_id = member.organization_id
WHERE member.user_id IN (SELECT user_id FROM filtered_users)
AND organizations.deleted = false
ORDER BY member.user_id, budget.spend_limit_micros DESC, organization_members.created_at ASC, budget.group_id ASC
),
user_fallback_group AS (
-- Per user, the Everyone group to fall back to when no override or budgeted
-- group applies. The Everyone group has id == organization_id. Prefers the
-- default org, then the earliest organization membership.
SELECT DISTINCT ON (organization_members.user_id)
organization_members.user_id,
organizations.id AS group_id
FROM organization_members
JOIN organizations ON organizations.id = organization_members.organization_id
WHERE organization_members.user_id IN (SELECT user_id FROM filtered_users)
AND organizations.deleted = false
ORDER BY organization_members.user_id, organizations.is_default DESC, organization_members.created_at ASC, organizations.id ASC
),
effective AS (
-- Effective budget per user: a per-user override wins over the highest-limit
-- group, which wins over the Everyone group fallback.
SELECT
filtered_users.user_id,
COALESCE(override.group_id, user_highest_group.group_id, user_fallback_group.group_id) AS raw_effective_group_id,
COALESCE(override.spend_limit_micros, user_highest_group.spend_limit_micros) AS spend_limit_micros,
(CASE
WHEN override.group_id IS NOT NULL THEN 'user_override'
WHEN user_highest_group.group_id IS NOT NULL THEN 'group'
END)::text AS limit_source
FROM filtered_users
LEFT JOIN user_ai_budget_overrides override ON override.user_id = filtered_users.user_id
LEFT JOIN user_highest_group ON user_highest_group.user_id = filtered_users.user_id
LEFT JOIN user_fallback_group ON user_fallback_group.user_id = filtered_users.user_id
),
applied_budget AS (
-- The limit and source only for users whose effective budget source is the
-- queried group.
SELECT user_id, spend_limit_micros, limit_source
FROM effective
WHERE raw_effective_group_id = $1
)
SELECT
effective.user_id,
queried_group.organization_id,
effective_group.id AS effective_group_id,
applied_budget.spend_limit_micros,
applied_budget.limit_source,
COALESCE(SUM(spend.spend_micros), 0)::BIGINT AS group_spend_micros
FROM effective
CROSS JOIN queried_group
LEFT JOIN groups effective_group
ON effective_group.id = effective.raw_effective_group_id
AND effective_group.organization_id = queried_group.organization_id
LEFT JOIN applied_budget ON applied_budget.user_id = effective.user_id
LEFT JOIN ai_user_daily_spend spend
ON spend.user_id = effective.user_id
AND spend.effective_group_id = $1
AND spend.day >= (($2::timestamptz) AT TIME ZONE 'UTC')::date
GROUP BY
effective.user_id,
queried_group.organization_id,
effective_group.id,
applied_budget.spend_limit_micros,
applied_budget.limit_source
ORDER BY effective.user_id
`
type GetGroupMembersAISpendParams struct {
GroupID uuid.UUID `db:"group_id" json:"group_id"`
PeriodStart time.Time `db:"period_start" json:"period_start"`
UserIds []uuid.UUID `db:"user_ids" json:"user_ids"`
}
type GetGroupMembersAISpendRow struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
EffectiveGroupID uuid.NullUUID `db:"effective_group_id" json:"effective_group_id"`
SpendLimitMicros sql.NullInt64 `db:"spend_limit_micros" json:"spend_limit_micros"`
LimitSource sql.NullString `db:"limit_source" json:"limit_source"`
GroupSpendMicros int64 `db:"group_spend_micros" json:"group_spend_micros"`
}
// Returns each user's AI spend attributed to the queried group, on or after
// period_start until NOW. Only current members of the queried group are
// returned. spend_limit_micros and limit_source are populated only when the
// queried group is the user's effective budget source. The effective group
// falls back to the Everyone group, and effective_group_id is null only when
// that group belongs to a different organization than the queried group.
// The period_start parameter is normalized to its UTC calendar day.
// TODO(AIGOV-527): unify effective group resolution in a single place.
// Spend is aggregated for the queried group, not the user's effective group.
// A LEFT JOIN leaves spend_limit_micros and limit_source null for users
// whose effective budget source is not the queried group.
func (q *sqlQuerier) GetGroupMembersAISpend(ctx context.Context, arg GetGroupMembersAISpendParams) ([]GetGroupMembersAISpendRow, error) {
rows, err := q.db.QueryContext(ctx, getGroupMembersAISpend, arg.GroupID, arg.PeriodStart, pq.Array(arg.UserIds))
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetGroupMembersAISpendRow
for rows.Next() {
var i GetGroupMembersAISpendRow
if err := rows.Scan(
&i.UserID,
&i.OrganizationID,
&i.EffectiveGroupID,
&i.SpendLimitMicros,
&i.LimitSource,
&i.GroupSpendMicros,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getHighestGroupAIBudgetByUser = `-- name: GetHighestGroupAIBudgetByUser :one
SELECT
budget.group_id,
budget.spend_limit_micros
FROM group_ai_budgets budget
JOIN group_members_expanded member ON member.group_id = budget.group_id
JOIN organizations ON organizations.id = member.organization_id
JOIN organization_members
ON organization_members.user_id = member.user_id
AND organization_members.organization_id = member.organization_id
WHERE member.user_id = $1
AND organizations.deleted = false
ORDER BY
budget.spend_limit_micros DESC, -- highest wins
organization_members.created_at ASC, -- earliest organization membership
budget.group_id ASC -- deterministic tiebreak
LIMIT 1
`
type GetHighestGroupAIBudgetByUserRow struct {
GroupID uuid.UUID `db:"group_id" json:"group_id"`
SpendLimitMicros int64 `db:"spend_limit_micros" json:"spend_limit_micros"`
}
// Returns the highest group AI budget across the groups the user belongs to,
// breaking ties by the earliest organization membership. Implements the
// "highest" budget policy. group_members_expanded is a UNION of group_members
// and organization_members, so the implicit "Everyone" group
// (group_id == organization_id) is included. Returns no rows when the user has
// no budgeted groups. Callers should treat sql.ErrNoRows as "no group budget".
func (q *sqlQuerier) GetHighestGroupAIBudgetByUser(ctx context.Context, userID uuid.UUID) (GetHighestGroupAIBudgetByUserRow, error) {
row := q.db.QueryRowContext(ctx, getHighestGroupAIBudgetByUser, userID)
var i GetHighestGroupAIBudgetByUserRow
err := row.Scan(&i.GroupID, &i.SpendLimitMicros)
return i, err
}
const getOrganizationGroupsAISpend = `-- name: GetOrganizationGroupsAISpend :many
WITH queried_groups AS (
-- The requested groups that belong to the queried organization.
SELECT groups.id, groups.organization_id
FROM groups
WHERE groups.organization_id = $1
AND groups.id = ANY($2::uuid[])
),
candidate_users AS (
-- Members of the queried groups. Uses group_members_expanded so the implicit
-- Everyone group counts.
SELECT DISTINCT member.user_id
FROM group_members_expanded member
WHERE member.group_id IN (SELECT id FROM queried_groups)
),
user_highest_group AS (
-- Per user, the highest-limit group they belong to. Uses
-- group_members_expanded so the implicit Everyone group counts.
SELECT DISTINCT ON (member.user_id)
member.user_id,
budget.group_id,
budget.spend_limit_micros
FROM group_ai_budgets budget
JOIN group_members_expanded member ON member.group_id = budget.group_id
JOIN organizations ON organizations.id = member.organization_id
JOIN organization_members
ON organization_members.user_id = member.user_id
AND organization_members.organization_id = member.organization_id
WHERE member.user_id IN (SELECT user_id FROM candidate_users)
AND organizations.deleted = false
ORDER BY member.user_id, budget.spend_limit_micros DESC, organization_members.created_at ASC, budget.group_id ASC
),
effective AS (
-- Effective budget group per user: an override wins over the highest-limit
-- group they belong to. Users with neither are left out, since the group they
-- fall back to has no budget and reports null.
SELECT
candidate_users.user_id,
COALESCE(override.group_id, user_highest_group.group_id) AS effective_group_id,
override.spend_limit_micros AS override_limit_micros
FROM candidate_users
LEFT JOIN user_ai_budget_overrides override ON override.user_id = candidate_users.user_id
LEFT JOIN user_highest_group ON user_highest_group.user_id = candidate_users.user_id
),
group_limits AS (
-- Per attributed group, how many members take the group's own limit and the
-- combined limit of those carrying an override.
SELECT
effective.effective_group_id AS group_id,
count(*) FILTER (WHERE effective.override_limit_micros IS NULL) AS plain_member_count,
COALESCE(SUM(effective.override_limit_micros), 0)::BIGINT AS override_limit_sum
FROM effective
WHERE effective.effective_group_id IS NOT NULL
GROUP BY effective.effective_group_id
),
group_totals AS (
-- Combined limit per budgeted group, counting members with no override at the
-- group's own limit and adding the overrides on top. Unbudgeted groups are
-- absent here, so the join below leaves their total null.
SELECT
queried_groups.id AS group_id,
(budget.spend_limit_micros * COALESCE(group_limits.plain_member_count, 0)
+ COALESCE(group_limits.override_limit_sum, 0))::BIGINT AS total_spend_limit_micros
FROM queried_groups
JOIN group_ai_budgets budget ON budget.group_id = queried_groups.id
LEFT JOIN group_limits ON group_limits.group_id = queried_groups.id
),
group_spend AS (
-- Spend per queried group over the period.
SELECT
spend.effective_group_id AS group_id,
COALESCE(SUM(spend.spend_micros), 0)::BIGINT AS current_spend_micros
FROM ai_user_daily_spend spend
WHERE spend.effective_group_id IN (SELECT id FROM queried_groups)
AND spend.day >= (($3::timestamptz) AT TIME ZONE 'UTC')::date
GROUP BY spend.effective_group_id
)
SELECT
queried_groups.id AS group_id,
queried_groups.organization_id AS organization_id,
budget.spend_limit_micros AS spend_limit_micros,
group_totals.total_spend_limit_micros AS total_spend_limit_micros,
COALESCE(group_spend.current_spend_micros, 0)::BIGINT AS current_spend_micros
FROM queried_groups
LEFT JOIN group_ai_budgets budget ON budget.group_id = queried_groups.id
LEFT JOIN group_totals ON group_totals.group_id = queried_groups.id
LEFT JOIN group_spend ON group_spend.group_id = queried_groups.id
ORDER BY queried_groups.id
`
type GetOrganizationGroupsAISpendParams struct {
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
GroupIds []uuid.UUID `db:"group_ids" json:"group_ids"`
PeriodStart time.Time `db:"period_start" json:"period_start"`
}
type GetOrganizationGroupsAISpendRow struct {
GroupID uuid.UUID `db:"group_id" json:"group_id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
SpendLimitMicros sql.NullInt64 `db:"spend_limit_micros" json:"spend_limit_micros"`
TotalSpendLimitMicros sql.NullInt64 `db:"total_spend_limit_micros" json:"total_spend_limit_micros"`
CurrentSpendMicros int64 `db:"current_spend_micros" json:"current_spend_micros"`
}
// Returns AI spend limits and aggregate spend for groups in @group_ids that
// belong to @organization_id, on or after period_start until NOW.
// spend_limit_micros is the per-member limit, null when the group has no budget.
// total_spend_limit_micros is the combined budget of the members attributed to
// the group, with each member's override replacing their share. It is null when
// the group has no budget.
// The period_start parameter is normalized to its UTC calendar day.
// TODO(AIGOV-527): unify effective group resolution in a single place.
func (q *sqlQuerier) GetOrganizationGroupsAISpend(ctx context.Context, arg GetOrganizationGroupsAISpendParams) ([]GetOrganizationGroupsAISpendRow, error) {
rows, err := q.db.QueryContext(ctx, getOrganizationGroupsAISpend, arg.OrganizationID, pq.Array(arg.GroupIds), arg.PeriodStart)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetOrganizationGroupsAISpendRow
for rows.Next() {
var i GetOrganizationGroupsAISpendRow
if err := rows.Scan(
&i.GroupID,
&i.OrganizationID,
&i.SpendLimitMicros,
&i.TotalSpendLimitMicros,
&i.CurrentSpendMicros,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getOverBudgetUsersPerGroup = `-- name: GetOverBudgetUsersPerGroup :many
WITH budgeted_users AS (
-- Users with an override or membership in a budgeted group.
SELECT user_id FROM user_ai_budget_overrides
UNION
SELECT DISTINCT member.user_id
FROM group_ai_budgets budget
JOIN group_members_expanded member ON member.group_id = budget.group_id
),
user_highest_group AS (
-- Per user, their highest-limit group ("highest" budget policy).
SELECT DISTINCT ON (member.user_id)
member.user_id,
budget.group_id,
budget.spend_limit_micros
FROM group_ai_budgets budget
JOIN group_members_expanded member ON member.group_id = budget.group_id
JOIN organizations ON organizations.id = member.organization_id
JOIN organization_members
ON organization_members.user_id = member.user_id
AND organization_members.organization_id = member.organization_id
WHERE member.user_id IN (SELECT user_id FROM budgeted_users)
AND organizations.deleted = false
ORDER BY member.user_id, budget.spend_limit_micros DESC, organization_members.created_at ASC, budget.group_id ASC
),
effective AS (
-- An override wins over the highest-limit group, and users with neither drop.
SELECT
budgeted_users.user_id,
COALESCE(override.group_id, user_highest_group.group_id) AS effective_group_id,
COALESCE(override.spend_limit_micros, user_highest_group.spend_limit_micros) AS spend_limit_micros
FROM budgeted_users
LEFT JOIN user_ai_budget_overrides override ON override.user_id = budgeted_users.user_id
LEFT JOIN user_highest_group ON user_highest_group.user_id = budgeted_users.user_id
WHERE COALESCE(override.group_id, user_highest_group.group_id) IS NOT NULL
),
user_spend AS (
-- Each user's spend against their effective group since period_start.
SELECT
effective.user_id,
effective.effective_group_id,
effective.spend_limit_micros,
COALESCE(SUM(spend.spend_micros), 0)::BIGINT AS current_spend_micros
FROM effective
LEFT JOIN ai_user_daily_spend spend
ON spend.user_id = effective.user_id
AND spend.effective_group_id = effective.effective_group_id
AND spend.day >= (($1::timestamptz) AT TIME ZONE 'UTC')::date
GROUP BY effective.user_id, effective.effective_group_id, effective.spend_limit_micros
)
SELECT
effective_group_id AS group_id,
COUNT(*)::BIGINT AS over_budget_users
FROM user_spend
WHERE current_spend_micros >= spend_limit_micros
GROUP BY effective_group_id
ORDER BY effective_group_id
`
type GetOverBudgetUsersPerGroupRow struct {
GroupID uuid.UUID `db:"group_id" json:"group_id"`
OverBudgetUsers int64 `db:"over_budget_users" json:"over_budget_users"`
}
// Returns, per effective group, the number of users at or over their spend
// limit since period_start. Only users with an enforceable limit (override or
// budgeted group) count, and the unlimited Everyone fallback does not.
// TODO(AIGOV-527): unify effective group resolution in a single place.
func (q *sqlQuerier) GetOverBudgetUsersPerGroup(ctx context.Context, periodStart time.Time) ([]GetOverBudgetUsersPerGroupRow, error) {
rows, err := q.db.QueryContext(ctx, getOverBudgetUsersPerGroup, periodStart)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetOverBudgetUsersPerGroupRow
for rows.Next() {
var i GetOverBudgetUsersPerGroupRow
if err := rows.Scan(&i.GroupID, &i.OverBudgetUsers); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getUserAIBudgetOverride = `-- name: GetUserAIBudgetOverride :one
SELECT user_id, group_id, spend_limit_micros, created_at, updated_at
FROM user_ai_budget_overrides
WHERE user_id = $1
`
func (q *sqlQuerier) GetUserAIBudgetOverride(ctx context.Context, userID uuid.UUID) (UserAIBudgetOverride, error) {
row := q.db.QueryRowContext(ctx, getUserAIBudgetOverride, userID)
var i UserAIBudgetOverride
err := row.Scan(
&i.UserID,
&i.GroupID,
&i.SpendLimitMicros,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getUserAISpendSince = `-- name: GetUserAISpendSince :one
SELECT
$1::uuid AS user_id,
$2::uuid AS effective_group_id,
(($3::timestamptz) AT TIME ZONE 'UTC')::date AS period_start,
COALESCE(SUM(spend_micros), 0)::BIGINT AS spend_micros
FROM ai_user_daily_spend
WHERE user_id = $1
AND effective_group_id = $2
AND day >= (($3::timestamptz) AT TIME ZONE 'UTC')::date
`
type GetUserAISpendSinceParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
EffectiveGroupID uuid.UUID `db:"effective_group_id" json:"effective_group_id"`
PeriodStart time.Time `db:"period_start" json:"period_start"`
}
type GetUserAISpendSinceRow struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
EffectiveGroupID uuid.UUID `db:"effective_group_id" json:"effective_group_id"`
PeriodStart time.Time `db:"period_start" json:"period_start"`
SpendMicros int64 `db:"spend_micros" json:"spend_micros"`
}
// Total spend for (user_id, effective_group_id) on or after period_start until NOW.
// The period_start parameter is normalized to its UTC calendar day.
func (q *sqlQuerier) GetUserAISpendSince(ctx context.Context, arg GetUserAISpendSinceParams) (GetUserAISpendSinceRow, error) {
row := q.db.QueryRowContext(ctx, getUserAISpendSince, arg.UserID, arg.EffectiveGroupID, arg.PeriodStart)
var i GetUserAISpendSinceRow
err := row.Scan(
&i.UserID,
&i.EffectiveGroupID,
&i.PeriodStart,
&i.SpendMicros,
)
return i, err
}
const getUserEveryoneFallbackGroup = `-- name: GetUserEveryoneFallbackGroup :one
SELECT organizations.id AS group_id
FROM organization_members
JOIN organizations ON organizations.id = organization_members.organization_id
WHERE organization_members.user_id = $1
AND organizations.deleted = false
ORDER BY
organizations.is_default DESC, -- prefer the default org
organization_members.created_at ASC, -- earliest organization membership
organizations.id ASC -- deterministic tiebreak
LIMIT 1
`
// Returns the "Everyone" group (id == organization_id) to attribute a user's
// spend to when no override or budgeted group applies. Prefers the default org,
// then the earliest organization membership. Returns no rows when the user has
// no organization membership.
func (q *sqlQuerier) GetUserEveryoneFallbackGroup(ctx context.Context, userID uuid.UUID) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, getUserEveryoneFallbackGroup, userID)
var group_id uuid.UUID
err := row.Scan(&group_id)
return group_id, err
}
const incrementUserAIDailySpend = `-- name: IncrementUserAIDailySpend :one
INSERT INTO ai_user_daily_spend (user_id, effective_group_id, day, spend_micros)
VALUES ($1, $2, (($3::timestamptz) AT TIME ZONE 'UTC')::date, $4)
ON CONFLICT (user_id, effective_group_id, day) DO UPDATE SET
spend_micros = ai_user_daily_spend.spend_micros + EXCLUDED.spend_micros
RETURNING user_id, effective_group_id, day, spend_micros
`
type IncrementUserAIDailySpendParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
EffectiveGroupID uuid.UUID `db:"effective_group_id" json:"effective_group_id"`
Day time.Time `db:"day" json:"day"`
CostMicros int64 `db:"cost_micros" json:"cost_micros"`
}
// Adds cost_micros to the spend for (user_id, effective_group_id, day).
// The day parameter is normalized to its UTC calendar day before storage.
func (q *sqlQuerier) IncrementUserAIDailySpend(ctx context.Context, arg IncrementUserAIDailySpendParams) (AIUserDailySpend, error) {
row := q.db.QueryRowContext(ctx, incrementUserAIDailySpend,
arg.UserID,
arg.EffectiveGroupID,
arg.Day,
arg.CostMicros,
)
var i AIUserDailySpend
err := row.Scan(
&i.UserID,
&i.EffectiveGroupID,
&i.Day,
&i.SpendMicros,
)
return i, err
}
const upsertAIModelPrices = `-- name: UpsertAIModelPrices :exec
INSERT INTO ai_model_prices (
provider, model, input_price, output_price, cache_read_price, cache_write_price
)
SELECT
elem->>'provider',
elem->>'model',
(elem->>'input_price')::bigint,
(elem->>'output_price')::bigint,
(elem->>'cache_read_price')::bigint,
(elem->>'cache_write_price')::bigint
FROM jsonb_array_elements($1::jsonb) AS elem
ON CONFLICT (provider, model) DO UPDATE SET
input_price = EXCLUDED.input_price,
output_price = EXCLUDED.output_price,
cache_read_price = EXCLUDED.cache_read_price,
cache_write_price = EXCLUDED.cache_write_price,
updated_at = NOW()
WHERE (
ai_model_prices.input_price,
ai_model_prices.output_price,
ai_model_prices.cache_read_price,
ai_model_prices.cache_write_price
) IS DISTINCT FROM (
EXCLUDED.input_price,
EXCLUDED.output_price,
EXCLUDED.cache_read_price,
EXCLUDED.cache_write_price
)
`
// Upsert a batch of (provider, model) rows from a JSON array. Each element
// must have provider, model, and the four price fields; null prices are
// written as SQL NULL.
// A conflicting row is only rewritten when a price differs, so updated_at
// records when a price last changed. Prices are nullable and a NULL on
// either side counts as a difference.
func (q *sqlQuerier) UpsertAIModelPrices(ctx context.Context, seed json.RawMessage) error {
_, err := q.db.ExecContext(ctx, upsertAIModelPrices, seed)
return err
}
const upsertGroupAIBudget = `-- name: UpsertGroupAIBudget :one
INSERT INTO group_ai_budgets (group_id, spend_limit_micros)
VALUES ($1, $2)
ON CONFLICT (group_id) DO UPDATE SET
spend_limit_micros = EXCLUDED.spend_limit_micros,
updated_at = NOW()
RETURNING group_id, spend_limit_micros, created_at, updated_at
`
type UpsertGroupAIBudgetParams struct {
GroupID uuid.UUID `db:"group_id" json:"group_id"`
SpendLimitMicros int64 `db:"spend_limit_micros" json:"spend_limit_micros"`
}
func (q *sqlQuerier) UpsertGroupAIBudget(ctx context.Context, arg UpsertGroupAIBudgetParams) (GroupAIBudget, error) {
row := q.db.QueryRowContext(ctx, upsertGroupAIBudget, arg.GroupID, arg.SpendLimitMicros)
var i GroupAIBudget
err := row.Scan(
&i.GroupID,
&i.SpendLimitMicros,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const upsertUserAIBudgetOverride = `-- name: UpsertUserAIBudgetOverride :one
INSERT INTO user_ai_budget_overrides (user_id, group_id, spend_limit_micros)
VALUES ($1, $2, $3)
ON CONFLICT (user_id) DO UPDATE SET
group_id = EXCLUDED.group_id,
spend_limit_micros = EXCLUDED.spend_limit_micros,
updated_at = NOW()
RETURNING user_id, group_id, spend_limit_micros, created_at, updated_at
`
type UpsertUserAIBudgetOverrideParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
GroupID uuid.UUID `db:"group_id" json:"group_id"`
SpendLimitMicros int64 `db:"spend_limit_micros" json:"spend_limit_micros"`
}
func (q *sqlQuerier) UpsertUserAIBudgetOverride(ctx context.Context, arg UpsertUserAIBudgetOverrideParams) (UserAIBudgetOverride, error) {
row := q.db.QueryRowContext(ctx, upsertUserAIBudgetOverride, arg.UserID, arg.GroupID, arg.SpendLimitMicros)
var i UserAIBudgetOverride
err := row.Scan(
&i.UserID,
&i.GroupID,
&i.SpendLimitMicros,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getActiveAISeatCount = `-- name: GetActiveAISeatCount :one
SELECT
COUNT(*)
FROM
ai_seat_state ais
JOIN
users u
ON
ais.user_id = u.id
WHERE
u.status = 'active'::user_status
AND u.deleted = false
AND u.is_system = false
`
func (q *sqlQuerier) GetActiveAISeatCount(ctx context.Context) (int64, error) {
row := q.db.QueryRowContext(ctx, getActiveAISeatCount)
var count int64
err := row.Scan(&count)
return count, err
}
const upsertAISeatState = `-- name: UpsertAISeatState :one
INSERT INTO ai_seat_state (
user_id,
first_used_at,
last_used_at,
last_event_type,
last_event_description,
updated_at
)
VALUES
($1, $2, $2, $3, $4, $2)
ON CONFLICT (user_id) DO UPDATE
SET
last_used_at = EXCLUDED.last_used_at,
last_event_type = EXCLUDED.last_event_type,
last_event_description = EXCLUDED.last_event_description,
updated_at = EXCLUDED.updated_at
RETURNING
-- Postgres vodoo to know if a row was inserted.
(xmax = 0)::boolean AS is_new
`
type UpsertAISeatStateParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
FirstUsedAt time.Time `db:"first_used_at" json:"first_used_at"`
LastEventType AISeatUsageReason `db:"last_event_type" json:"last_event_type"`
LastEventDescription string `db:"last_event_description" json:"last_event_description"`
}
// Returns true if a new rows was inserted, false otherwise.
func (q *sqlQuerier) UpsertAISeatState(ctx context.Context, arg UpsertAISeatStateParams) (bool, error) {
row := q.db.QueryRowContext(ctx, upsertAISeatState,
arg.UserID,
arg.FirstUsedAt,
arg.LastEventType,
arg.LastEventDescription,
)
var is_new bool
err := row.Scan(&is_new)
return is_new, err
}
const getUserAISeatStates = `-- name: GetUserAISeatStates :many
SELECT
ais.user_id
FROM
ai_seat_state ais
JOIN
users u
ON
ais.user_id = u.id
WHERE
ais.user_id = ANY($1::uuid[])
AND u.status = 'active'::user_status
AND u.deleted = false
AND u.is_system = false
`
// Returns user IDs from the provided list that are consuming an AI seat.
// Filters to active, non-deleted, non-system users to match the canonical
// seat count query (GetActiveAISeatCount).
func (q *sqlQuerier) GetUserAISeatStates(ctx context.Context, userIds []uuid.UUID) ([]uuid.UUID, error) {
rows, err := q.db.QueryContext(ctx, getUserAISeatStates, pq.Array(userIds))
if err != nil {
return nil, err
}
defer rows.Close()
var items []uuid.UUID
for rows.Next() {
var user_id uuid.UUID
if err := rows.Scan(&user_id); err != nil {
return nil, err
}
items = append(items, user_id)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const deleteAPIKeyByID = `-- name: DeleteAPIKeyByID :exec
DELETE FROM
api_keys
WHERE
id = $1
`
func (q *sqlQuerier) DeleteAPIKeyByID(ctx context.Context, id string) error {
_, err := q.db.ExecContext(ctx, deleteAPIKeyByID, id)
return err
}
const deleteAPIKeysByUserID = `-- name: DeleteAPIKeysByUserID :exec
DELETE FROM
api_keys
WHERE
user_id = $1
`
func (q *sqlQuerier) DeleteAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteAPIKeysByUserID, userID)
return err
}
const deleteApplicationConnectAPIKeysByUserID = `-- name: DeleteApplicationConnectAPIKeysByUserID :exec
DELETE FROM
api_keys
WHERE
user_id = $1 AND
'coder:application_connect'::api_key_scope = ANY(scopes)
`
func (q *sqlQuerier) DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteApplicationConnectAPIKeysByUserID, userID)
return err
}
const deleteExpiredAPIKeys = `-- name: DeleteExpiredAPIKeys :execrows
WITH expired_keys AS (
SELECT id
FROM api_keys
-- expired keys only
WHERE expires_at < $1::timestamptz
LIMIT $2
)
DELETE FROM
api_keys
USING
expired_keys
WHERE
api_keys.id = expired_keys.id
`
type DeleteExpiredAPIKeysParams struct {
Before time.Time `db:"before" json:"before"`
LimitCount int32 `db:"limit_count" json:"limit_count"`
}
func (q *sqlQuerier) DeleteExpiredAPIKeys(ctx context.Context, arg DeleteExpiredAPIKeysParams) (int64, error) {
result, err := q.db.ExecContext(ctx, deleteExpiredAPIKeys, arg.Before, arg.LimitCount)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const expirePrebuildsAPIKeys = `-- name: ExpirePrebuildsAPIKeys :exec
WITH unexpired_prebuilds_workspace_session_tokens AS (
SELECT id, SUBSTRING(token_name FROM 38 FOR 36)::uuid AS workspace_id
FROM api_keys
WHERE user_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid
AND expires_at > $1::timestamptz
AND token_name SIMILAR TO 'c42fdf75-3097-471c-8c33-fb52454d81c0_[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}_session_token'
),
stale_prebuilds_workspace_session_tokens AS (
SELECT upwst.id
FROM unexpired_prebuilds_workspace_session_tokens upwst
LEFT JOIN workspaces w
ON w.id = upwst.workspace_id
WHERE w.owner_id <> 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid
),
unnamed_prebuilds_api_keys AS (
SELECT id
FROM api_keys
WHERE user_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid
AND token_name = ''
AND expires_at > $1::timestamptz
)
UPDATE api_keys
SET expires_at = $1::timestamptz
WHERE id IN (
SELECT id FROM stale_prebuilds_workspace_session_tokens
UNION
SELECT id FROM unnamed_prebuilds_api_keys
)
`
// Firstly, collect api_keys owned by the prebuilds user that correlate
// to workspaces no longer owned by the prebuilds user.
// Next, collect api_keys that belong to the prebuilds user but have no token name.
// These were most likely created via 'coder login' as the prebuilds user.
func (q *sqlQuerier) ExpirePrebuildsAPIKeys(ctx context.Context, now time.Time) error {
_, err := q.db.ExecContext(ctx, expirePrebuildsAPIKeys, now)
return err
}
const getAPIKeyByID = `-- name: GetAPIKeyByID :one
SELECT
id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, token_name, scopes, allow_list
FROM
api_keys
WHERE
id = $1
LIMIT
1
`
func (q *sqlQuerier) GetAPIKeyByID(ctx context.Context, id string) (APIKey, error) {
row := q.db.QueryRowContext(ctx, getAPIKeyByID, id)
var i APIKey
err := row.Scan(
&i.ID,
&i.HashedSecret,
&i.UserID,
&i.LastUsed,
&i.ExpiresAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.LoginType,
&i.LifetimeSeconds,
&i.IPAddress,
&i.TokenName,
&i.Scopes,
&i.AllowList,
)
return i, err
}
const getAPIKeyByName = `-- name: GetAPIKeyByName :one
SELECT
id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, token_name, scopes, allow_list
FROM
api_keys
WHERE
user_id = $1 AND
token_name = $2 AND
token_name != ''
LIMIT
1
`
type GetAPIKeyByNameParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
TokenName string `db:"token_name" json:"token_name"`
}
// there is no unique constraint on empty token names
func (q *sqlQuerier) GetAPIKeyByName(ctx context.Context, arg GetAPIKeyByNameParams) (APIKey, error) {
row := q.db.QueryRowContext(ctx, getAPIKeyByName, arg.UserID, arg.TokenName)
var i APIKey
err := row.Scan(
&i.ID,
&i.HashedSecret,
&i.UserID,
&i.LastUsed,
&i.ExpiresAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.LoginType,
&i.LifetimeSeconds,
&i.IPAddress,
&i.TokenName,
&i.Scopes,
&i.AllowList,
)
return i, err
}
const getAPIKeysByLoginType = `-- name: GetAPIKeysByLoginType :many
SELECT id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, token_name, scopes, allow_list FROM api_keys WHERE login_type = $1
AND ($2::bool OR expires_at > now())
`
type GetAPIKeysByLoginTypeParams struct {
LoginType LoginType `db:"login_type" json:"login_type"`
IncludeExpired bool `db:"include_expired" json:"include_expired"`
}
func (q *sqlQuerier) GetAPIKeysByLoginType(ctx context.Context, arg GetAPIKeysByLoginTypeParams) ([]APIKey, error) {
rows, err := q.db.QueryContext(ctx, getAPIKeysByLoginType, arg.LoginType, arg.IncludeExpired)
if err != nil {
return nil, err
}
defer rows.Close()
var items []APIKey
for rows.Next() {
var i APIKey
if err := rows.Scan(
&i.ID,
&i.HashedSecret,
&i.UserID,
&i.LastUsed,
&i.ExpiresAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.LoginType,
&i.LifetimeSeconds,
&i.IPAddress,
&i.TokenName,
&i.Scopes,
&i.AllowList,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getAPIKeysByUserID = `-- name: GetAPIKeysByUserID :many
SELECT id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, token_name, scopes, allow_list FROM api_keys WHERE login_type = $1 AND user_id = $2
AND ($3::bool OR expires_at > now())
`
type GetAPIKeysByUserIDParams struct {
LoginType LoginType `db:"login_type" json:"login_type"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
IncludeExpired bool `db:"include_expired" json:"include_expired"`
}
func (q *sqlQuerier) GetAPIKeysByUserID(ctx context.Context, arg GetAPIKeysByUserIDParams) ([]APIKey, error) {
rows, err := q.db.QueryContext(ctx, getAPIKeysByUserID, arg.LoginType, arg.UserID, arg.IncludeExpired)
if err != nil {
return nil, err
}
defer rows.Close()
var items []APIKey
for rows.Next() {
var i APIKey
if err := rows.Scan(
&i.ID,
&i.HashedSecret,
&i.UserID,
&i.LastUsed,
&i.ExpiresAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.LoginType,
&i.LifetimeSeconds,
&i.IPAddress,
&i.TokenName,
&i.Scopes,
&i.AllowList,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getAPIKeysLastUsedAfter = `-- name: GetAPIKeysLastUsedAfter :many
SELECT id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, token_name, scopes, allow_list FROM api_keys WHERE last_used > $1
`
func (q *sqlQuerier) GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.Time) ([]APIKey, error) {
rows, err := q.db.QueryContext(ctx, getAPIKeysLastUsedAfter, lastUsed)
if err != nil {
return nil, err
}
defer rows.Close()
var items []APIKey
for rows.Next() {
var i APIKey
if err := rows.Scan(
&i.ID,
&i.HashedSecret,
&i.UserID,
&i.LastUsed,
&i.ExpiresAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.LoginType,
&i.LifetimeSeconds,
&i.IPAddress,
&i.TokenName,
&i.Scopes,
&i.AllowList,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getChatGatewayAPIKey = `-- name: GetChatGatewayAPIKey :one
SELECT
id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, token_name, scopes, allow_list
FROM
api_keys
WHERE
user_id = $1 AND
token_name = $2 AND
-- Token names are unvalidated user input, so a user could create a token
-- with the chat gateway name. Excluding login_type 'token' ensures chatd
-- never picks up (and extends) a real bearer token. Synthetic gateway
-- keys are minted with the owner's login type, which is never 'token'.
login_type != 'token'
ORDER BY
created_at ASC, id ASC
LIMIT
1
`
type GetChatGatewayAPIKeyParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
TokenName string `db:"token_name" json:"token_name"`
}
func (q *sqlQuerier) GetChatGatewayAPIKey(ctx context.Context, arg GetChatGatewayAPIKeyParams) (APIKey, error) {
row := q.db.QueryRowContext(ctx, getChatGatewayAPIKey, arg.UserID, arg.TokenName)
var i APIKey
err := row.Scan(
&i.ID,
&i.HashedSecret,
&i.UserID,
&i.LastUsed,
&i.ExpiresAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.LoginType,
&i.LifetimeSeconds,
&i.IPAddress,
&i.TokenName,
&i.Scopes,
&i.AllowList,
)
return i, err
}
const insertAPIKey = `-- name: InsertAPIKey :one
INSERT INTO
api_keys (
id,
lifetime_seconds,
hashed_secret,
ip_address,
user_id,
last_used,
expires_at,
created_at,
updated_at,
login_type,
scopes,
allow_list,
token_name
)
VALUES
($1,
-- If the lifetime is set to 0, default to 24hrs
CASE $2::bigint
WHEN 0 THEN 86400
ELSE $2::bigint
END
, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, token_name, scopes, allow_list
`
type InsertAPIKeyParams struct {
ID string `db:"id" json:"id"`
LifetimeSeconds int64 `db:"lifetime_seconds" json:"lifetime_seconds"`
HashedSecret []byte `db:"hashed_secret" json:"hashed_secret"`
IPAddress pqtype.Inet `db:"ip_address" json:"ip_address"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
LastUsed time.Time `db:"last_used" json:"last_used"`
ExpiresAt time.Time `db:"expires_at" json:"expires_at"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
LoginType LoginType `db:"login_type" json:"login_type"`
Scopes APIKeyScopes `db:"scopes" json:"scopes"`
AllowList AllowList `db:"allow_list" json:"allow_list"`
TokenName string `db:"token_name" json:"token_name"`
}
func (q *sqlQuerier) InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error) {
row := q.db.QueryRowContext(ctx, insertAPIKey,
arg.ID,
arg.LifetimeSeconds,
arg.HashedSecret,
arg.IPAddress,
arg.UserID,
arg.LastUsed,
arg.ExpiresAt,
arg.CreatedAt,
arg.UpdatedAt,
arg.LoginType,
arg.Scopes,
arg.AllowList,
arg.TokenName,
)
var i APIKey
err := row.Scan(
&i.ID,
&i.HashedSecret,
&i.UserID,
&i.LastUsed,
&i.ExpiresAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.LoginType,
&i.LifetimeSeconds,
&i.IPAddress,
&i.TokenName,
&i.Scopes,
&i.AllowList,
)
return i, err
}
const updateAPIKeyByID = `-- name: UpdateAPIKeyByID :exec
UPDATE
api_keys
SET
last_used = $2,
expires_at = $3,
ip_address = $4
WHERE
id = $1
`
type UpdateAPIKeyByIDParams struct {
ID string `db:"id" json:"id"`
LastUsed time.Time `db:"last_used" json:"last_used"`
ExpiresAt time.Time `db:"expires_at" json:"expires_at"`
IPAddress pqtype.Inet `db:"ip_address" json:"ip_address"`
}
func (q *sqlQuerier) UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error {
_, err := q.db.ExecContext(ctx, updateAPIKeyByID,
arg.ID,
arg.LastUsed,
arg.ExpiresAt,
arg.IPAddress,
)
return err
}
const countAuditLogs = `-- name: CountAuditLogs :one
SELECT COUNT(*) FROM (
SELECT 1
FROM audit_logs
LEFT JOIN users ON audit_logs.user_id = users.id
LEFT JOIN organizations ON audit_logs.organization_id = organizations.id
-- First join on workspaces to get the initial workspace create
-- to workspace build 1 id. This is because the first create is
-- is a different audit log than subsequent starts.
LEFT JOIN workspaces ON audit_logs.resource_type = 'workspace'
AND audit_logs.resource_id = workspaces.id
-- Get the reason from the build if the resource type
-- is a workspace_build
LEFT JOIN workspace_builds wb_build ON audit_logs.resource_type = 'workspace_build'
AND audit_logs.resource_id = wb_build.id
-- Get the reason from the build #1 if this is the first
-- workspace create.
LEFT JOIN workspace_builds wb_workspace ON audit_logs.resource_type = 'workspace'
AND audit_logs.action = 'create'
AND workspaces.id = wb_workspace.workspace_id
AND wb_workspace.build_number = 1
WHERE
-- Filter resource_type
CASE
WHEN $1::text != '' THEN resource_type = $1::resource_type
ELSE true
END
-- Filter resource_id
AND CASE
WHEN $2::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN resource_id = $2
ELSE true
END
-- Filter organization_id
AND CASE
WHEN $3::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN audit_logs.organization_id = $3
ELSE true
END
-- Filter by resource_target
AND CASE
WHEN $4::text != '' THEN resource_target = $4
ELSE true
END
-- Filter action
AND CASE
WHEN $5::text != '' THEN action = $5::audit_action
ELSE true
END
-- Filter by user_id
AND CASE
WHEN $6::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN user_id = $6
ELSE true
END
-- Filter by username
AND CASE
WHEN $7::text != '' THEN user_id = (
SELECT id
FROM users
WHERE lower(username) = lower($7)
AND deleted = false
)
ELSE true
END
-- Filter by user_email
AND CASE
WHEN $8::text != '' THEN users.email = $8
ELSE true
END
-- Filter by date_from
AND CASE
WHEN $9::timestamp with time zone != '0001-01-01 00:00:00Z' THEN "time" >= $9
ELSE true
END
-- Filter by date_to
AND CASE
WHEN $10::timestamp with time zone != '0001-01-01 00:00:00Z' THEN "time" <= $10
ELSE true
END
-- Filter by build_reason
AND CASE
WHEN $11::text != '' THEN COALESCE(wb_build.reason::text, wb_workspace.reason::text) = $11
ELSE true
END
-- Filter request_id
AND CASE
WHEN $12::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN audit_logs.request_id = $12
ELSE true
END
-- Authorize Filter clause will be injected below in CountAuthorizedAuditLogs
-- @authorize_filter
-- Avoid a slow scan on a large table with joins. The caller
-- passes the count cap and we add 1 so the frontend can detect
-- capping and show "... of N+". A cap of 0 means no limit (NULLIF
-- -> NULL + 1 = NULL).
-- NOTE: Parameterizing this so that we can easily change from,
-- e.g., 2000 to 5000. However, use literal NULL (or no LIMIT)
-- here if disabling the capping on a large table permanently.
-- This way the PG planner can plan parallel execution for
-- potential large wins.
LIMIT NULLIF($13::int, 0) + 1
) AS limited_count
`
type CountAuditLogsParams struct {
ResourceType string `db:"resource_type" json:"resource_type"`
ResourceID uuid.UUID `db:"resource_id" json:"resource_id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
ResourceTarget string `db:"resource_target" json:"resource_target"`
Action string `db:"action" json:"action"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
Username string `db:"username" json:"username"`
Email string `db:"email" json:"email"`
DateFrom time.Time `db:"date_from" json:"date_from"`
DateTo time.Time `db:"date_to" json:"date_to"`
BuildReason string `db:"build_reason" json:"build_reason"`
RequestID uuid.UUID `db:"request_id" json:"request_id"`
CountCap int32 `db:"count_cap" json:"count_cap"`
}
func (q *sqlQuerier) CountAuditLogs(ctx context.Context, arg CountAuditLogsParams) (int64, error) {
row := q.db.QueryRowContext(ctx, countAuditLogs,
arg.ResourceType,
arg.ResourceID,
arg.OrganizationID,
arg.ResourceTarget,
arg.Action,
arg.UserID,
arg.Username,
arg.Email,
arg.DateFrom,
arg.DateTo,
arg.BuildReason,
arg.RequestID,
arg.CountCap,
)
var count int64
err := row.Scan(&count)
return count, err
}
const deleteOldAuditLogConnectionEvents = `-- name: DeleteOldAuditLogConnectionEvents :exec
DELETE FROM audit_logs
WHERE id IN (
SELECT id FROM audit_logs
WHERE
(
action = 'connect'
OR action = 'disconnect'
OR action = 'open'
OR action = 'close'
)
AND "time" < $1::timestamp with time zone
ORDER BY "time" ASC
LIMIT $2
)
`
type DeleteOldAuditLogConnectionEventsParams struct {
BeforeTime time.Time `db:"before_time" json:"before_time"`
LimitCount int32 `db:"limit_count" json:"limit_count"`
}
func (q *sqlQuerier) DeleteOldAuditLogConnectionEvents(ctx context.Context, arg DeleteOldAuditLogConnectionEventsParams) error {
_, err := q.db.ExecContext(ctx, deleteOldAuditLogConnectionEvents, arg.BeforeTime, arg.LimitCount)
return err
}
const deleteOldAuditLogs = `-- name: DeleteOldAuditLogs :execrows
WITH old_logs AS (
SELECT id
FROM audit_logs
WHERE
"time" < $1::timestamp with time zone
AND action NOT IN ('connect', 'disconnect', 'open', 'close')
ORDER BY "time" ASC
LIMIT $2
)
DELETE FROM audit_logs
USING old_logs
WHERE audit_logs.id = old_logs.id
`
type DeleteOldAuditLogsParams struct {
BeforeTime time.Time `db:"before_time" json:"before_time"`
LimitCount int32 `db:"limit_count" json:"limit_count"`
}
// Deletes old audit logs based on retention policy, excluding deprecated
// connection events (connect, disconnect, open, close) which are handled
// separately by DeleteOldAuditLogConnectionEvents.
func (q *sqlQuerier) DeleteOldAuditLogs(ctx context.Context, arg DeleteOldAuditLogsParams) (int64, error) {
result, err := q.db.ExecContext(ctx, deleteOldAuditLogs, arg.BeforeTime, arg.LimitCount)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const getAuditLogsOffset = `-- name: GetAuditLogsOffset :many
SELECT audit_logs.id, audit_logs.time, audit_logs.user_id, audit_logs.organization_id, audit_logs.ip, audit_logs.user_agent, audit_logs.resource_type, audit_logs.resource_id, audit_logs.resource_target, audit_logs.action, audit_logs.diff, audit_logs.status_code, audit_logs.additional_fields, audit_logs.request_id, audit_logs.resource_icon,
-- sqlc.embed(users) would be nice but it does not seem to play well with
-- left joins.
users.username AS user_username,
users.name AS user_name,
users.email AS user_email,
users.created_at AS user_created_at,
users.updated_at AS user_updated_at,
users.last_seen_at AS user_last_seen_at,
users.status AS user_status,
users.login_type AS user_login_type,
users.rbac_roles AS user_roles,
users.avatar_url AS user_avatar_url,
users.deleted AS user_deleted,
users.quiet_hours_schedule AS user_quiet_hours_schedule,
COALESCE(organizations.name, '') AS organization_name,
COALESCE(organizations.display_name, '') AS organization_display_name,
COALESCE(organizations.icon, '') AS organization_icon
FROM audit_logs
LEFT JOIN users ON audit_logs.user_id = users.id
LEFT JOIN organizations ON audit_logs.organization_id = organizations.id
-- First join on workspaces to get the initial workspace create
-- to workspace build 1 id. This is because the first create is
-- is a different audit log than subsequent starts.
LEFT JOIN workspaces ON audit_logs.resource_type = 'workspace'
AND audit_logs.resource_id = workspaces.id
-- Get the reason from the build if the resource type
-- is a workspace_build
LEFT JOIN workspace_builds wb_build ON audit_logs.resource_type = 'workspace_build'
AND audit_logs.resource_id = wb_build.id
-- Get the reason from the build #1 if this is the first
-- workspace create.
LEFT JOIN workspace_builds wb_workspace ON audit_logs.resource_type = 'workspace'
AND audit_logs.action = 'create'
AND workspaces.id = wb_workspace.workspace_id
AND wb_workspace.build_number = 1
WHERE
-- Filter resource_type
CASE
WHEN $1::text != '' THEN resource_type = $1::resource_type
ELSE true
END
-- Filter resource_id
AND CASE
WHEN $2::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN resource_id = $2
ELSE true
END
-- Filter organization_id
AND CASE
WHEN $3::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN audit_logs.organization_id = $3
ELSE true
END
-- Filter by resource_target
AND CASE
WHEN $4::text != '' THEN resource_target = $4
ELSE true
END
-- Filter action
AND CASE
WHEN $5::text != '' THEN action = $5::audit_action
ELSE true
END
-- Filter by user_id
AND CASE
WHEN $6::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN user_id = $6
ELSE true
END
-- Filter by username
AND CASE
WHEN $7::text != '' THEN user_id = (
SELECT id
FROM users
WHERE lower(username) = lower($7)
AND deleted = false
)
ELSE true
END
-- Filter by user_email
AND CASE
WHEN $8::text != '' THEN users.email = $8
ELSE true
END
-- Filter by date_from
AND CASE
WHEN $9::timestamp with time zone != '0001-01-01 00:00:00Z' THEN "time" >= $9
ELSE true
END
-- Filter by date_to
AND CASE
WHEN $10::timestamp with time zone != '0001-01-01 00:00:00Z' THEN "time" <= $10
ELSE true
END
-- Filter by build_reason
AND CASE
WHEN $11::text != '' THEN COALESCE(wb_build.reason::text, wb_workspace.reason::text) = $11
ELSE true
END
-- Filter request_id
AND CASE
WHEN $12::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN audit_logs.request_id = $12
ELSE true
END
-- Authorize Filter clause will be injected below in GetAuthorizedAuditLogsOffset
-- @authorize_filter
ORDER BY "time" DESC
LIMIT -- a limit of 0 means "no limit". The audit log table is unbounded
-- in size, and is expected to be quite large. Implement a default
-- limit of 100 to prevent accidental excessively large queries.
COALESCE(NULLIF($14::int, 0), 100) OFFSET $13
`
type GetAuditLogsOffsetParams struct {
ResourceType string `db:"resource_type" json:"resource_type"`
ResourceID uuid.UUID `db:"resource_id" json:"resource_id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
ResourceTarget string `db:"resource_target" json:"resource_target"`
Action string `db:"action" json:"action"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
Username string `db:"username" json:"username"`
Email string `db:"email" json:"email"`
DateFrom time.Time `db:"date_from" json:"date_from"`
DateTo time.Time `db:"date_to" json:"date_to"`
BuildReason string `db:"build_reason" json:"build_reason"`
RequestID uuid.UUID `db:"request_id" json:"request_id"`
OffsetOpt int32 `db:"offset_opt" json:"offset_opt"`
LimitOpt int32 `db:"limit_opt" json:"limit_opt"`
}
type GetAuditLogsOffsetRow struct {
AuditLog AuditLog `db:"audit_log" json:"audit_log"`
UserUsername sql.NullString `db:"user_username" json:"user_username"`
UserName sql.NullString `db:"user_name" json:"user_name"`
UserEmail sql.NullString `db:"user_email" json:"user_email"`
UserCreatedAt sql.NullTime `db:"user_created_at" json:"user_created_at"`
UserUpdatedAt sql.NullTime `db:"user_updated_at" json:"user_updated_at"`
UserLastSeenAt sql.NullTime `db:"user_last_seen_at" json:"user_last_seen_at"`
UserStatus NullUserStatus `db:"user_status" json:"user_status"`
UserLoginType NullLoginType `db:"user_login_type" json:"user_login_type"`
UserRoles pq.StringArray `db:"user_roles" json:"user_roles"`
UserAvatarUrl sql.NullString `db:"user_avatar_url" json:"user_avatar_url"`
UserDeleted sql.NullBool `db:"user_deleted" json:"user_deleted"`
UserQuietHoursSchedule sql.NullString `db:"user_quiet_hours_schedule" json:"user_quiet_hours_schedule"`
OrganizationName string `db:"organization_name" json:"organization_name"`
OrganizationDisplayName string `db:"organization_display_name" json:"organization_display_name"`
OrganizationIcon string `db:"organization_icon" json:"organization_icon"`
}
// GetAuditLogsBefore retrieves `row_limit` number of audit logs before the provided
// ID.
func (q *sqlQuerier) GetAuditLogsOffset(ctx context.Context, arg GetAuditLogsOffsetParams) ([]GetAuditLogsOffsetRow, error) {
rows, err := q.db.QueryContext(ctx, getAuditLogsOffset,
arg.ResourceType,
arg.ResourceID,
arg.OrganizationID,
arg.ResourceTarget,
arg.Action,
arg.UserID,
arg.Username,
arg.Email,
arg.DateFrom,
arg.DateTo,
arg.BuildReason,
arg.RequestID,
arg.OffsetOpt,
arg.LimitOpt,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetAuditLogsOffsetRow
for rows.Next() {
var i GetAuditLogsOffsetRow
if err := rows.Scan(
&i.AuditLog.ID,
&i.AuditLog.Time,
&i.AuditLog.UserID,
&i.AuditLog.OrganizationID,
&i.AuditLog.Ip,
&i.AuditLog.UserAgent,
&i.AuditLog.ResourceType,
&i.AuditLog.ResourceID,
&i.AuditLog.ResourceTarget,
&i.AuditLog.Action,
&i.AuditLog.Diff,
&i.AuditLog.StatusCode,
&i.AuditLog.AdditionalFields,
&i.AuditLog.RequestID,
&i.AuditLog.ResourceIcon,
&i.UserUsername,
&i.UserName,
&i.UserEmail,
&i.UserCreatedAt,
&i.UserUpdatedAt,
&i.UserLastSeenAt,
&i.UserStatus,
&i.UserLoginType,
&i.UserRoles,
&i.UserAvatarUrl,
&i.UserDeleted,
&i.UserQuietHoursSchedule,
&i.OrganizationName,
&i.OrganizationDisplayName,
&i.OrganizationIcon,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertAuditLog = `-- name: InsertAuditLog :one
INSERT INTO audit_logs (
id,
"time",
user_id,
organization_id,
ip,
user_agent,
resource_type,
resource_id,
resource_target,
action,
diff,
status_code,
additional_fields,
request_id,
resource_icon
)
VALUES (
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
$9,
$10,
$11,
$12,
$13,
$14,
$15
)
RETURNING id, time, user_id, organization_id, ip, user_agent, resource_type, resource_id, resource_target, action, diff, status_code, additional_fields, request_id, resource_icon
`
type InsertAuditLogParams struct {
ID uuid.UUID `db:"id" json:"id"`
Time time.Time `db:"time" json:"time"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
Ip pqtype.Inet `db:"ip" json:"ip"`
UserAgent sql.NullString `db:"user_agent" json:"user_agent"`
ResourceType ResourceType `db:"resource_type" json:"resource_type"`
ResourceID uuid.UUID `db:"resource_id" json:"resource_id"`
ResourceTarget string `db:"resource_target" json:"resource_target"`
Action AuditAction `db:"action" json:"action"`
Diff json.RawMessage `db:"diff" json:"diff"`
StatusCode int32 `db:"status_code" json:"status_code"`
AdditionalFields json.RawMessage `db:"additional_fields" json:"additional_fields"`
RequestID uuid.UUID `db:"request_id" json:"request_id"`
ResourceIcon string `db:"resource_icon" json:"resource_icon"`
}
func (q *sqlQuerier) InsertAuditLog(ctx context.Context, arg InsertAuditLogParams) (AuditLog, error) {
row := q.db.QueryRowContext(ctx, insertAuditLog,
arg.ID,
arg.Time,
arg.UserID,
arg.OrganizationID,
arg.Ip,
arg.UserAgent,
arg.ResourceType,
arg.ResourceID,
arg.ResourceTarget,
arg.Action,
arg.Diff,
arg.StatusCode,
arg.AdditionalFields,
arg.RequestID,
arg.ResourceIcon,
)
var i AuditLog
err := row.Scan(
&i.ID,
&i.Time,
&i.UserID,
&i.OrganizationID,
&i.Ip,
&i.UserAgent,
&i.ResourceType,
&i.ResourceID,
&i.ResourceTarget,
&i.Action,
&i.Diff,
&i.StatusCode,
&i.AdditionalFields,
&i.RequestID,
&i.ResourceIcon,
)
return i, err
}
const deleteOldBoundaryLogs = `-- name: DeleteOldBoundaryLogs :execrows
WITH old_logs AS (
SELECT id
FROM boundary_logs
WHERE captured_at < $1::timestamptz
ORDER BY captured_at ASC
LIMIT $2
)
DELETE FROM boundary_logs
USING old_logs
WHERE boundary_logs.id = old_logs.id
`
type DeleteOldBoundaryLogsParams struct {
BeforeTime time.Time `db:"before_time" json:"before_time"`
LimitCount int32 `db:"limit_count" json:"limit_count"`
}
// Deletes boundary logs older than the given time, bounded by a row limit
// to avoid long-running transactions.
func (q *sqlQuerier) DeleteOldBoundaryLogs(ctx context.Context, arg DeleteOldBoundaryLogsParams) (int64, error) {
result, err := q.db.ExecContext(ctx, deleteOldBoundaryLogs, arg.BeforeTime, arg.LimitCount)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const deleteOldBoundarySessions = `-- name: DeleteOldBoundarySessions :execrows
WITH old_sessions AS (
SELECT bs.id
FROM boundary_sessions bs
WHERE bs.updated_at < $1::timestamptz
AND NOT EXISTS (
SELECT 1 FROM boundary_logs bl WHERE bl.session_id = bs.id
)
ORDER BY bs.updated_at ASC
LIMIT $2
)
DELETE FROM boundary_sessions
USING old_sessions
WHERE boundary_sessions.id = old_sessions.id
`
type DeleteOldBoundarySessionsParams struct {
BeforeTime time.Time `db:"before_time" json:"before_time"`
LimitCount int32 `db:"limit_count" json:"limit_count"`
}
// Deletes boundary sessions that have aged past retention and no longer
// have any associated logs.
func (q *sqlQuerier) DeleteOldBoundarySessions(ctx context.Context, arg DeleteOldBoundarySessionsParams) (int64, error) {
result, err := q.db.ExecContext(ctx, deleteOldBoundarySessions, arg.BeforeTime, arg.LimitCount)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const getBoundaryLogByID = `-- name: GetBoundaryLogByID :one
SELECT id, session_id, sequence_number, captured_at, created_at, proto, method, detail, matched_rule, owner_id FROM boundary_logs WHERE id = $1
`
func (q *sqlQuerier) GetBoundaryLogByID(ctx context.Context, id uuid.UUID) (BoundaryLog, error) {
row := q.db.QueryRowContext(ctx, getBoundaryLogByID, id)
var i BoundaryLog
err := row.Scan(
&i.ID,
&i.SessionID,
&i.SequenceNumber,
&i.CapturedAt,
&i.CreatedAt,
&i.Proto,
&i.Method,
&i.Detail,
&i.MatchedRule,
&i.OwnerID,
)
return i, err
}
const getBoundarySessionByID = `-- name: GetBoundarySessionByID :one
SELECT
bs.id, bs.workspace_agent_id, bs.confined_process_name, bs.started_at, bs.updated_at, bs.owner_id,
w.id AS workspace_id,
w.owner_id AS workspace_owner_id
FROM
boundary_sessions bs
JOIN
workspace_agents wa ON wa.id = bs.workspace_agent_id
JOIN
workspace_resources wr ON wr.id = wa.resource_id
JOIN
workspace_builds wb ON wb.job_id = wr.job_id
JOIN
workspaces w ON w.id = wb.workspace_id
WHERE
bs.id = $1
`
type GetBoundarySessionByIDRow struct {
ID uuid.UUID `db:"id" json:"id"`
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
ConfinedProcessName string `db:"confined_process_name" json:"confined_process_name"`
StartedAt time.Time `db:"started_at" json:"started_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
OwnerID uuid.NullUUID `db:"owner_id" json:"owner_id"`
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
WorkspaceOwnerID uuid.UUID `db:"workspace_owner_id" json:"workspace_owner_id"`
}
func (q *sqlQuerier) GetBoundarySessionByID(ctx context.Context, id uuid.UUID) (GetBoundarySessionByIDRow, error) {
row := q.db.QueryRowContext(ctx, getBoundarySessionByID, id)
var i GetBoundarySessionByIDRow
err := row.Scan(
&i.ID,
&i.WorkspaceAgentID,
&i.ConfinedProcessName,
&i.StartedAt,
&i.UpdatedAt,
&i.OwnerID,
&i.WorkspaceID,
&i.WorkspaceOwnerID,
)
return i, err
}
const insertBoundaryLogs = `-- name: InsertBoundaryLogs :many
INSERT INTO boundary_logs (
id,
session_id,
owner_id,
sequence_number,
captured_at,
created_at,
proto,
method,
detail,
matched_rule
)
SELECT
unnest($1 :: uuid[]),
$2 :: uuid,
$3 :: uuid,
unnest($4 :: int[]),
unnest($5 :: timestamptz[]),
unnest($6 :: timestamptz[]),
unnest($7 :: text[]),
unnest($8 :: text[]),
unnest($9 :: text[]),
NULLIF(unnest($10 :: text[]), '')
RETURNING id, session_id, sequence_number, captured_at, created_at, proto, method, detail, matched_rule, owner_id
`
type InsertBoundaryLogsParams struct {
ID []uuid.UUID `db:"id" json:"id"`
SessionID uuid.UUID `db:"session_id" json:"session_id"`
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
SequenceNumber []int32 `db:"sequence_number" json:"sequence_number"`
CapturedAt []time.Time `db:"captured_at" json:"captured_at"`
CreatedAt []time.Time `db:"created_at" json:"created_at"`
Proto []string `db:"proto" json:"proto"`
Method []string `db:"method" json:"method"`
Detail []string `db:"detail" json:"detail"`
MatchedRule []string `db:"matched_rule" json:"matched_rule"`
}
func (q *sqlQuerier) InsertBoundaryLogs(ctx context.Context, arg InsertBoundaryLogsParams) ([]BoundaryLog, error) {
rows, err := q.db.QueryContext(ctx, insertBoundaryLogs,
pq.Array(arg.ID),
arg.SessionID,
arg.OwnerID,
pq.Array(arg.SequenceNumber),
pq.Array(arg.CapturedAt),
pq.Array(arg.CreatedAt),
pq.Array(arg.Proto),
pq.Array(arg.Method),
pq.Array(arg.Detail),
pq.Array(arg.MatchedRule),
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []BoundaryLog
for rows.Next() {
var i BoundaryLog
if err := rows.Scan(
&i.ID,
&i.SessionID,
&i.SequenceNumber,
&i.CapturedAt,
&i.CreatedAt,
&i.Proto,
&i.Method,
&i.Detail,
&i.MatchedRule,
&i.OwnerID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertBoundarySession = `-- name: InsertBoundarySession :one
INSERT INTO boundary_sessions (
id,
workspace_agent_id,
owner_id,
confined_process_name,
started_at,
updated_at
) VALUES (
$1,
$2,
$3,
$4,
$5,
$6
) RETURNING id, workspace_agent_id, confined_process_name, started_at, updated_at, owner_id
`
type InsertBoundarySessionParams struct {
ID uuid.UUID `db:"id" json:"id"`
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
OwnerID uuid.NullUUID `db:"owner_id" json:"owner_id"`
ConfinedProcessName string `db:"confined_process_name" json:"confined_process_name"`
StartedAt time.Time `db:"started_at" json:"started_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
func (q *sqlQuerier) InsertBoundarySession(ctx context.Context, arg InsertBoundarySessionParams) (BoundarySession, error) {
row := q.db.QueryRowContext(ctx, insertBoundarySession,
arg.ID,
arg.WorkspaceAgentID,
arg.OwnerID,
arg.ConfinedProcessName,
arg.StartedAt,
arg.UpdatedAt,
)
var i BoundarySession
err := row.Scan(
&i.ID,
&i.WorkspaceAgentID,
&i.ConfinedProcessName,
&i.StartedAt,
&i.UpdatedAt,
&i.OwnerID,
)
return i, err
}
const listBoundaryLogsBySessionID = `-- name: ListBoundaryLogsBySessionID :many
SELECT id, session_id, sequence_number, captured_at, created_at, proto, method, detail, matched_rule, owner_id
FROM boundary_logs
WHERE
session_id = $1
AND CASE
WHEN $2::int IS NOT NULL THEN sequence_number >= $2
ELSE true
END
AND CASE
WHEN $3::int IS NOT NULL THEN sequence_number < $3
ELSE true
END
ORDER BY sequence_number ASC
LIMIT COALESCE(NULLIF($4::int, 0), 100)
`
type ListBoundaryLogsBySessionIDParams struct {
SessionID uuid.UUID `db:"session_id" json:"session_id"`
SeqAfter sql.NullInt32 `db:"seq_after" json:"seq_after"`
SeqBefore sql.NullInt32 `db:"seq_before" json:"seq_before"`
LimitOpt int32 `db:"limit_opt" json:"limit_opt"`
}
// Lists boundary logs for a session, sorted by sequence number ascending.
// Supports an inclusive lower bound (seq_after) and an exclusive upper bound
// (seq_before) for fetching events between two known interceptions.
func (q *sqlQuerier) ListBoundaryLogsBySessionID(ctx context.Context, arg ListBoundaryLogsBySessionIDParams) ([]BoundaryLog, error) {
rows, err := q.db.QueryContext(ctx, listBoundaryLogsBySessionID,
arg.SessionID,
arg.SeqAfter,
arg.SeqBefore,
arg.LimitOpt,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []BoundaryLog
for rows.Next() {
var i BoundaryLog
if err := rows.Scan(
&i.ID,
&i.SessionID,
&i.SequenceNumber,
&i.CapturedAt,
&i.CreatedAt,
&i.Proto,
&i.Method,
&i.Detail,
&i.MatchedRule,
&i.OwnerID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getAndResetBoundaryUsageSummary = `-- name: GetAndResetBoundaryUsageSummary :one
WITH deleted AS (
DELETE FROM boundary_usage_stats
RETURNING replica_id, unique_workspaces_count, unique_users_count, allowed_requests, denied_requests, window_start, updated_at
)
SELECT
COALESCE(SUM(unique_workspaces_count) FILTER (
WHERE window_start >= NOW() - ($1::bigint || ' ms')::interval
), 0)::bigint AS unique_workspaces,
COALESCE(SUM(unique_users_count) FILTER (
WHERE window_start >= NOW() - ($1::bigint || ' ms')::interval
), 0)::bigint AS unique_users,
COALESCE(SUM(allowed_requests) FILTER (
WHERE window_start >= NOW() - ($1::bigint || ' ms')::interval
), 0)::bigint AS allowed_requests,
COALESCE(SUM(denied_requests) FILTER (
WHERE window_start >= NOW() - ($1::bigint || ' ms')::interval
), 0)::bigint AS denied_requests
FROM deleted
`
type GetAndResetBoundaryUsageSummaryRow struct {
UniqueWorkspaces int64 `db:"unique_workspaces" json:"unique_workspaces"`
UniqueUsers int64 `db:"unique_users" json:"unique_users"`
AllowedRequests int64 `db:"allowed_requests" json:"allowed_requests"`
DeniedRequests int64 `db:"denied_requests" json:"denied_requests"`
}
// Atomic read+delete prevents replicas that flush between a separate read and
// reset from having their data deleted before the next snapshot. Uses a common
// table expression with DELETE...RETURNING so the rows we sum are exactly the
// rows we delete. Stale rows are excluded from the sum but still deleted.
func (q *sqlQuerier) GetAndResetBoundaryUsageSummary(ctx context.Context, maxStalenessMs int64) (GetAndResetBoundaryUsageSummaryRow, error) {
row := q.db.QueryRowContext(ctx, getAndResetBoundaryUsageSummary, maxStalenessMs)
var i GetAndResetBoundaryUsageSummaryRow
err := row.Scan(
&i.UniqueWorkspaces,
&i.UniqueUsers,
&i.AllowedRequests,
&i.DeniedRequests,
)
return i, err
}
const upsertBoundaryUsageStats = `-- name: UpsertBoundaryUsageStats :one
INSERT INTO boundary_usage_stats (
replica_id,
unique_workspaces_count,
unique_users_count,
allowed_requests,
denied_requests,
window_start,
updated_at
) VALUES (
$1,
$2,
$3,
$4,
$5,
NOW(),
NOW()
) ON CONFLICT (replica_id) DO UPDATE SET
unique_workspaces_count = $6,
unique_users_count = $7,
allowed_requests = boundary_usage_stats.allowed_requests + EXCLUDED.allowed_requests,
denied_requests = boundary_usage_stats.denied_requests + EXCLUDED.denied_requests,
updated_at = NOW()
RETURNING (xmax = 0) AS new_period
`
type UpsertBoundaryUsageStatsParams struct {
ReplicaID uuid.UUID `db:"replica_id" json:"replica_id"`
UniqueWorkspacesDelta int64 `db:"unique_workspaces_delta" json:"unique_workspaces_delta"`
UniqueUsersDelta int64 `db:"unique_users_delta" json:"unique_users_delta"`
AllowedRequests int64 `db:"allowed_requests" json:"allowed_requests"`
DeniedRequests int64 `db:"denied_requests" json:"denied_requests"`
UniqueWorkspacesCount int64 `db:"unique_workspaces_count" json:"unique_workspaces_count"`
UniqueUsersCount int64 `db:"unique_users_count" json:"unique_users_count"`
}
// Upserts boundary usage statistics for a replica. On INSERT (new period), uses
// delta values for unique counts (only data since last flush). On UPDATE, uses
// cumulative values for unique counts (accurate period totals). Request counts
// are always deltas, accumulated in DB. Returns true if insert, false if update.
func (q *sqlQuerier) UpsertBoundaryUsageStats(ctx context.Context, arg UpsertBoundaryUsageStatsParams) (bool, error) {
row := q.db.QueryRowContext(ctx, upsertBoundaryUsageStats,
arg.ReplicaID,
arg.UniqueWorkspacesDelta,
arg.UniqueUsersDelta,
arg.AllowedRequests,
arg.DeniedRequests,
arg.UniqueWorkspacesCount,
arg.UniqueUsersCount,
)
var new_period bool
err := row.Scan(&new_period)
return new_period, err
}
const deleteChatDebugDataAfterMessageID = `-- name: DeleteChatDebugDataAfterMessageID :execrows
WITH affected_runs AS (
SELECT DISTINCT run.id
FROM chat_debug_runs run
WHERE run.chat_id = $1::uuid
AND run.started_at < $2::timestamptz
AND (
run.history_tip_message_id > $3::bigint
OR run.trigger_message_id > $3::bigint
)
UNION
SELECT DISTINCT step.run_id AS id
FROM chat_debug_steps step
JOIN chat_debug_runs run ON run.id = step.run_id
AND run.chat_id = step.chat_id
WHERE step.chat_id = $1::uuid
AND run.started_at < $2::timestamptz
AND (
step.assistant_message_id > $3::bigint
OR step.history_tip_message_id > $3::bigint
)
)
DELETE FROM chat_debug_runs
WHERE chat_id = $1::uuid
AND id IN (SELECT id FROM affected_runs)
`
type DeleteChatDebugDataAfterMessageIDParams struct {
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
StartedBefore time.Time `db:"started_before" json:"started_before"`
MessageID int64 `db:"message_id" json:"message_id"`
}
// Deletes debug runs (and their cascaded steps) whose message IDs
// exceed the cutoff. The started_before bound prevents retried
// cleanup from deleting runs created by a replacement turn that
// raced ahead of the retry window.
func (q *sqlQuerier) DeleteChatDebugDataAfterMessageID(ctx context.Context, arg DeleteChatDebugDataAfterMessageIDParams) (int64, error) {
result, err := q.db.ExecContext(ctx, deleteChatDebugDataAfterMessageID, arg.ChatID, arg.StartedBefore, arg.MessageID)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const deleteChatDebugDataByChatID = `-- name: DeleteChatDebugDataByChatID :execrows
DELETE FROM chat_debug_runs
WHERE chat_id = $1::uuid
AND started_at < $2::timestamptz
`
type DeleteChatDebugDataByChatIDParams struct {
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
StartedBefore time.Time `db:"started_before" json:"started_before"`
}
// The started_before bound prevents retried cleanup from deleting
// runs created by a replacement turn that races ahead of the retry
// window (for example, after an unarchive races with a pending
// archive-cleanup retry).
func (q *sqlQuerier) DeleteChatDebugDataByChatID(ctx context.Context, arg DeleteChatDebugDataByChatIDParams) (int64, error) {
result, err := q.db.ExecContext(ctx, deleteChatDebugDataByChatID, arg.ChatID, arg.StartedBefore)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const deleteOldChatDebugRuns = `-- name: DeleteOldChatDebugRuns :execrows
WITH deletable AS (
SELECT id, chat_id
FROM chat_debug_runs
WHERE updated_at < $1::timestamptz
ORDER BY updated_at ASC
LIMIT $2::int
)
DELETE FROM chat_debug_runs
USING deletable
WHERE chat_debug_runs.id = deletable.id
AND chat_debug_runs.chat_id = deletable.chat_id
`
type DeleteOldChatDebugRunsParams struct {
BeforeTime time.Time `db:"before_time" json:"before_time"`
LimitCount int32 `db:"limit_count" json:"limit_count"`
}
// updated_at is the retention clock, so the window starts after the run
// stops being written to.
// Intentionally no finished_at IS NOT NULL guard: abandoned in-flight rows
// older than the cutoff are also purged.
func (q *sqlQuerier) DeleteOldChatDebugRuns(ctx context.Context, arg DeleteOldChatDebugRunsParams) (int64, error) {
result, err := q.db.ExecContext(ctx, deleteOldChatDebugRuns, arg.BeforeTime, arg.LimitCount)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const finalizeStaleChatDebugRows = `-- name: FinalizeStaleChatDebugRows :one
WITH finalized_runs AS (
UPDATE chat_debug_runs
SET
status = 'interrupted',
updated_at = $1::timestamptz,
finished_at = $1::timestamptz
WHERE updated_at < $2::timestamptz
AND finished_at IS NULL
AND status NOT IN ('completed', 'error', 'interrupted')
RETURNING id
), finalized_steps AS (
UPDATE chat_debug_steps
SET
status = 'interrupted',
updated_at = $1::timestamptz,
finished_at = $1::timestamptz
WHERE (
updated_at < $2::timestamptz
OR run_id IN (SELECT id FROM finalized_runs)
)
AND finished_at IS NULL
AND status NOT IN ('completed', 'error', 'interrupted')
RETURNING 1
)
SELECT
(SELECT COUNT(*) FROM finalized_runs)::bigint AS runs_finalized,
(SELECT COUNT(*) FROM finalized_steps)::bigint AS steps_finalized
`
type FinalizeStaleChatDebugRowsParams struct {
Now time.Time `db:"now" json:"now"`
UpdatedBefore time.Time `db:"updated_before" json:"updated_before"`
}
type FinalizeStaleChatDebugRowsRow struct {
RunsFinalized int64 `db:"runs_finalized" json:"runs_finalized"`
StepsFinalized int64 `db:"steps_finalized" json:"steps_finalized"`
}
// Marks orphaned in-progress rows as interrupted so they do not stay
// in a non-terminal state forever. The NOT IN list must match the
// terminal statuses defined by ChatDebugStatus in codersdk/chats.go.
//
// The steps CTE also catches steps whose parent run was just finalized
// (via run_id IN), because PostgreSQL data-modifying CTEs share the
// same snapshot and cannot see each other's row updates. Without this,
// a step with a recent updated_at would survive its run's finalization
// and remain in 'in_progress' state permanently.
//
// @now is the caller's clock timestamp so that mock-clock tests stay
// consistent with the @updated_before cutoff.
func (q *sqlQuerier) FinalizeStaleChatDebugRows(ctx context.Context, arg FinalizeStaleChatDebugRowsParams) (FinalizeStaleChatDebugRowsRow, error) {
row := q.db.QueryRowContext(ctx, finalizeStaleChatDebugRows, arg.Now, arg.UpdatedBefore)
var i FinalizeStaleChatDebugRowsRow
err := row.Scan(&i.RunsFinalized, &i.StepsFinalized)
return i, err
}
const getChatDebugRunByID = `-- name: GetChatDebugRunByID :one
SELECT id, chat_id, root_chat_id, parent_chat_id, model_config_id, trigger_message_id, history_tip_message_id, kind, status, provider, model, summary, started_at, updated_at, finished_at
FROM chat_debug_runs
WHERE id = $1::uuid
`
func (q *sqlQuerier) GetChatDebugRunByID(ctx context.Context, id uuid.UUID) (ChatDebugRun, error) {
row := q.db.QueryRowContext(ctx, getChatDebugRunByID, id)
var i ChatDebugRun
err := row.Scan(
&i.ID,
&i.ChatID,
&i.RootChatID,
&i.ParentChatID,
&i.ModelConfigID,
&i.TriggerMessageID,
&i.HistoryTipMessageID,
&i.Kind,
&i.Status,
&i.Provider,
&i.Model,
&i.Summary,
&i.StartedAt,
&i.UpdatedAt,
&i.FinishedAt,
)
return i, err
}
const getChatDebugRunsByChatID = `-- name: GetChatDebugRunsByChatID :many
SELECT id, chat_id, root_chat_id, parent_chat_id, model_config_id, trigger_message_id, history_tip_message_id, kind, status, provider, model, summary, started_at, updated_at, finished_at
FROM chat_debug_runs
WHERE chat_id = $1::uuid
ORDER BY started_at DESC, id DESC
LIMIT $2::int
`
type GetChatDebugRunsByChatIDParams struct {
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
LimitVal int32 `db:"limit_val" json:"limit_val"`
}
// Returns the most recent debug runs for a chat, ordered newest-first.
// Callers must supply an explicit limit to avoid unbounded result sets.
func (q *sqlQuerier) GetChatDebugRunsByChatID(ctx context.Context, arg GetChatDebugRunsByChatIDParams) ([]ChatDebugRun, error) {
rows, err := q.db.QueryContext(ctx, getChatDebugRunsByChatID, arg.ChatID, arg.LimitVal)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ChatDebugRun
for rows.Next() {
var i ChatDebugRun
if err := rows.Scan(
&i.ID,
&i.ChatID,
&i.RootChatID,
&i.ParentChatID,
&i.ModelConfigID,
&i.TriggerMessageID,
&i.HistoryTipMessageID,
&i.Kind,
&i.Status,
&i.Provider,
&i.Model,
&i.Summary,
&i.StartedAt,
&i.UpdatedAt,
&i.FinishedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getChatDebugStepsByRunID = `-- name: GetChatDebugStepsByRunID :many
SELECT id, run_id, chat_id, step_number, operation, status, history_tip_message_id, assistant_message_id, normalized_request, normalized_response, usage, attempts, error, metadata, started_at, updated_at, finished_at
FROM chat_debug_steps
WHERE run_id = $1::uuid
ORDER BY step_number ASC, started_at ASC
`
func (q *sqlQuerier) GetChatDebugStepsByRunID(ctx context.Context, runID uuid.UUID) ([]ChatDebugStep, error) {
rows, err := q.db.QueryContext(ctx, getChatDebugStepsByRunID, runID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ChatDebugStep
for rows.Next() {
var i ChatDebugStep
if err := rows.Scan(
&i.ID,
&i.RunID,
&i.ChatID,
&i.StepNumber,
&i.Operation,
&i.Status,
&i.HistoryTipMessageID,
&i.AssistantMessageID,
&i.NormalizedRequest,
&i.NormalizedResponse,
&i.Usage,
&i.Attempts,
&i.Error,
&i.Metadata,
&i.StartedAt,
&i.UpdatedAt,
&i.FinishedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertChatDebugRun = `-- name: InsertChatDebugRun :one
INSERT INTO chat_debug_runs (
chat_id,
root_chat_id,
parent_chat_id,
model_config_id,
trigger_message_id,
history_tip_message_id,
kind,
status,
provider,
model,
summary,
started_at,
updated_at,
finished_at
)
VALUES (
$1::uuid,
$2::uuid,
$3::uuid,
$4::uuid,
$5::bigint,
$6::bigint,
$7::text,
$8::text,
$9::text,
$10::text,
COALESCE($11::jsonb, '{}'::jsonb),
COALESCE($12::timestamptz, NOW()),
COALESCE($13::timestamptz, NOW()),
$14::timestamptz
)
RETURNING id, chat_id, root_chat_id, parent_chat_id, model_config_id, trigger_message_id, history_tip_message_id, kind, status, provider, model, summary, started_at, updated_at, finished_at
`
type InsertChatDebugRunParams struct {
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
RootChatID uuid.NullUUID `db:"root_chat_id" json:"root_chat_id"`
ParentChatID uuid.NullUUID `db:"parent_chat_id" json:"parent_chat_id"`
ModelConfigID uuid.NullUUID `db:"model_config_id" json:"model_config_id"`
TriggerMessageID sql.NullInt64 `db:"trigger_message_id" json:"trigger_message_id"`
HistoryTipMessageID sql.NullInt64 `db:"history_tip_message_id" json:"history_tip_message_id"`
Kind string `db:"kind" json:"kind"`
Status string `db:"status" json:"status"`
Provider sql.NullString `db:"provider" json:"provider"`
Model sql.NullString `db:"model" json:"model"`
Summary pqtype.NullRawMessage `db:"summary" json:"summary"`
StartedAt sql.NullTime `db:"started_at" json:"started_at"`
UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"`
FinishedAt sql.NullTime `db:"finished_at" json:"finished_at"`
}
// updated_at is the retention clock used by DeleteOldChatDebugRuns.
// Set it on every write to keep retention semantics correct.
func (q *sqlQuerier) InsertChatDebugRun(ctx context.Context, arg InsertChatDebugRunParams) (ChatDebugRun, error) {
row := q.db.QueryRowContext(ctx, insertChatDebugRun,
arg.ChatID,
arg.RootChatID,
arg.ParentChatID,
arg.ModelConfigID,
arg.TriggerMessageID,
arg.HistoryTipMessageID,
arg.Kind,
arg.Status,
arg.Provider,
arg.Model,
arg.Summary,
arg.StartedAt,
arg.UpdatedAt,
arg.FinishedAt,
)
var i ChatDebugRun
err := row.Scan(
&i.ID,
&i.ChatID,
&i.RootChatID,
&i.ParentChatID,
&i.ModelConfigID,
&i.TriggerMessageID,
&i.HistoryTipMessageID,
&i.Kind,
&i.Status,
&i.Provider,
&i.Model,
&i.Summary,
&i.StartedAt,
&i.UpdatedAt,
&i.FinishedAt,
)
return i, err
}
const insertChatDebugStep = `-- name: InsertChatDebugStep :one
WITH locked_run AS (
UPDATE chat_debug_runs
SET updated_at = COALESCE($14::timestamptz, NOW())
WHERE id = $1::uuid
AND chat_id = $16::uuid
AND finished_at IS NULL
RETURNING chat_id
)
INSERT INTO chat_debug_steps (
run_id,
chat_id,
step_number,
operation,
status,
history_tip_message_id,
assistant_message_id,
normalized_request,
normalized_response,
usage,
attempts,
error,
metadata,
started_at,
updated_at,
finished_at
)
SELECT
$1::uuid,
locked_run.chat_id,
$2::int,
$3::text,
$4::text,
$5::bigint,
$6::bigint,
COALESCE($7::jsonb, '{}'::jsonb),
$8::jsonb,
$9::jsonb,
COALESCE($10::jsonb, '[]'::jsonb),
$11::jsonb,
COALESCE($12::jsonb, '{}'::jsonb),
COALESCE($13::timestamptz, NOW()),
COALESCE($14::timestamptz, NOW()),
$15::timestamptz
FROM locked_run
RETURNING id, run_id, chat_id, step_number, operation, status, history_tip_message_id, assistant_message_id, normalized_request, normalized_response, usage, attempts, error, metadata, started_at, updated_at, finished_at
`
type InsertChatDebugStepParams struct {
RunID uuid.UUID `db:"run_id" json:"run_id"`
StepNumber int32 `db:"step_number" json:"step_number"`
Operation string `db:"operation" json:"operation"`
Status string `db:"status" json:"status"`
HistoryTipMessageID sql.NullInt64 `db:"history_tip_message_id" json:"history_tip_message_id"`
AssistantMessageID sql.NullInt64 `db:"assistant_message_id" json:"assistant_message_id"`
NormalizedRequest pqtype.NullRawMessage `db:"normalized_request" json:"normalized_request"`
NormalizedResponse pqtype.NullRawMessage `db:"normalized_response" json:"normalized_response"`
Usage pqtype.NullRawMessage `db:"usage" json:"usage"`
Attempts pqtype.NullRawMessage `db:"attempts" json:"attempts"`
Error pqtype.NullRawMessage `db:"error" json:"error"`
Metadata pqtype.NullRawMessage `db:"metadata" json:"metadata"`
StartedAt sql.NullTime `db:"started_at" json:"started_at"`
UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"`
FinishedAt sql.NullTime `db:"finished_at" json:"finished_at"`
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
}
// The CTE atomically locks the parent run via UPDATE, bumps its
// updated_at (eliminating a separate TouchChatDebugRunUpdatedAt
// call), and enforces the finalization guard: if the run is already
// finished, the UPDATE returns zero rows, the INSERT gets no source
// rows, and sql.ErrNoRows is returned. The UPDATE also serializes
// with concurrent FinalizeStale under READ COMMITTED isolation.
func (q *sqlQuerier) InsertChatDebugStep(ctx context.Context, arg InsertChatDebugStepParams) (ChatDebugStep, error) {
row := q.db.QueryRowContext(ctx, insertChatDebugStep,
arg.RunID,
arg.StepNumber,
arg.Operation,
arg.Status,
arg.HistoryTipMessageID,
arg.AssistantMessageID,
arg.NormalizedRequest,
arg.NormalizedResponse,
arg.Usage,
arg.Attempts,
arg.Error,
arg.Metadata,
arg.StartedAt,
arg.UpdatedAt,
arg.FinishedAt,
arg.ChatID,
)
var i ChatDebugStep
err := row.Scan(
&i.ID,
&i.RunID,
&i.ChatID,
&i.StepNumber,
&i.Operation,
&i.Status,
&i.HistoryTipMessageID,
&i.AssistantMessageID,
&i.NormalizedRequest,
&i.NormalizedResponse,
&i.Usage,
&i.Attempts,
&i.Error,
&i.Metadata,
&i.StartedAt,
&i.UpdatedAt,
&i.FinishedAt,
)
return i, err
}
const touchChatDebugRunUpdatedAt = `-- name: TouchChatDebugRunUpdatedAt :exec
UPDATE chat_debug_runs
SET updated_at = $1::timestamptz
WHERE id = $2::uuid
AND chat_id = $3::uuid
`
type TouchChatDebugRunUpdatedAtParams struct {
Now time.Time `db:"now" json:"now"`
ID uuid.UUID `db:"id" json:"id"`
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
}
// Overrides updated_at on the parent run without touching any
// other column. Used by tests that need to stamp a run with a
// specific timestamp after the InsertChatDebugStep CTE has
// already bumped it to NOW(), so stale-row finalization paths
// can be exercised deterministically. The chatdebug service
// itself does not call this: heartbeats go through
// TouchChatDebugStepAndRun, and step creation updates the parent
// run via the InsertChatDebugStep CTE.
func (q *sqlQuerier) TouchChatDebugRunUpdatedAt(ctx context.Context, arg TouchChatDebugRunUpdatedAtParams) error {
_, err := q.db.ExecContext(ctx, touchChatDebugRunUpdatedAt, arg.Now, arg.ID, arg.ChatID)
return err
}
const touchChatDebugStepAndRun = `-- name: TouchChatDebugStepAndRun :exec
WITH touched_run AS (
UPDATE chat_debug_runs
SET updated_at = $1::timestamptz
WHERE id = $3::uuid
AND chat_id = $4::uuid
RETURNING id, chat_id
)
UPDATE chat_debug_steps
SET updated_at = $1::timestamptz
FROM touched_run
WHERE chat_debug_steps.id = $2::uuid
AND chat_debug_steps.run_id = touched_run.id
AND chat_debug_steps.chat_id = touched_run.chat_id
`
type TouchChatDebugStepAndRunParams struct {
Now time.Time `db:"now" json:"now"`
StepID uuid.UUID `db:"step_id" json:"step_id"`
RunID uuid.UUID `db:"run_id" json:"run_id"`
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
}
// Atomically bumps updated_at on both the step and its parent run
// in a single statement. This prevents FinalizeStale from
// interleaving between the two touches and finalizing a run whose
// step heartbeat was just written.
//
// The step UPDATE joins through touched_run (via FROM) and reads
// its RETURNING rows. Per the PostgreSQL WITH semantics, RETURNING
// is the only way to communicate values between a data-modifying
// CTE and the main query, and consuming those rows forces the run
// UPDATE to complete before the step UPDATE. That matches the
// lock order used by FinalizeStaleChatDebugRows and avoids a
// deadlock between concurrent heartbeats and stale sweeps. The
// join also constrains the step update to the specified run so a
// mismatched (run_id, step_id) pair cannot silently refresh an
// unrelated step.
func (q *sqlQuerier) TouchChatDebugStepAndRun(ctx context.Context, arg TouchChatDebugStepAndRunParams) error {
_, err := q.db.ExecContext(ctx, touchChatDebugStepAndRun,
arg.Now,
arg.StepID,
arg.RunID,
arg.ChatID,
)
return err
}
const updateChatDebugRun = `-- name: UpdateChatDebugRun :one
UPDATE chat_debug_runs
SET
root_chat_id = COALESCE($1::uuid, root_chat_id),
parent_chat_id = COALESCE($2::uuid, parent_chat_id),
model_config_id = COALESCE($3::uuid, model_config_id),
trigger_message_id = COALESCE($4::bigint, trigger_message_id),
history_tip_message_id = COALESCE($5::bigint, history_tip_message_id),
status = COALESCE($6::text, status),
provider = COALESCE($7::text, provider),
model = COALESCE($8::text, model),
summary = COALESCE($9::jsonb, summary),
finished_at = COALESCE(finished_at, $10::timestamptz),
updated_at = $11::timestamptz
WHERE id = $12::uuid
AND chat_id = $13::uuid
RETURNING id, chat_id, root_chat_id, parent_chat_id, model_config_id, trigger_message_id, history_tip_message_id, kind, status, provider, model, summary, started_at, updated_at, finished_at
`
type UpdateChatDebugRunParams struct {
RootChatID uuid.NullUUID `db:"root_chat_id" json:"root_chat_id"`
ParentChatID uuid.NullUUID `db:"parent_chat_id" json:"parent_chat_id"`
ModelConfigID uuid.NullUUID `db:"model_config_id" json:"model_config_id"`
TriggerMessageID sql.NullInt64 `db:"trigger_message_id" json:"trigger_message_id"`
HistoryTipMessageID sql.NullInt64 `db:"history_tip_message_id" json:"history_tip_message_id"`
Status sql.NullString `db:"status" json:"status"`
Provider sql.NullString `db:"provider" json:"provider"`
Model sql.NullString `db:"model" json:"model"`
Summary pqtype.NullRawMessage `db:"summary" json:"summary"`
FinishedAt sql.NullTime `db:"finished_at" json:"finished_at"`
Now time.Time `db:"now" json:"now"`
ID uuid.UUID `db:"id" json:"id"`
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
}
// Uses COALESCE so that passing NULL from Go means "keep the
// existing value." This is intentional: debug rows follow a
// write-once-finalize pattern where fields are set at creation
// or finalization and never cleared back to NULL. The @now
// parameter keeps updated_at under the caller's clock.
// updated_at is also the retention clock used by DeleteOldChatDebugRuns.
//
// finished_at is enforced as write-once at the SQL level: once
// populated it cannot be overwritten by a later call. Callers
// that issue a summary or status refresh after the run has
// already finalized therefore cannot corrupt the original
// completion timestamp, which keeps duration and ordering
// calculations stable regardless of how many times the row is
// updated.
func (q *sqlQuerier) UpdateChatDebugRun(ctx context.Context, arg UpdateChatDebugRunParams) (ChatDebugRun, error) {
row := q.db.QueryRowContext(ctx, updateChatDebugRun,
arg.RootChatID,
arg.ParentChatID,
arg.ModelConfigID,
arg.TriggerMessageID,
arg.HistoryTipMessageID,
arg.Status,
arg.Provider,
arg.Model,
arg.Summary,
arg.FinishedAt,
arg.Now,
arg.ID,
arg.ChatID,
)
var i ChatDebugRun
err := row.Scan(
&i.ID,
&i.ChatID,
&i.RootChatID,
&i.ParentChatID,
&i.ModelConfigID,
&i.TriggerMessageID,
&i.HistoryTipMessageID,
&i.Kind,
&i.Status,
&i.Provider,
&i.Model,
&i.Summary,
&i.StartedAt,
&i.UpdatedAt,
&i.FinishedAt,
)
return i, err
}
const updateChatDebugStep = `-- name: UpdateChatDebugStep :one
UPDATE chat_debug_steps
SET
status = COALESCE($1::text, status),
history_tip_message_id = COALESCE($2::bigint, history_tip_message_id),
assistant_message_id = COALESCE($3::bigint, assistant_message_id),
normalized_request = COALESCE($4::jsonb, normalized_request),
normalized_response = COALESCE($5::jsonb, normalized_response),
usage = COALESCE($6::jsonb, usage),
attempts = COALESCE($7::jsonb, attempts),
error = COALESCE($8::jsonb, error),
metadata = COALESCE($9::jsonb, metadata),
finished_at = COALESCE($10::timestamptz, finished_at),
updated_at = $11::timestamptz
WHERE id = $12::uuid
AND chat_id = $13::uuid
RETURNING id, run_id, chat_id, step_number, operation, status, history_tip_message_id, assistant_message_id, normalized_request, normalized_response, usage, attempts, error, metadata, started_at, updated_at, finished_at
`
type UpdateChatDebugStepParams struct {
Status sql.NullString `db:"status" json:"status"`
HistoryTipMessageID sql.NullInt64 `db:"history_tip_message_id" json:"history_tip_message_id"`
AssistantMessageID sql.NullInt64 `db:"assistant_message_id" json:"assistant_message_id"`
NormalizedRequest pqtype.NullRawMessage `db:"normalized_request" json:"normalized_request"`
NormalizedResponse pqtype.NullRawMessage `db:"normalized_response" json:"normalized_response"`
Usage pqtype.NullRawMessage `db:"usage" json:"usage"`
Attempts pqtype.NullRawMessage `db:"attempts" json:"attempts"`
Error pqtype.NullRawMessage `db:"error" json:"error"`
Metadata pqtype.NullRawMessage `db:"metadata" json:"metadata"`
FinishedAt sql.NullTime `db:"finished_at" json:"finished_at"`
Now time.Time `db:"now" json:"now"`
ID uuid.UUID `db:"id" json:"id"`
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
}
// Uses COALESCE so that passing NULL from Go means "keep the
// existing value." This is intentional: debug rows follow a
// write-once-finalize pattern where fields are set at creation
// or finalization and never cleared back to NULL. The @now
// parameter keeps updated_at under the caller's clock, matching
// the injectable quartz.Clock used by FinalizeStale sweeps.
func (q *sqlQuerier) UpdateChatDebugStep(ctx context.Context, arg UpdateChatDebugStepParams) (ChatDebugStep, error) {
row := q.db.QueryRowContext(ctx, updateChatDebugStep,
arg.Status,
arg.HistoryTipMessageID,
arg.AssistantMessageID,
arg.NormalizedRequest,
arg.NormalizedResponse,
arg.Usage,
arg.Attempts,
arg.Error,
arg.Metadata,
arg.FinishedAt,
arg.Now,
arg.ID,
arg.ChatID,
)
var i ChatDebugStep
err := row.Scan(
&i.ID,
&i.RunID,
&i.ChatID,
&i.StepNumber,
&i.Operation,
&i.Status,
&i.HistoryTipMessageID,
&i.AssistantMessageID,
&i.NormalizedRequest,
&i.NormalizedResponse,
&i.Usage,
&i.Attempts,
&i.Error,
&i.Metadata,
&i.StartedAt,
&i.UpdatedAt,
&i.FinishedAt,
)
return i, err
}
const deleteUnlinkedChatFilesByIDs = `-- name: DeleteUnlinkedChatFilesByIDs :execrows
DELETE FROM chat_files cf
WHERE cf.id = ANY($1::uuid[])
AND cf.created_at < $2::timestamptz
AND NOT EXISTS (
SELECT 1 FROM chat_file_links cfl WHERE cfl.file_id = cf.id
)
`
type DeleteUnlinkedChatFilesByIDsParams struct {
IDs []uuid.UUID `db:"ids" json:"ids"`
BeforeTime time.Time `db:"before_time" json:"before_time"`
}
func (q *sqlQuerier) DeleteUnlinkedChatFilesByIDs(ctx context.Context, arg DeleteUnlinkedChatFilesByIDsParams) (int64, error) {
result, err := q.db.ExecContext(ctx, deleteUnlinkedChatFilesByIDs, pq.Array(arg.IDs), arg.BeforeTime)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const getChatFileByID = `-- name: GetChatFileByID :one
SELECT id, owner_id, organization_id, created_at, name, mimetype, data FROM chat_files WHERE id = $1::uuid
`
func (q *sqlQuerier) GetChatFileByID(ctx context.Context, id uuid.UUID) (ChatFile, error) {
row := q.db.QueryRowContext(ctx, getChatFileByID, id)
var i ChatFile
err := row.Scan(
&i.ID,
&i.OwnerID,
&i.OrganizationID,
&i.CreatedAt,
&i.Name,
&i.Mimetype,
&i.Data,
)
return i, err
}
const getChatFileDataPrefixesByIDs = `-- name: GetChatFileDataPrefixesByIDs :many
SELECT id, owner_id, organization_id, substr(data, 1, $1::int) AS data_prefix
FROM chat_files
WHERE id = ANY($2::uuid[])
`
type GetChatFileDataPrefixesByIDsParams struct {
PrefixBytes int32 `db:"prefix_bytes" json:"prefix_bytes"`
IDs []uuid.UUID `db:"ids" json:"ids"`
}
type GetChatFileDataPrefixesByIDsRow struct {
ID uuid.UUID `db:"id" json:"id"`
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
DataPrefix []byte `db:"data_prefix" json:"data_prefix"`
}
// GetChatFileDataPrefixesByIDs returns a bounded prefix of each
// file's content, keeping full blobs out of server memory. Owner and
// organization columns support row-level authorization.
func (q *sqlQuerier) GetChatFileDataPrefixesByIDs(ctx context.Context, arg GetChatFileDataPrefixesByIDsParams) ([]GetChatFileDataPrefixesByIDsRow, error) {
rows, err := q.db.QueryContext(ctx, getChatFileDataPrefixesByIDs, arg.PrefixBytes, pq.Array(arg.IDs))
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetChatFileDataPrefixesByIDsRow
for rows.Next() {
var i GetChatFileDataPrefixesByIDsRow
if err := rows.Scan(
&i.ID,
&i.OwnerID,
&i.OrganizationID,
&i.DataPrefix,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getChatFileMetadataByChatID = `-- name: GetChatFileMetadataByChatID :many
SELECT cf.id, cf.owner_id, cf.organization_id, cf.name, cf.mimetype, cf.created_at
FROM chat_files cf
JOIN chat_file_links cfl ON cfl.file_id = cf.id
WHERE cfl.chat_id = $1::uuid
ORDER BY cf.created_at ASC
`
type GetChatFileMetadataByChatIDRow struct {
ID uuid.UUID `db:"id" json:"id"`
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
Name string `db:"name" json:"name"`
Mimetype string `db:"mimetype" json:"mimetype"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
// GetChatFileMetadataByChatID returns lightweight file metadata for
// all files linked to a chat. The data column is excluded to avoid
// loading file content.
func (q *sqlQuerier) GetChatFileMetadataByChatID(ctx context.Context, chatID uuid.UUID) ([]GetChatFileMetadataByChatIDRow, error) {
rows, err := q.db.QueryContext(ctx, getChatFileMetadataByChatID, chatID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetChatFileMetadataByChatIDRow
for rows.Next() {
var i GetChatFileMetadataByChatIDRow
if err := rows.Scan(
&i.ID,
&i.OwnerID,
&i.OrganizationID,
&i.Name,
&i.Mimetype,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getChatFilesByIDs = `-- name: GetChatFilesByIDs :many
SELECT id, owner_id, organization_id, created_at, name, mimetype, data FROM chat_files WHERE id = ANY($1::uuid[])
`
func (q *sqlQuerier) GetChatFilesByIDs(ctx context.Context, ids []uuid.UUID) ([]ChatFile, error) {
rows, err := q.db.QueryContext(ctx, getChatFilesByIDs, pq.Array(ids))
if err != nil {
return nil, err
}
defer rows.Close()
var items []ChatFile
for rows.Next() {
var i ChatFile
if err := rows.Scan(
&i.ID,
&i.OwnerID,
&i.OrganizationID,
&i.CreatedAt,
&i.Name,
&i.Mimetype,
&i.Data,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getOldUnlinkedChatFileIDs = `-- name: GetOldUnlinkedChatFileIDs :many
SELECT cf.id
FROM chat_files cf
WHERE cf.created_at < $1::timestamptz
AND NOT EXISTS (
SELECT 1 FROM chat_file_links cfl WHERE cfl.file_id = cf.id
)
ORDER BY cf.created_at ASC
LIMIT $2
FOR UPDATE OF cf SKIP LOCKED
`
type GetOldUnlinkedChatFileIDsParams struct {
BeforeTime time.Time `db:"before_time" json:"before_time"`
LimitCount int32 `db:"limit_count" json:"limit_count"`
}
// Locks candidate rows against foreign-key inserts for the transaction.
func (q *sqlQuerier) GetOldUnlinkedChatFileIDs(ctx context.Context, arg GetOldUnlinkedChatFileIDsParams) ([]uuid.UUID, error) {
rows, err := q.db.QueryContext(ctx, getOldUnlinkedChatFileIDs, arg.BeforeTime, arg.LimitCount)
if err != nil {
return nil, err
}
defer rows.Close()
var items []uuid.UUID
for rows.Next() {
var id uuid.UUID
if err := rows.Scan(&id); err != nil {
return nil, err
}
items = append(items, id)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertChatFile = `-- name: InsertChatFile :one
INSERT INTO chat_files (owner_id, organization_id, name, mimetype, data)
VALUES ($1::uuid, $2::uuid, $3::text, $4::text, $5::bytea)
RETURNING id, owner_id, organization_id, created_at, name, mimetype
`
type InsertChatFileParams struct {
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
Name string `db:"name" json:"name"`
Mimetype string `db:"mimetype" json:"mimetype"`
Data []byte `db:"data" json:"data"`
}
type InsertChatFileRow struct {
ID uuid.UUID `db:"id" json:"id"`
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
Name string `db:"name" json:"name"`
Mimetype string `db:"mimetype" json:"mimetype"`
}
func (q *sqlQuerier) InsertChatFile(ctx context.Context, arg InsertChatFileParams) (InsertChatFileRow, error) {
row := q.db.QueryRowContext(ctx, insertChatFile,
arg.OwnerID,
arg.OrganizationID,
arg.Name,
arg.Mimetype,
arg.Data,
)
var i InsertChatFileRow
err := row.Scan(
&i.ID,
&i.OwnerID,
&i.OrganizationID,
&i.CreatedAt,
&i.Name,
&i.Mimetype,
)
return i, err
}
const deleteChatModelConfigByID = `-- name: DeleteChatModelConfigByID :exec
UPDATE
chat_model_configs
SET
deleted = TRUE,
deleted_at = NOW(),
updated_at = NOW()
WHERE
id = $1::uuid
`
func (q *sqlQuerier) DeleteChatModelConfigByID(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteChatModelConfigByID, id)
return err
}
const deleteChatModelConfigsByAIProviderID = `-- name: DeleteChatModelConfigsByAIProviderID :exec
UPDATE
chat_model_configs
SET
deleted = TRUE,
deleted_at = NOW(),
updated_at = NOW()
WHERE
ai_provider_id = $1::uuid
AND deleted = FALSE
`
func (q *sqlQuerier) DeleteChatModelConfigsByAIProviderID(ctx context.Context, aiProviderID uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteChatModelConfigsByAIProviderID, aiProviderID)
return err
}
const getChatModelConfigByID = `-- name: GetChatModelConfigByID :one
SELECT
id, model, display_name, created_by, updated_by, enabled, is_default, deleted, deleted_at, created_at, updated_at, context_limit, compression_threshold, options, ai_provider_id
FROM
chat_model_configs
WHERE
id = $1::uuid
AND deleted = FALSE
`
func (q *sqlQuerier) GetChatModelConfigByID(ctx context.Context, id uuid.UUID) (ChatModelConfig, error) {
row := q.db.QueryRowContext(ctx, getChatModelConfigByID, id)
var i ChatModelConfig
err := row.Scan(
&i.ID,
&i.Model,
&i.DisplayName,
&i.CreatedBy,
&i.UpdatedBy,
&i.Enabled,
&i.IsDefault,
&i.Deleted,
&i.DeletedAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ContextLimit,
&i.CompressionThreshold,
&i.Options,
&i.AIProviderID,
)
return i, err
}
const getChatModelConfigs = `-- name: GetChatModelConfigs :many
SELECT
cmc.id, cmc.model, cmc.display_name, cmc.created_by, cmc.updated_by, cmc.enabled, cmc.is_default, cmc.deleted, cmc.deleted_at, cmc.created_at, cmc.updated_at, cmc.context_limit, cmc.compression_threshold, cmc.options, cmc.ai_provider_id
FROM
chat_model_configs cmc
LEFT JOIN
ai_providers ap ON ap.id = cmc.ai_provider_id
WHERE
cmc.deleted = FALSE
ORDER BY
ap.type::text ASC,
cmc.model ASC,
cmc.updated_at DESC,
cmc.id DESC
`
func (q *sqlQuerier) GetChatModelConfigs(ctx context.Context) ([]ChatModelConfig, error) {
rows, err := q.db.QueryContext(ctx, getChatModelConfigs)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ChatModelConfig
for rows.Next() {
var i ChatModelConfig
if err := rows.Scan(
&i.ID,
&i.Model,
&i.DisplayName,
&i.CreatedBy,
&i.UpdatedBy,
&i.Enabled,
&i.IsDefault,
&i.Deleted,
&i.DeletedAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ContextLimit,
&i.CompressionThreshold,
&i.Options,
&i.AIProviderID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getDefaultChatModelConfig = `-- name: GetDefaultChatModelConfig :one
SELECT
id, model, display_name, created_by, updated_by, enabled, is_default, deleted, deleted_at, created_at, updated_at, context_limit, compression_threshold, options, ai_provider_id
FROM
chat_model_configs
WHERE
is_default = TRUE
AND deleted = FALSE
`
func (q *sqlQuerier) GetDefaultChatModelConfig(ctx context.Context) (ChatModelConfig, error) {
row := q.db.QueryRowContext(ctx, getDefaultChatModelConfig)
var i ChatModelConfig
err := row.Scan(
&i.ID,
&i.Model,
&i.DisplayName,
&i.CreatedBy,
&i.UpdatedBy,
&i.Enabled,
&i.IsDefault,
&i.Deleted,
&i.DeletedAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ContextLimit,
&i.CompressionThreshold,
&i.Options,
&i.AIProviderID,
)
return i, err
}
const getEnabledChatModelConfigByID = `-- name: GetEnabledChatModelConfigByID :one
SELECT
cmc.id, cmc.model, cmc.display_name, cmc.created_by, cmc.updated_by, cmc.enabled, cmc.is_default, cmc.deleted, cmc.deleted_at, cmc.created_at, cmc.updated_at, cmc.context_limit, cmc.compression_threshold, cmc.options, cmc.ai_provider_id
FROM
chat_model_configs cmc
JOIN
ai_providers ap ON ap.id = cmc.ai_provider_id
WHERE
cmc.id = $1::uuid
AND cmc.deleted = FALSE
AND cmc.enabled = TRUE
AND ap.enabled = TRUE
AND ap.deleted = FALSE
`
// Providers can be disabled independently of their model configs.
// Check both to ensure the selected config is actually usable.
func (q *sqlQuerier) GetEnabledChatModelConfigByID(ctx context.Context, id uuid.UUID) (ChatModelConfig, error) {
row := q.db.QueryRowContext(ctx, getEnabledChatModelConfigByID, id)
var i ChatModelConfig
err := row.Scan(
&i.ID,
&i.Model,
&i.DisplayName,
&i.CreatedBy,
&i.UpdatedBy,
&i.Enabled,
&i.IsDefault,
&i.Deleted,
&i.DeletedAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ContextLimit,
&i.CompressionThreshold,
&i.Options,
&i.AIProviderID,
)
return i, err
}
const getEnabledChatModelConfigs = `-- name: GetEnabledChatModelConfigs :many
SELECT
cmc.id, cmc.model, cmc.display_name, cmc.created_by, cmc.updated_by, cmc.enabled, cmc.is_default, cmc.deleted, cmc.deleted_at, cmc.created_at, cmc.updated_at, cmc.context_limit, cmc.compression_threshold, cmc.options, cmc.ai_provider_id,
ap.type::text AS provider
FROM
chat_model_configs cmc
JOIN
ai_providers ap ON ap.id = cmc.ai_provider_id
WHERE
cmc.enabled = TRUE
AND cmc.deleted = FALSE
AND ap.enabled = TRUE
AND ap.deleted = FALSE
ORDER BY
ap.type::text ASC,
cmc.model ASC,
cmc.updated_at DESC,
cmc.id DESC
`
type GetEnabledChatModelConfigsRow struct {
ChatModelConfig ChatModelConfig `db:"chat_model_config" json:"chat_model_config"`
Provider string `db:"provider" json:"provider"`
}
func (q *sqlQuerier) GetEnabledChatModelConfigs(ctx context.Context) ([]GetEnabledChatModelConfigsRow, error) {
rows, err := q.db.QueryContext(ctx, getEnabledChatModelConfigs)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetEnabledChatModelConfigsRow
for rows.Next() {
var i GetEnabledChatModelConfigsRow
if err := rows.Scan(
&i.ChatModelConfig.ID,
&i.ChatModelConfig.Model,
&i.ChatModelConfig.DisplayName,
&i.ChatModelConfig.CreatedBy,
&i.ChatModelConfig.UpdatedBy,
&i.ChatModelConfig.Enabled,
&i.ChatModelConfig.IsDefault,
&i.ChatModelConfig.Deleted,
&i.ChatModelConfig.DeletedAt,
&i.ChatModelConfig.CreatedAt,
&i.ChatModelConfig.UpdatedAt,
&i.ChatModelConfig.ContextLimit,
&i.ChatModelConfig.CompressionThreshold,
&i.ChatModelConfig.Options,
&i.ChatModelConfig.AIProviderID,
&i.Provider,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertChatModelConfig = `-- name: InsertChatModelConfig :one
INSERT INTO chat_model_configs (
model,
display_name,
created_by,
updated_by,
enabled,
is_default,
context_limit,
compression_threshold,
options,
ai_provider_id
) VALUES (
$1::text,
$2::text,
$3::uuid,
$4::uuid,
$5::boolean,
$6::boolean,
$7::bigint,
$8::integer,
$9::jsonb,
$10::uuid
)
RETURNING
id, model, display_name, created_by, updated_by, enabled, is_default, deleted, deleted_at, created_at, updated_at, context_limit, compression_threshold, options, ai_provider_id
`
type InsertChatModelConfigParams struct {
Model string `db:"model" json:"model"`
DisplayName string `db:"display_name" json:"display_name"`
CreatedBy uuid.NullUUID `db:"created_by" json:"created_by"`
UpdatedBy uuid.NullUUID `db:"updated_by" json:"updated_by"`
Enabled bool `db:"enabled" json:"enabled"`
IsDefault bool `db:"is_default" json:"is_default"`
ContextLimit int64 `db:"context_limit" json:"context_limit"`
CompressionThreshold int32 `db:"compression_threshold" json:"compression_threshold"`
Options json.RawMessage `db:"options" json:"options"`
AIProviderID uuid.NullUUID `db:"ai_provider_id" json:"ai_provider_id"`
}
func (q *sqlQuerier) InsertChatModelConfig(ctx context.Context, arg InsertChatModelConfigParams) (ChatModelConfig, error) {
row := q.db.QueryRowContext(ctx, insertChatModelConfig,
arg.Model,
arg.DisplayName,
arg.CreatedBy,
arg.UpdatedBy,
arg.Enabled,
arg.IsDefault,
arg.ContextLimit,
arg.CompressionThreshold,
arg.Options,
arg.AIProviderID,
)
var i ChatModelConfig
err := row.Scan(
&i.ID,
&i.Model,
&i.DisplayName,
&i.CreatedBy,
&i.UpdatedBy,
&i.Enabled,
&i.IsDefault,
&i.Deleted,
&i.DeletedAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ContextLimit,
&i.CompressionThreshold,
&i.Options,
&i.AIProviderID,
)
return i, err
}
const unsetDefaultChatModelConfigs = `-- name: UnsetDefaultChatModelConfigs :exec
UPDATE
chat_model_configs
SET
is_default = FALSE,
updated_at = NOW()
WHERE
is_default = TRUE
AND deleted = FALSE
`
func (q *sqlQuerier) UnsetDefaultChatModelConfigs(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, unsetDefaultChatModelConfigs)
return err
}
const updateChatModelConfig = `-- name: UpdateChatModelConfig :one
UPDATE
chat_model_configs
SET
model = $1::text,
display_name = $2::text,
updated_by = $3::uuid,
enabled = $4::boolean,
is_default = $5::boolean,
context_limit = $6::bigint,
compression_threshold = $7::integer,
options = $8::jsonb,
ai_provider_id = $9::uuid,
updated_at = NOW()
WHERE
id = $10::uuid
AND deleted = FALSE
RETURNING
id, model, display_name, created_by, updated_by, enabled, is_default, deleted, deleted_at, created_at, updated_at, context_limit, compression_threshold, options, ai_provider_id
`
type UpdateChatModelConfigParams struct {
Model string `db:"model" json:"model"`
DisplayName string `db:"display_name" json:"display_name"`
UpdatedBy uuid.NullUUID `db:"updated_by" json:"updated_by"`
Enabled bool `db:"enabled" json:"enabled"`
IsDefault bool `db:"is_default" json:"is_default"`
ContextLimit int64 `db:"context_limit" json:"context_limit"`
CompressionThreshold int32 `db:"compression_threshold" json:"compression_threshold"`
Options json.RawMessage `db:"options" json:"options"`
AIProviderID uuid.NullUUID `db:"ai_provider_id" json:"ai_provider_id"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateChatModelConfig(ctx context.Context, arg UpdateChatModelConfigParams) (ChatModelConfig, error) {
row := q.db.QueryRowContext(ctx, updateChatModelConfig,
arg.Model,
arg.DisplayName,
arg.UpdatedBy,
arg.Enabled,
arg.IsDefault,
arg.ContextLimit,
arg.CompressionThreshold,
arg.Options,
arg.AIProviderID,
arg.ID,
)
var i ChatModelConfig
err := row.Scan(
&i.ID,
&i.Model,
&i.DisplayName,
&i.CreatedBy,
&i.UpdatedBy,
&i.Enabled,
&i.IsDefault,
&i.Deleted,
&i.DeletedAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ContextLimit,
&i.CompressionThreshold,
&i.Options,
&i.AIProviderID,
)
return i, err
}
const acquireStaleChatDiffStatuses = `-- name: AcquireStaleChatDiffStatuses :many
WITH acquired AS (
UPDATE
chat_diff_statuses
SET
-- Claim for 5 minutes. The worker sets the real stale_at
-- after refresh. If the worker crashes, rows become eligible
-- again after this interval.
-- NOTE: updated_at is intentionally NOT touched here so
-- the worker can read it as "when was this row last
-- externally changed" (by MarkStale or a successful
-- refresh).
stale_at = NOW() + INTERVAL '5 minutes'
WHERE
chat_id IN (
SELECT
cds.chat_id
FROM
chat_diff_statuses cds
INNER JOIN
chats c ON c.id = cds.chat_id
WHERE
cds.stale_at <= NOW()
AND cds.git_remote_origin != ''
AND cds.git_branch != ''
AND c.archived = FALSE
ORDER BY
cds.stale_at ASC
FOR UPDATE OF cds
SKIP LOCKED
LIMIT
$1::int
)
RETURNING chat_id, url, pull_request_state, changes_requested, additions, deletions, changed_files, refreshed_at, stale_at, created_at, updated_at, git_branch, git_remote_origin, pull_request_title, pull_request_draft, author_login, author_avatar_url, base_branch, pr_number, commits, approved, reviewer_count, head_branch
)
SELECT
acquired.chat_id, acquired.url, acquired.pull_request_state, acquired.changes_requested, acquired.additions, acquired.deletions, acquired.changed_files, acquired.refreshed_at, acquired.stale_at, acquired.created_at, acquired.updated_at, acquired.git_branch, acquired.git_remote_origin, acquired.pull_request_title, acquired.pull_request_draft, acquired.author_login, acquired.author_avatar_url, acquired.base_branch, acquired.pr_number, acquired.commits, acquired.approved, acquired.reviewer_count, acquired.head_branch,
c.owner_id
FROM
acquired
INNER JOIN
chats c ON c.id = acquired.chat_id
`
type AcquireStaleChatDiffStatusesRow struct {
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
Url sql.NullString `db:"url" json:"url"`
PullRequestState sql.NullString `db:"pull_request_state" json:"pull_request_state"`
ChangesRequested bool `db:"changes_requested" json:"changes_requested"`
Additions int32 `db:"additions" json:"additions"`
Deletions int32 `db:"deletions" json:"deletions"`
ChangedFiles int32 `db:"changed_files" json:"changed_files"`
RefreshedAt sql.NullTime `db:"refreshed_at" json:"refreshed_at"`
StaleAt time.Time `db:"stale_at" json:"stale_at"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
GitBranch string `db:"git_branch" json:"git_branch"`
GitRemoteOrigin string `db:"git_remote_origin" json:"git_remote_origin"`
PullRequestTitle string `db:"pull_request_title" json:"pull_request_title"`
PullRequestDraft bool `db:"pull_request_draft" json:"pull_request_draft"`
AuthorLogin sql.NullString `db:"author_login" json:"author_login"`
AuthorAvatarUrl sql.NullString `db:"author_avatar_url" json:"author_avatar_url"`
BaseBranch sql.NullString `db:"base_branch" json:"base_branch"`
PrNumber sql.NullInt32 `db:"pr_number" json:"pr_number"`
Commits sql.NullInt32 `db:"commits" json:"commits"`
Approved sql.NullBool `db:"approved" json:"approved"`
ReviewerCount sql.NullInt32 `db:"reviewer_count" json:"reviewer_count"`
HeadBranch sql.NullString `db:"head_branch" json:"head_branch"`
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
}
func (q *sqlQuerier) AcquireStaleChatDiffStatuses(ctx context.Context, limitVal int32) ([]AcquireStaleChatDiffStatusesRow, error) {
rows, err := q.db.QueryContext(ctx, acquireStaleChatDiffStatuses, limitVal)
if err != nil {
return nil, err
}
defer rows.Close()
var items []AcquireStaleChatDiffStatusesRow
for rows.Next() {
var i AcquireStaleChatDiffStatusesRow
if err := rows.Scan(
&i.ChatID,
&i.Url,
&i.PullRequestState,
&i.ChangesRequested,
&i.Additions,
&i.Deletions,
&i.ChangedFiles,
&i.RefreshedAt,
&i.StaleAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.GitBranch,
&i.GitRemoteOrigin,
&i.PullRequestTitle,
&i.PullRequestDraft,
&i.AuthorLogin,
&i.AuthorAvatarUrl,
&i.BaseBranch,
&i.PrNumber,
&i.Commits,
&i.Approved,
&i.ReviewerCount,
&i.HeadBranch,
&i.OwnerID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const archiveChatByID = `-- name: ArchiveChatByID :many
WITH updated_chats AS (
UPDATE chats
SET archived = true, pin_order = 0, updated_at = NOW()
WHERE id = $1::uuid OR root_chat_id = $1::uuid
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, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
),
chats_expanded AS (
SELECT
updated_chats.id,
updated_chats.owner_id,
updated_chats.workspace_id,
updated_chats.title,
updated_chats.status,
updated_chats.worker_id,
updated_chats.started_at,
updated_chats.heartbeat_at,
updated_chats.created_at,
updated_chats.updated_at,
updated_chats.parent_chat_id,
updated_chats.root_chat_id,
updated_chats.last_model_config_id,
updated_chats.last_reasoning_effort,
updated_chats.archived,
updated_chats.last_error,
updated_chats.mode,
updated_chats.mcp_server_ids,
updated_chats.labels,
updated_chats.build_id,
updated_chats.agent_id,
updated_chats.pin_order,
updated_chats.last_read_message_id,
updated_chats.dynamic_tools,
updated_chats.organization_id,
updated_chats.plan_mode,
updated_chats.client_type,
updated_chats.last_turn_summary,
updated_chats.summary,
updated_chats.summary_generated_at,
updated_chats.snapshot_version,
updated_chats.history_version,
updated_chats.queue_version,
updated_chats.generation_attempt,
updated_chats.retry_state,
updated_chats.retry_state_version,
updated_chats.runner_id,
updated_chats.requires_action_deadline_at,
COALESCE(root.user_acl, updated_chats.user_acl) AS user_acl,
COALESCE(root.group_acl, updated_chats.group_acl) AS group_acl,
owner.username AS owner_username,
owner.name AS owner_name,
updated_chats.context_aggregate_hash,
updated_chats.context_dirty_since,
updated_chats.context_dirty_resources,
updated_chats.context_error,
updated_chats.compaction_requested_at
FROM
updated_chats
LEFT JOIN chats root ON root.id = COALESCE(updated_chats.root_chat_id, updated_chats.parent_chat_id)
JOIN visible_users owner ON owner.id = updated_chats.owner_id
)
SELECT 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, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
ORDER BY (chats_expanded.id = $1::uuid) DESC, chats_expanded.created_at ASC, chats_expanded.id ASC
`
func (q *sqlQuerier) ArchiveChatByID(ctx context.Context, id uuid.UUID) ([]Chat, error) {
rows, err := q.db.QueryContext(ctx, archiveChatByID, id)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Chat
for rows.Next() {
var i Chat
if err := rows.Scan(
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.Title,
&i.Status,
&i.WorkerID,
&i.StartedAt,
&i.HeartbeatAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ParentChatID,
&i.RootChatID,
&i.LastModelConfigID,
&i.LastReasoningEffort,
&i.Archived,
&i.LastError,
&i.Mode,
pq.Array(&i.MCPServerIDs),
&i.Labels,
&i.BuildID,
&i.AgentID,
&i.PinOrder,
&i.LastReadMessageID,
&i.DynamicTools,
&i.OrganizationID,
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
&i.Summary,
&i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
&i.GenerationAttempt,
&i.RetryState,
&i.RetryStateVersion,
&i.RunnerID,
&i.RequiresActionDeadlineAt,
&i.UserACL,
&i.GroupACL,
&i.OwnerUsername,
&i.OwnerName,
&i.ContextAggregateHash,
&i.ContextDirtySince,
&i.ContextDirtyResources,
&i.ContextError,
&i.CompactionRequestedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const autoArchiveInactiveChats = `-- name: AutoArchiveInactiveChats :many
WITH to_archive AS (
SELECT
c.id,
-- Activity = MAX(cm.created_at) across the family, or c.created_at
-- when the family has no non-deleted messages.
COALESCE(activity.last_activity_at, c.created_at) AS last_activity_at
FROM chats c
LEFT JOIN LATERAL (
SELECT MAX(cm.created_at) AS last_activity_at
FROM chat_messages cm
JOIN chats fc ON fc.id = cm.chat_id
WHERE (fc.id = c.id OR fc.root_chat_id = c.id)
AND cm.deleted = false
) activity ON TRUE
WHERE c.archived = false
AND c.pin_order = 0
AND c.parent_chat_id IS NULL -- roots only
-- Redundant filter helps the planner use the partial index on created_at.
AND c.created_at < $1::timestamptz
-- New active statuses must be added here to prevent archiving.
AND c.status NOT IN ('running', 'requires_action')
AND COALESCE(activity.last_activity_at, c.created_at) < $1::timestamptz
-- Sorting by created_at lets Postgres drive the scan from the
-- partial index instead of evaluating every LATERAL subquery
-- before sorting. All candidates are past the cutoff, so the
-- archive order is immaterial once the backlog drains.
ORDER BY c.created_at ASC
LIMIT $2
),
archived AS (
UPDATE chats c
SET archived = true, pin_order = 0, updated_at = NOW()
FROM to_archive t
WHERE (c.id = t.id OR c.root_chat_id = t.id) -- cascade to children
AND c.archived = false
RETURNING c.id, c.owner_id, c.workspace_id, c.title, c.status, c.worker_id, c.started_at, c.heartbeat_at, c.created_at, c.updated_at, c.parent_chat_id, c.root_chat_id, c.last_model_config_id, c.archived, c.last_error, c.mode, c.mcp_server_ids, c.labels, c.build_id, c.agent_id, c.pin_order, c.last_read_message_id, c.dynamic_tools, c.organization_id, c.plan_mode, c.client_type, c.last_turn_summary, c.user_acl, c.group_acl, c.snapshot_version, c.history_version, c.queue_version, c.generation_attempt, c.retry_state, c.retry_state_version, c.runner_id, c.requires_action_deadline_at, c.context_aggregate_hash, c.context_dirty_since, c.context_dirty_resources, c.context_error, c.last_reasoning_effort, c.compaction_requested_at, c.summary, c.summary_generated_at
)
SELECT
a.id, a.owner_id, a.workspace_id, a.title, a.status, a.worker_id, a.started_at, a.heartbeat_at, a.created_at, a.updated_at, a.parent_chat_id, a.root_chat_id, a.last_model_config_id, a.archived, a.last_error, a.mode, a.mcp_server_ids, a.labels, a.build_id, a.agent_id, a.pin_order, a.last_read_message_id, a.dynamic_tools, a.organization_id, a.plan_mode, a.client_type, a.last_turn_summary, a.user_acl, a.group_acl, a.snapshot_version, a.history_version, a.queue_version, a.generation_attempt, a.retry_state, a.retry_state_version, a.runner_id, a.requires_action_deadline_at, a.context_aggregate_hash, a.context_dirty_since, a.context_dirty_resources, a.context_error, a.last_reasoning_effort, a.compaction_requested_at, a.summary, a.summary_generated_at,
-- Children inherit their root's activity so last_activity_at is never null.
COALESCE(
t.last_activity_at,
(SELECT tr.last_activity_at FROM to_archive tr WHERE tr.id = a.root_chat_id),
a.created_at
)::timestamptz AS last_activity_at
FROM archived a
LEFT JOIN to_archive t ON t.id = a.id
ORDER BY (a.root_chat_id IS NULL) DESC, a.owner_id ASC, a.created_at ASC, a.id ASC
`
type AutoArchiveInactiveChatsParams struct {
ArchiveCutoff time.Time `db:"archive_cutoff" json:"archive_cutoff"`
LimitCount int32 `db:"limit_count" json:"limit_count"`
}
type AutoArchiveInactiveChatsRow struct {
ID uuid.UUID `db:"id" json:"id"`
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"`
Title string `db:"title" json:"title"`
Status ChatStatus `db:"status" json:"status"`
WorkerID uuid.NullUUID `db:"worker_id" json:"worker_id"`
StartedAt sql.NullTime `db:"started_at" json:"started_at"`
HeartbeatAt sql.NullTime `db:"heartbeat_at" json:"heartbeat_at"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
ParentChatID uuid.NullUUID `db:"parent_chat_id" json:"parent_chat_id"`
RootChatID uuid.NullUUID `db:"root_chat_id" json:"root_chat_id"`
LastModelConfigID uuid.UUID `db:"last_model_config_id" json:"last_model_config_id"`
Archived bool `db:"archived" json:"archived"`
LastError pqtype.NullRawMessage `db:"last_error" json:"last_error"`
Mode NullChatMode `db:"mode" json:"mode"`
MCPServerIDs []uuid.UUID `db:"mcp_server_ids" json:"mcp_server_ids"`
Labels json.RawMessage `db:"labels" json:"labels"`
BuildID uuid.NullUUID `db:"build_id" json:"build_id"`
AgentID uuid.NullUUID `db:"agent_id" json:"agent_id"`
PinOrder int32 `db:"pin_order" json:"pin_order"`
LastReadMessageID sql.NullInt64 `db:"last_read_message_id" json:"last_read_message_id"`
DynamicTools pqtype.NullRawMessage `db:"dynamic_tools" json:"dynamic_tools"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
PlanMode NullChatPlanMode `db:"plan_mode" json:"plan_mode"`
ClientType ChatClientType `db:"client_type" json:"client_type"`
LastTurnSummary sql.NullString `db:"last_turn_summary" json:"last_turn_summary"`
UserACL json.RawMessage `db:"user_acl" json:"user_acl"`
GroupACL json.RawMessage `db:"group_acl" json:"group_acl"`
SnapshotVersion int64 `db:"snapshot_version" json:"snapshot_version"`
HistoryVersion int64 `db:"history_version" json:"history_version"`
QueueVersion int64 `db:"queue_version" json:"queue_version"`
GenerationAttempt int64 `db:"generation_attempt" json:"generation_attempt"`
RetryState pqtype.NullRawMessage `db:"retry_state" json:"retry_state"`
RetryStateVersion int64 `db:"retry_state_version" json:"retry_state_version"`
RunnerID uuid.NullUUID `db:"runner_id" json:"runner_id"`
RequiresActionDeadlineAt sql.NullTime `db:"requires_action_deadline_at" json:"requires_action_deadline_at"`
ContextAggregateHash []byte `db:"context_aggregate_hash" json:"context_aggregate_hash"`
ContextDirtySince sql.NullTime `db:"context_dirty_since" json:"context_dirty_since"`
ContextDirtyResources pqtype.NullRawMessage `db:"context_dirty_resources" json:"context_dirty_resources"`
ContextError string `db:"context_error" json:"context_error"`
LastReasoningEffort NullChatReasoningEffort `db:"last_reasoning_effort" json:"last_reasoning_effort"`
CompactionRequestedAt sql.NullTime `db:"compaction_requested_at" json:"compaction_requested_at"`
Summary sql.NullString `db:"summary" json:"summary"`
SummaryGeneratedAt sql.NullTime `db:"summary_generated_at" json:"summary_generated_at"`
LastActivityAt time.Time `db:"last_activity_at" json:"last_activity_at"`
}
// Archives inactive root chats (pinned and already-archived chats skipped),
// cascading to children via root_chat_id. Limits apply to roots, not total
// rows. The Go caller passes @archive_cutoff as UTC midnight so that all
// chats sharing the same last-activity date are archived together.
// Used by dbpurge.
// created_at ASC flows through to dbpurge's digest truncation; see
// buildDigestData in dbpurge.go for the tradeoff rationale.
func (q *sqlQuerier) AutoArchiveInactiveChats(ctx context.Context, arg AutoArchiveInactiveChatsParams) ([]AutoArchiveInactiveChatsRow, error) {
rows, err := q.db.QueryContext(ctx, autoArchiveInactiveChats, arg.ArchiveCutoff, arg.LimitCount)
if err != nil {
return nil, err
}
defer rows.Close()
var items []AutoArchiveInactiveChatsRow
for rows.Next() {
var i AutoArchiveInactiveChatsRow
if err := rows.Scan(
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.Title,
&i.Status,
&i.WorkerID,
&i.StartedAt,
&i.HeartbeatAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ParentChatID,
&i.RootChatID,
&i.LastModelConfigID,
&i.Archived,
&i.LastError,
&i.Mode,
pq.Array(&i.MCPServerIDs),
&i.Labels,
&i.BuildID,
&i.AgentID,
&i.PinOrder,
&i.LastReadMessageID,
&i.DynamicTools,
&i.OrganizationID,
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
&i.UserACL,
&i.GroupACL,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
&i.GenerationAttempt,
&i.RetryState,
&i.RetryStateVersion,
&i.RunnerID,
&i.RequiresActionDeadlineAt,
&i.ContextAggregateHash,
&i.ContextDirtySince,
&i.ContextDirtyResources,
&i.ContextError,
&i.LastReasoningEffort,
&i.CompactionRequestedAt,
&i.Summary,
&i.SummaryGeneratedAt,
&i.LastActivityAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const backfillChatMessagesSearchTsv = `-- 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 $1::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
`
// Backfills chat_messages.search_tsv for pending rows, newest first.
// The WHERE clause must match the predicate of
// idx_chat_messages_search_tsv_pending exactly so the partial index
// serves this query.
// NULL means "pending", empty tsvector means "backfilled, no text".
func (q *sqlQuerier) BackfillChatMessagesSearchTsv(ctx context.Context, batchSize int32) (int64, error) {
result, err := q.db.ExecContext(ctx, backfillChatMessagesSearchTsv, batchSize)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const backoffChatDiffStatus = `-- name: BackoffChatDiffStatus :exec
UPDATE
chat_diff_statuses
SET
-- NOTE: updated_at is intentionally NOT touched here so
-- the worker can read it as "when was this row last
-- externally changed" (by MarkStale or a successful
-- refresh).
stale_at = $1::timestamptz
WHERE
chat_id = $2::uuid
`
type BackoffChatDiffStatusParams struct {
StaleAt time.Time `db:"stale_at" json:"stale_at"`
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
}
func (q *sqlQuerier) BackoffChatDiffStatus(ctx context.Context, arg BackoffChatDiffStatusParams) error {
_, err := q.db.ExecContext(ctx, backoffChatDiffStatus, arg.StaleAt, arg.ChatID)
return err
}
const batchDeleteChatHeartbeats = `-- name: BatchDeleteChatHeartbeats :execrows
DELETE FROM chat_heartbeats
USING unnest($1::uuid[]) WITH ORDINALITY AS chat_ids(chat_id, ord)
JOIN unnest($2::uuid[]) WITH ORDINALITY AS runner_ids(runner_id, ord) USING (ord)
WHERE chat_heartbeats.chat_id = chat_ids.chat_id
AND chat_heartbeats.runner_id = runner_ids.runner_id
`
type BatchDeleteChatHeartbeatsParams struct {
ChatIds []uuid.UUID `db:"chat_ids" json:"chat_ids"`
RunnerIds []uuid.UUID `db:"runner_ids" json:"runner_ids"`
}
// Deletes heartbeat rows for the supplied (chat_id, runner_id) pairs.
func (q *sqlQuerier) BatchDeleteChatHeartbeats(ctx context.Context, arg BatchDeleteChatHeartbeatsParams) (int64, error) {
result, err := q.db.ExecContext(ctx, batchDeleteChatHeartbeats, pq.Array(arg.ChatIds), pq.Array(arg.RunnerIds))
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const batchUpsertChatHeartbeats = `-- name: BatchUpsertChatHeartbeats :exec
INSERT INTO chat_heartbeats (chat_id, runner_id, heartbeat_at)
SELECT chat_ids.chat_id, runner_ids.runner_id, NOW()
FROM unnest($1::uuid[]) WITH ORDINALITY AS chat_ids(chat_id, ord)
JOIN unnest($2::uuid[]) WITH ORDINALITY AS runner_ids(runner_id, ord) USING (ord)
ON CONFLICT (chat_id, runner_id) DO UPDATE
SET heartbeat_at = EXCLUDED.heartbeat_at
`
type BatchUpsertChatHeartbeatsParams struct {
ChatIds []uuid.UUID `db:"chat_ids" json:"chat_ids"`
RunnerIds []uuid.UUID `db:"runner_ids" json:"runner_ids"`
}
func (q *sqlQuerier) BatchUpsertChatHeartbeats(ctx context.Context, arg BatchUpsertChatHeartbeatsParams) error {
_, err := q.db.ExecContext(ctx, batchUpsertChatHeartbeats, pq.Array(arg.ChatIds), pq.Array(arg.RunnerIds))
return err
}
const countChatQueuedMessages = `-- name: CountChatQueuedMessages :one
SELECT COUNT(*)::bigint AS count
FROM chat_queued_messages
WHERE chat_id = $1::uuid
`
// Cheap queue-length check used by ChatMachine.Update when deciding
// whether the chat is in a "1" sub-state.
func (q *sqlQuerier) CountChatQueuedMessages(ctx context.Context, chatID uuid.UUID) (int64, error) {
row := q.db.QueryRowContext(ctx, countChatQueuedMessages, chatID)
var count int64
err := row.Scan(&count)
return count, err
}
const deleteAllChatHeartbeats = `-- name: DeleteAllChatHeartbeats :exec
DELETE FROM chat_heartbeats WHERE chat_id = $1::uuid
`
// Deletes all heartbeat rows for the chat. Used during ownership
// transitions that abandon a lease.
func (q *sqlQuerier) DeleteAllChatHeartbeats(ctx context.Context, chatID uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteAllChatHeartbeats, chatID)
return err
}
const deleteAllChatQueuedMessages = `-- name: DeleteAllChatQueuedMessages :exec
DELETE FROM chat_queued_messages WHERE chat_id = $1
`
func (q *sqlQuerier) DeleteAllChatQueuedMessages(ctx context.Context, chatID uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteAllChatQueuedMessages, chatID)
return err
}
const deleteAllChatQueuedMessagesReturningCount = `-- name: DeleteAllChatQueuedMessagesReturningCount :execrows
DELETE FROM chat_queued_messages
WHERE chat_id = $1::uuid
`
func (q *sqlQuerier) DeleteAllChatQueuedMessagesReturningCount(ctx context.Context, chatID uuid.UUID) (int64, error) {
result, err := q.db.ExecContext(ctx, deleteAllChatQueuedMessagesReturningCount, chatID)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const deleteChatContextResourcesByChatID = `-- name: DeleteChatContextResourcesByChatID :exec
DELETE FROM chat_context_resources
WHERE chat_id = $1::uuid
`
// Clears a chat's pinned context resources. Used as the first half of a
// clear-then-copy re-pin, and on its own when the chat's current agent
// has no snapshot.
func (q *sqlQuerier) DeleteChatContextResourcesByChatID(ctx context.Context, chatID uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteChatContextResourcesByChatID, chatID)
return err
}
const deleteChatQueuedMessage = `-- name: DeleteChatQueuedMessage :exec
DELETE FROM chat_queued_messages WHERE id = $1 AND chat_id = $2
`
type DeleteChatQueuedMessageParams struct {
ID int64 `db:"id" json:"id"`
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
}
func (q *sqlQuerier) DeleteChatQueuedMessage(ctx context.Context, arg DeleteChatQueuedMessageParams) error {
_, err := q.db.ExecContext(ctx, deleteChatQueuedMessage, arg.ID, arg.ChatID)
return err
}
const deleteChatQueuedMessageReturningCount = `-- name: DeleteChatQueuedMessageReturningCount :execrows
DELETE FROM chat_queued_messages
WHERE id = $1::bigint AND chat_id = $2::uuid
`
type DeleteChatQueuedMessageReturningCountParams struct {
ID int64 `db:"id" json:"id"`
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
}
// Deletes a queued message, scoped to the parent chat. Returns the
// number of affected rows so callers can detect missing rows without
// a follow-up read.
func (q *sqlQuerier) DeleteChatQueuedMessageReturningCount(ctx context.Context, arg DeleteChatQueuedMessageReturningCountParams) (int64, error) {
result, err := q.db.ExecContext(ctx, deleteChatQueuedMessageReturningCount, arg.ID, arg.ChatID)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const deleteOldChats = `-- name: DeleteOldChats :execrows
WITH deletable AS (
SELECT id
FROM chats
WHERE archived = true
AND updated_at < $1::timestamptz
ORDER BY updated_at ASC
LIMIT $2
)
DELETE FROM chats
USING deletable
WHERE chats.id = deletable.id
AND chats.archived = true
`
type DeleteOldChatsParams struct {
BeforeTime time.Time `db:"before_time" json:"before_time"`
LimitCount int32 `db:"limit_count" json:"limit_count"`
}
// Deletes chats that have been archived for longer than the given
// threshold. Active (non-archived) chats are never deleted.
// All chat-scoped child tables are removed via ON DELETE CASCADE.
// Parent/root references on child chats are SET NULL.
func (q *sqlQuerier) DeleteOldChats(ctx context.Context, arg DeleteOldChatsParams) (int64, error) {
result, err := q.db.ExecContext(ctx, deleteOldChats, arg.BeforeTime, arg.LimitCount)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const deleteStaleChatHeartbeats = `-- name: DeleteStaleChatHeartbeats :execrows
DELETE FROM chat_heartbeats
WHERE heartbeat_at < NOW() - (INTERVAL '1 second' * $1::int)
`
func (q *sqlQuerier) DeleteStaleChatHeartbeats(ctx context.Context, staleSeconds int32) (int64, error) {
result, err := q.db.ExecContext(ctx, deleteStaleChatHeartbeats, staleSeconds)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const getActiveChatsByAgentID = `-- name: GetActiveChatsByAgentID :many
SELECT 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, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
WHERE agent_id = $1::uuid
AND archived = false
-- Active statuses only: waiting, running, requires_action.
-- Excludes error (terminal state) and interrupting.
AND status IN ('waiting', 'running', 'requires_action')
ORDER BY updated_at DESC
`
func (q *sqlQuerier) GetActiveChatsByAgentID(ctx context.Context, agentID uuid.UUID) ([]Chat, error) {
rows, err := q.db.QueryContext(ctx, getActiveChatsByAgentID, agentID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Chat
for rows.Next() {
var i Chat
if err := rows.Scan(
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.Title,
&i.Status,
&i.WorkerID,
&i.StartedAt,
&i.HeartbeatAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ParentChatID,
&i.RootChatID,
&i.LastModelConfigID,
&i.LastReasoningEffort,
&i.Archived,
&i.LastError,
&i.Mode,
pq.Array(&i.MCPServerIDs),
&i.Labels,
&i.BuildID,
&i.AgentID,
&i.PinOrder,
&i.LastReadMessageID,
&i.DynamicTools,
&i.OrganizationID,
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
&i.Summary,
&i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
&i.GenerationAttempt,
&i.RetryState,
&i.RetryStateVersion,
&i.RunnerID,
&i.RequiresActionDeadlineAt,
&i.UserACL,
&i.GroupACL,
&i.OwnerUsername,
&i.OwnerName,
&i.ContextAggregateHash,
&i.ContextDirtySince,
&i.ContextDirtyResources,
&i.ContextError,
&i.CompactionRequestedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getAutoArchiveInactiveChatCandidates = `-- name: GetAutoArchiveInactiveChatCandidates :many
SELECT
chats_expanded.id, chats_expanded.owner_id, chats_expanded.workspace_id, chats_expanded.title, chats_expanded.status, chats_expanded.worker_id, chats_expanded.started_at, chats_expanded.heartbeat_at, chats_expanded.created_at, chats_expanded.updated_at, chats_expanded.parent_chat_id, chats_expanded.root_chat_id, chats_expanded.last_model_config_id, chats_expanded.last_reasoning_effort, chats_expanded.archived, chats_expanded.last_error, chats_expanded.mode, chats_expanded.mcp_server_ids, chats_expanded.labels, chats_expanded.build_id, chats_expanded.agent_id, chats_expanded.pin_order, chats_expanded.last_read_message_id, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.summary, chats_expanded.summary_generated_at, chats_expanded.snapshot_version, chats_expanded.history_version, chats_expanded.queue_version, chats_expanded.generation_attempt, chats_expanded.retry_state, chats_expanded.retry_state_version, chats_expanded.runner_id, chats_expanded.requires_action_deadline_at, chats_expanded.user_acl, chats_expanded.group_acl, chats_expanded.owner_username, chats_expanded.owner_name, chats_expanded.context_aggregate_hash, chats_expanded.context_dirty_since, chats_expanded.context_dirty_resources, chats_expanded.context_error, chats_expanded.compaction_requested_at,
COALESCE(activity.last_activity_at, chats_expanded.created_at)::timestamptz AS last_activity_at
FROM chats_expanded
LEFT JOIN LATERAL (
SELECT MAX(chat_messages.created_at) AS last_activity_at
FROM chat_messages
JOIN chats family_chat ON family_chat.id = chat_messages.chat_id
WHERE (family_chat.id = chats_expanded.id OR family_chat.root_chat_id = chats_expanded.id)
AND chat_messages.deleted = false
) activity ON TRUE
WHERE
chats_expanded.archived = false
AND chats_expanded.pin_order = 0
AND chats_expanded.parent_chat_id IS NULL
AND chats_expanded.created_at < $1::timestamptz
AND chats_expanded.status NOT IN (
'running'::chat_status,
'interrupting'::chat_status,
'requires_action'::chat_status
)
AND COALESCE(activity.last_activity_at, chats_expanded.created_at) < $1::timestamptz
ORDER BY chats_expanded.created_at ASC
LIMIT $2::int
`
type GetAutoArchiveInactiveChatCandidatesParams struct {
ArchiveCutoff time.Time `db:"archive_cutoff" json:"archive_cutoff"`
LimitCount int32 `db:"limit_count" json:"limit_count"`
}
type GetAutoArchiveInactiveChatCandidatesRow struct {
ID uuid.UUID `db:"id" json:"id"`
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"`
Title string `db:"title" json:"title"`
Status ChatStatus `db:"status" json:"status"`
WorkerID uuid.NullUUID `db:"worker_id" json:"worker_id"`
StartedAt sql.NullTime `db:"started_at" json:"started_at"`
HeartbeatAt sql.NullTime `db:"heartbeat_at" json:"heartbeat_at"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
ParentChatID uuid.NullUUID `db:"parent_chat_id" json:"parent_chat_id"`
RootChatID uuid.NullUUID `db:"root_chat_id" json:"root_chat_id"`
LastModelConfigID uuid.UUID `db:"last_model_config_id" json:"last_model_config_id"`
LastReasoningEffort NullChatReasoningEffort `db:"last_reasoning_effort" json:"last_reasoning_effort"`
Archived bool `db:"archived" json:"archived"`
LastError pqtype.NullRawMessage `db:"last_error" json:"last_error"`
Mode NullChatMode `db:"mode" json:"mode"`
MCPServerIDs []uuid.UUID `db:"mcp_server_ids" json:"mcp_server_ids"`
Labels StringMap `db:"labels" json:"labels"`
BuildID uuid.NullUUID `db:"build_id" json:"build_id"`
AgentID uuid.NullUUID `db:"agent_id" json:"agent_id"`
PinOrder int32 `db:"pin_order" json:"pin_order"`
LastReadMessageID sql.NullInt64 `db:"last_read_message_id" json:"last_read_message_id"`
DynamicTools pqtype.NullRawMessage `db:"dynamic_tools" json:"dynamic_tools"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
PlanMode NullChatPlanMode `db:"plan_mode" json:"plan_mode"`
ClientType ChatClientType `db:"client_type" json:"client_type"`
LastTurnSummary sql.NullString `db:"last_turn_summary" json:"last_turn_summary"`
Summary sql.NullString `db:"summary" json:"summary"`
SummaryGeneratedAt sql.NullTime `db:"summary_generated_at" json:"summary_generated_at"`
SnapshotVersion int64 `db:"snapshot_version" json:"snapshot_version"`
HistoryVersion int64 `db:"history_version" json:"history_version"`
QueueVersion int64 `db:"queue_version" json:"queue_version"`
GenerationAttempt int64 `db:"generation_attempt" json:"generation_attempt"`
RetryState pqtype.NullRawMessage `db:"retry_state" json:"retry_state"`
RetryStateVersion int64 `db:"retry_state_version" json:"retry_state_version"`
RunnerID uuid.NullUUID `db:"runner_id" json:"runner_id"`
RequiresActionDeadlineAt sql.NullTime `db:"requires_action_deadline_at" json:"requires_action_deadline_at"`
UserACL ChatACL `db:"user_acl" json:"user_acl"`
GroupACL ChatACL `db:"group_acl" json:"group_acl"`
OwnerUsername string `db:"owner_username" json:"owner_username"`
OwnerName string `db:"owner_name" json:"owner_name"`
ContextAggregateHash []byte `db:"context_aggregate_hash" json:"context_aggregate_hash"`
ContextDirtySince sql.NullTime `db:"context_dirty_since" json:"context_dirty_since"`
ContextDirtyResources pqtype.NullRawMessage `db:"context_dirty_resources" json:"context_dirty_resources"`
ContextError string `db:"context_error" json:"context_error"`
CompactionRequestedAt sql.NullTime `db:"compaction_requested_at" json:"compaction_requested_at"`
LastActivityAt time.Time `db:"last_activity_at" json:"last_activity_at"`
}
// Returns read-only root chat candidates for state-machine-backed
// auto-archive. Activity is computed across the root family. The query
// limits roots, not total family members.
func (q *sqlQuerier) GetAutoArchiveInactiveChatCandidates(ctx context.Context, arg GetAutoArchiveInactiveChatCandidatesParams) ([]GetAutoArchiveInactiveChatCandidatesRow, error) {
rows, err := q.db.QueryContext(ctx, getAutoArchiveInactiveChatCandidates, arg.ArchiveCutoff, arg.LimitCount)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetAutoArchiveInactiveChatCandidatesRow
for rows.Next() {
var i GetAutoArchiveInactiveChatCandidatesRow
if err := rows.Scan(
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.Title,
&i.Status,
&i.WorkerID,
&i.StartedAt,
&i.HeartbeatAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ParentChatID,
&i.RootChatID,
&i.LastModelConfigID,
&i.LastReasoningEffort,
&i.Archived,
&i.LastError,
&i.Mode,
pq.Array(&i.MCPServerIDs),
&i.Labels,
&i.BuildID,
&i.AgentID,
&i.PinOrder,
&i.LastReadMessageID,
&i.DynamicTools,
&i.OrganizationID,
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
&i.Summary,
&i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
&i.GenerationAttempt,
&i.RetryState,
&i.RetryStateVersion,
&i.RunnerID,
&i.RequiresActionDeadlineAt,
&i.UserACL,
&i.GroupACL,
&i.OwnerUsername,
&i.OwnerName,
&i.ContextAggregateHash,
&i.ContextDirtySince,
&i.ContextDirtyResources,
&i.ContextError,
&i.CompactionRequestedAt,
&i.LastActivityAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getChatACLByID = `-- name: GetChatACLByID :one
SELECT
user_acl AS users,
group_acl AS groups
FROM
chats
WHERE
id = $1::uuid
`
type GetChatACLByIDRow struct {
Users ChatACL `db:"users" json:"users"`
Groups ChatACL `db:"groups" json:"groups"`
}
func (q *sqlQuerier) GetChatACLByID(ctx context.Context, id uuid.UUID) (GetChatACLByIDRow, error) {
row := q.db.QueryRowContext(ctx, getChatACLByID, id)
var i GetChatACLByIDRow
err := row.Scan(&i.Users, &i.Groups)
return i, err
}
const getChatByID = `-- name: GetChatByID :one
SELECT 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, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
WHERE id = $1::uuid
`
func (q *sqlQuerier) GetChatByID(ctx context.Context, id uuid.UUID) (Chat, error) {
row := q.db.QueryRowContext(ctx, getChatByID, id)
var i Chat
err := row.Scan(
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.Title,
&i.Status,
&i.WorkerID,
&i.StartedAt,
&i.HeartbeatAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ParentChatID,
&i.RootChatID,
&i.LastModelConfigID,
&i.LastReasoningEffort,
&i.Archived,
&i.LastError,
&i.Mode,
pq.Array(&i.MCPServerIDs),
&i.Labels,
&i.BuildID,
&i.AgentID,
&i.PinOrder,
&i.LastReadMessageID,
&i.DynamicTools,
&i.OrganizationID,
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
&i.Summary,
&i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
&i.GenerationAttempt,
&i.RetryState,
&i.RetryStateVersion,
&i.RunnerID,
&i.RequiresActionDeadlineAt,
&i.UserACL,
&i.GroupACL,
&i.OwnerUsername,
&i.OwnerName,
&i.ContextAggregateHash,
&i.ContextDirtySince,
&i.ContextDirtyResources,
&i.ContextError,
&i.CompactionRequestedAt,
)
return i, err
}
const getChatByIDForShare = `-- name: GetChatByIDForShare :one
WITH shared_chat AS (
SELECT 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, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
FROM chats
WHERE id = $1::uuid
FOR SHARE
),
chats_expanded AS (
SELECT
shared_chat.id,
shared_chat.owner_id,
shared_chat.workspace_id,
shared_chat.title,
shared_chat.status,
shared_chat.worker_id,
shared_chat.started_at,
shared_chat.heartbeat_at,
shared_chat.created_at,
shared_chat.updated_at,
shared_chat.parent_chat_id,
shared_chat.root_chat_id,
shared_chat.last_model_config_id,
shared_chat.last_reasoning_effort,
shared_chat.archived,
shared_chat.last_error,
shared_chat.mode,
shared_chat.mcp_server_ids,
shared_chat.labels,
shared_chat.build_id,
shared_chat.agent_id,
shared_chat.pin_order,
shared_chat.last_read_message_id,
shared_chat.dynamic_tools,
shared_chat.organization_id,
shared_chat.plan_mode,
shared_chat.client_type,
shared_chat.last_turn_summary,
shared_chat.summary,
shared_chat.summary_generated_at,
shared_chat.snapshot_version,
shared_chat.history_version,
shared_chat.queue_version,
shared_chat.generation_attempt,
shared_chat.retry_state,
shared_chat.retry_state_version,
shared_chat.runner_id,
shared_chat.requires_action_deadline_at,
COALESCE(root.user_acl, shared_chat.user_acl) AS user_acl,
COALESCE(root.group_acl, shared_chat.group_acl) AS group_acl,
owner.username AS owner_username,
owner.name AS owner_name,
shared_chat.context_aggregate_hash,
shared_chat.context_dirty_since,
shared_chat.context_dirty_resources,
shared_chat.context_error,
shared_chat.compaction_requested_at
FROM
shared_chat
LEFT JOIN chats root ON root.id = COALESCE(shared_chat.root_chat_id, shared_chat.parent_chat_id)
JOIN visible_users owner ON owner.id = shared_chat.owner_id
)
SELECT 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, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
`
func (q *sqlQuerier) GetChatByIDForShare(ctx context.Context, id uuid.UUID) (Chat, error) {
row := q.db.QueryRowContext(ctx, getChatByIDForShare, id)
var i Chat
err := row.Scan(
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.Title,
&i.Status,
&i.WorkerID,
&i.StartedAt,
&i.HeartbeatAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ParentChatID,
&i.RootChatID,
&i.LastModelConfigID,
&i.LastReasoningEffort,
&i.Archived,
&i.LastError,
&i.Mode,
pq.Array(&i.MCPServerIDs),
&i.Labels,
&i.BuildID,
&i.AgentID,
&i.PinOrder,
&i.LastReadMessageID,
&i.DynamicTools,
&i.OrganizationID,
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
&i.Summary,
&i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
&i.GenerationAttempt,
&i.RetryState,
&i.RetryStateVersion,
&i.RunnerID,
&i.RequiresActionDeadlineAt,
&i.UserACL,
&i.GroupACL,
&i.OwnerUsername,
&i.OwnerName,
&i.ContextAggregateHash,
&i.ContextDirtySince,
&i.ContextDirtyResources,
&i.ContextError,
&i.CompactionRequestedAt,
)
return i, err
}
const getChatByIDForUpdate = `-- name: GetChatByIDForUpdate :one
WITH locked_chat AS (
SELECT 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, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
FROM chats
WHERE id = $1::uuid
FOR UPDATE
),
chats_expanded AS (
SELECT
locked_chat.id,
locked_chat.owner_id,
locked_chat.workspace_id,
locked_chat.title,
locked_chat.status,
locked_chat.worker_id,
locked_chat.started_at,
locked_chat.heartbeat_at,
locked_chat.created_at,
locked_chat.updated_at,
locked_chat.parent_chat_id,
locked_chat.root_chat_id,
locked_chat.last_model_config_id,
locked_chat.last_reasoning_effort,
locked_chat.archived,
locked_chat.last_error,
locked_chat.mode,
locked_chat.mcp_server_ids,
locked_chat.labels,
locked_chat.build_id,
locked_chat.agent_id,
locked_chat.pin_order,
locked_chat.last_read_message_id,
locked_chat.dynamic_tools,
locked_chat.organization_id,
locked_chat.plan_mode,
locked_chat.client_type,
locked_chat.last_turn_summary,
locked_chat.summary,
locked_chat.summary_generated_at,
locked_chat.snapshot_version,
locked_chat.history_version,
locked_chat.queue_version,
locked_chat.generation_attempt,
locked_chat.retry_state,
locked_chat.retry_state_version,
locked_chat.runner_id,
locked_chat.requires_action_deadline_at,
COALESCE(root.user_acl, locked_chat.user_acl) AS user_acl,
COALESCE(root.group_acl, locked_chat.group_acl) AS group_acl,
owner.username AS owner_username,
owner.name AS owner_name,
locked_chat.context_aggregate_hash,
locked_chat.context_dirty_since,
locked_chat.context_dirty_resources,
locked_chat.context_error,
locked_chat.compaction_requested_at
FROM
locked_chat
LEFT JOIN chats root ON root.id = COALESCE(locked_chat.root_chat_id, locked_chat.parent_chat_id)
JOIN visible_users owner ON owner.id = locked_chat.owner_id
)
SELECT 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, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
`
func (q *sqlQuerier) GetChatByIDForUpdate(ctx context.Context, id uuid.UUID) (Chat, error) {
row := q.db.QueryRowContext(ctx, getChatByIDForUpdate, id)
var i Chat
err := row.Scan(
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.Title,
&i.Status,
&i.WorkerID,
&i.StartedAt,
&i.HeartbeatAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ParentChatID,
&i.RootChatID,
&i.LastModelConfigID,
&i.LastReasoningEffort,
&i.Archived,
&i.LastError,
&i.Mode,
pq.Array(&i.MCPServerIDs),
&i.Labels,
&i.BuildID,
&i.AgentID,
&i.PinOrder,
&i.LastReadMessageID,
&i.DynamicTools,
&i.OrganizationID,
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
&i.Summary,
&i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
&i.GenerationAttempt,
&i.RetryState,
&i.RetryStateVersion,
&i.RunnerID,
&i.RequiresActionDeadlineAt,
&i.UserACL,
&i.GroupACL,
&i.OwnerUsername,
&i.OwnerName,
&i.ContextAggregateHash,
&i.ContextDirtySince,
&i.ContextDirtyResources,
&i.ContextError,
&i.CompactionRequestedAt,
)
return i, err
}
const getChatDiffStatusByChatID = `-- name: GetChatDiffStatusByChatID :one
SELECT
chat_id, url, pull_request_state, changes_requested, additions, deletions, changed_files, refreshed_at, stale_at, created_at, updated_at, git_branch, git_remote_origin, pull_request_title, pull_request_draft, author_login, author_avatar_url, base_branch, pr_number, commits, approved, reviewer_count, head_branch
FROM
chat_diff_statuses
WHERE
chat_id = $1::uuid
`
func (q *sqlQuerier) GetChatDiffStatusByChatID(ctx context.Context, chatID uuid.UUID) (ChatDiffStatus, error) {
row := q.db.QueryRowContext(ctx, getChatDiffStatusByChatID, chatID)
var i ChatDiffStatus
err := row.Scan(
&i.ChatID,
&i.Url,
&i.PullRequestState,
&i.ChangesRequested,
&i.Additions,
&i.Deletions,
&i.ChangedFiles,
&i.RefreshedAt,
&i.StaleAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.GitBranch,
&i.GitRemoteOrigin,
&i.PullRequestTitle,
&i.PullRequestDraft,
&i.AuthorLogin,
&i.AuthorAvatarUrl,
&i.BaseBranch,
&i.PrNumber,
&i.Commits,
&i.Approved,
&i.ReviewerCount,
&i.HeadBranch,
)
return i, err
}
const getChatDiffStatusSummary = `-- name: GetChatDiffStatusSummary :one
WITH deduped AS (
SELECT DISTINCT ON (COALESCE(NULLIF(cds.url, ''), c.id::text))
cds.pull_request_state
FROM chat_diff_statuses cds
JOIN chats c ON c.id = cds.chat_id
WHERE cds.pull_request_state IN ('open', 'merged', 'closed')
ORDER BY COALESCE(NULLIF(cds.url, ''), c.id::text), cds.updated_at DESC, c.id DESC
)
SELECT
COUNT(*)::bigint AS total,
COUNT(*) FILTER (WHERE pull_request_state = 'open')::bigint AS open,
COUNT(*) FILTER (WHERE pull_request_state = 'merged')::bigint AS merged,
COUNT(*) FILTER (WHERE pull_request_state = 'closed')::bigint AS closed
FROM deduped
`
type GetChatDiffStatusSummaryRow struct {
Total int64 `db:"total" json:"total"`
Open int64 `db:"open" json:"open"`
Merged int64 `db:"merged" json:"merged"`
Closed int64 `db:"closed" json:"closed"`
}
// Returns aggregate PR counts across all agent chats for telemetry.
// Deduplicates by PR URL so forked chats referencing the same pull
// request are counted once (using the most recently refreshed state).
// Total is derived from the three recognized state buckets and
// always equals open + merged + closed; other non-NULL states are
// intentionally excluded from these aggregates.
func (q *sqlQuerier) GetChatDiffStatusSummary(ctx context.Context) (GetChatDiffStatusSummaryRow, error) {
row := q.db.QueryRowContext(ctx, getChatDiffStatusSummary)
var i GetChatDiffStatusSummaryRow
err := row.Scan(
&i.Total,
&i.Open,
&i.Merged,
&i.Closed,
)
return i, err
}
const getChatDiffStatusesByChatIDs = `-- name: GetChatDiffStatusesByChatIDs :many
SELECT
chat_id, url, pull_request_state, changes_requested, additions, deletions, changed_files, refreshed_at, stale_at, created_at, updated_at, git_branch, git_remote_origin, pull_request_title, pull_request_draft, author_login, author_avatar_url, base_branch, pr_number, commits, approved, reviewer_count, head_branch
FROM
chat_diff_statuses
WHERE
chat_id = ANY($1::uuid[])
`
func (q *sqlQuerier) GetChatDiffStatusesByChatIDs(ctx context.Context, chatIds []uuid.UUID) ([]ChatDiffStatus, error) {
rows, err := q.db.QueryContext(ctx, getChatDiffStatusesByChatIDs, pq.Array(chatIds))
if err != nil {
return nil, err
}
defer rows.Close()
var items []ChatDiffStatus
for rows.Next() {
var i ChatDiffStatus
if err := rows.Scan(
&i.ChatID,
&i.Url,
&i.PullRequestState,
&i.ChangesRequested,
&i.Additions,
&i.Deletions,
&i.ChangedFiles,
&i.RefreshedAt,
&i.StaleAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.GitBranch,
&i.GitRemoteOrigin,
&i.PullRequestTitle,
&i.PullRequestDraft,
&i.AuthorLogin,
&i.AuthorAvatarUrl,
&i.BaseBranch,
&i.PrNumber,
&i.Commits,
&i.Approved,
&i.ReviewerCount,
&i.HeadBranch,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getChatFamilyIDsByRootID = `-- name: GetChatFamilyIDsByRootID :many
SELECT id
FROM chats
WHERE id = $1::uuid OR root_chat_id = $1::uuid
ORDER BY (id = $1::uuid) DESC, created_at ASC, id ASC
`
// Returns the chat IDs of every chat in a family (root + all children)
// in deterministic order. The id parameter must be the root id; the
// query does not walk up from a child.
func (q *sqlQuerier) GetChatFamilyIDsByRootID(ctx context.Context, id uuid.UUID) ([]uuid.UUID, error) {
rows, err := q.db.QueryContext(ctx, getChatFamilyIDsByRootID, id)
if err != nil {
return nil, err
}
defer rows.Close()
var items []uuid.UUID
for rows.Next() {
var id uuid.UUID
if err := rows.Scan(&id); err != nil {
return nil, err
}
items = append(items, id)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getChatHeartbeat = `-- name: GetChatHeartbeat :one
SELECT chat_id, runner_id, heartbeat_at FROM chat_heartbeats
WHERE chat_id = $1::uuid AND runner_id = $2::uuid
`
type GetChatHeartbeatParams struct {
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
RunnerID uuid.UUID `db:"runner_id" json:"runner_id"`
}
func (q *sqlQuerier) GetChatHeartbeat(ctx context.Context, arg GetChatHeartbeatParams) (ChatHeartbeat, error) {
row := q.db.QueryRowContext(ctx, getChatHeartbeat, arg.ChatID, arg.RunnerID)
var i ChatHeartbeat
err := row.Scan(&i.ChatID, &i.RunnerID, &i.HeartbeatAt)
return i, err
}
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, deleted, provider_response_id, revision, reasoning_effort, search_tsv
FROM
chat_messages
WHERE
id = $1::bigint
AND deleted = false
`
func (q *sqlQuerier) GetChatMessageByID(ctx context.Context, id int64) (ChatMessage, error) {
row := q.db.QueryRowContext(ctx, getChatMessageByID, id)
var i ChatMessage
err := row.Scan(
&i.ID,
&i.ChatID,
&i.ModelConfigID,
&i.CreatedAt,
&i.Role,
&i.Content,
&i.Visibility,
&i.InputTokens,
&i.OutputTokens,
&i.TotalTokens,
&i.ReasoningTokens,
&i.CacheCreationTokens,
&i.CacheReadTokens,
&i.ContextLimit,
&i.Compressed,
&i.CreatedBy,
&i.ContentVersion,
&i.TotalCostMicros,
&i.RuntimeMs,
&i.Deleted,
&i.ProviderResponseID,
&i.Revision,
&i.ReasoningEffort,
&i.SearchTsv,
)
return i, err
}
const getChatMessageSummariesPerChat = `-- name: GetChatMessageSummariesPerChat :many
SELECT
cm.chat_id,
COUNT(*)::bigint AS message_count,
COUNT(*) FILTER (WHERE cm.role = 'user')::bigint AS user_message_count,
COUNT(*) FILTER (WHERE cm.role = 'assistant')::bigint AS assistant_message_count,
COUNT(*) FILTER (WHERE cm.role = 'tool')::bigint AS tool_message_count,
COUNT(*) FILTER (WHERE cm.role = 'system')::bigint AS system_message_count,
COALESCE(SUM(cm.input_tokens), 0)::bigint AS total_input_tokens,
COALESCE(SUM(cm.output_tokens), 0)::bigint AS total_output_tokens,
COALESCE(SUM(cm.reasoning_tokens), 0)::bigint AS total_reasoning_tokens,
COALESCE(SUM(cm.cache_creation_tokens), 0)::bigint AS total_cache_creation_tokens,
COALESCE(SUM(cm.cache_read_tokens), 0)::bigint AS total_cache_read_tokens,
COALESCE(SUM(cm.runtime_ms), 0)::bigint AS total_runtime_ms,
COUNT(DISTINCT cm.model_config_id)::bigint AS distinct_model_count,
COUNT(*) FILTER (WHERE cm.compressed)::bigint AS compressed_message_count
FROM chat_messages cm
WHERE cm.created_at > $1
AND cm.deleted = false
GROUP BY cm.chat_id
`
type GetChatMessageSummariesPerChatRow struct {
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
MessageCount int64 `db:"message_count" json:"message_count"`
UserMessageCount int64 `db:"user_message_count" json:"user_message_count"`
AssistantMessageCount int64 `db:"assistant_message_count" json:"assistant_message_count"`
ToolMessageCount int64 `db:"tool_message_count" json:"tool_message_count"`
SystemMessageCount int64 `db:"system_message_count" json:"system_message_count"`
TotalInputTokens int64 `db:"total_input_tokens" json:"total_input_tokens"`
TotalOutputTokens int64 `db:"total_output_tokens" json:"total_output_tokens"`
TotalReasoningTokens int64 `db:"total_reasoning_tokens" json:"total_reasoning_tokens"`
TotalCacheCreationTokens int64 `db:"total_cache_creation_tokens" json:"total_cache_creation_tokens"`
TotalCacheReadTokens int64 `db:"total_cache_read_tokens" json:"total_cache_read_tokens"`
TotalRuntimeMs int64 `db:"total_runtime_ms" json:"total_runtime_ms"`
DistinctModelCount int64 `db:"distinct_model_count" json:"distinct_model_count"`
CompressedMessageCount int64 `db:"compressed_message_count" json:"compressed_message_count"`
}
// Aggregates message-level metrics per chat for messages created
// after the given timestamp. Uses message created_at so that
// ongoing activity in long-running chats is captured each window.
func (q *sqlQuerier) GetChatMessageSummariesPerChat(ctx context.Context, createdAfter time.Time) ([]GetChatMessageSummariesPerChatRow, error) {
rows, err := q.db.QueryContext(ctx, getChatMessageSummariesPerChat, createdAfter)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetChatMessageSummariesPerChatRow
for rows.Next() {
var i GetChatMessageSummariesPerChatRow
if err := rows.Scan(
&i.ChatID,
&i.MessageCount,
&i.UserMessageCount,
&i.AssistantMessageCount,
&i.ToolMessageCount,
&i.SystemMessageCount,
&i.TotalInputTokens,
&i.TotalOutputTokens,
&i.TotalReasoningTokens,
&i.TotalCacheCreationTokens,
&i.TotalCacheReadTokens,
&i.TotalRuntimeMs,
&i.DistinctModelCount,
&i.CompressedMessageCount,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
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, deleted, provider_response_id, revision, reasoning_effort, search_tsv
FROM
chat_messages
WHERE
chat_id = $1::uuid
AND id > $2::bigint
AND visibility IN ('user', 'both')
AND deleted = false
ORDER BY
id ASC
`
type GetChatMessagesByChatIDParams struct {
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
AfterID int64 `db:"after_id" json:"after_id"`
}
// Ordered by id to match the @after_id cursor. created_at is the transaction
// start time, so it can disagree with append order when a transaction takes the
// chat row lock later than one that started after it.
func (q *sqlQuerier) GetChatMessagesByChatID(ctx context.Context, arg GetChatMessagesByChatIDParams) ([]ChatMessage, error) {
rows, err := q.db.QueryContext(ctx, getChatMessagesByChatID, arg.ChatID, arg.AfterID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ChatMessage
for rows.Next() {
var i ChatMessage
if err := rows.Scan(
&i.ID,
&i.ChatID,
&i.ModelConfigID,
&i.CreatedAt,
&i.Role,
&i.Content,
&i.Visibility,
&i.InputTokens,
&i.OutputTokens,
&i.TotalTokens,
&i.ReasoningTokens,
&i.CacheCreationTokens,
&i.CacheReadTokens,
&i.ContextLimit,
&i.Compressed,
&i.CreatedBy,
&i.ContentVersion,
&i.TotalCostMicros,
&i.RuntimeMs,
&i.Deleted,
&i.ProviderResponseID,
&i.Revision,
&i.ReasoningEffort,
&i.SearchTsv,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getChatMessagesByChatIDAscPaginated = `-- name: GetChatMessagesByChatIDAscPaginated :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, deleted, provider_response_id, revision, reasoning_effort, search_tsv
FROM
chat_messages
WHERE
chat_id = $1::uuid
AND id > $2::bigint
AND visibility IN ('user', 'both')
AND deleted = false
ORDER BY
id ASC
LIMIT
COALESCE(NULLIF($3::int, 0), 50)
`
type GetChatMessagesByChatIDAscPaginatedParams struct {
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
AfterID int64 `db:"after_id" json:"after_id"`
LimitVal int32 `db:"limit_val" json:"limit_val"`
}
func (q *sqlQuerier) GetChatMessagesByChatIDAscPaginated(ctx context.Context, arg GetChatMessagesByChatIDAscPaginatedParams) ([]ChatMessage, error) {
rows, err := q.db.QueryContext(ctx, getChatMessagesByChatIDAscPaginated, arg.ChatID, arg.AfterID, arg.LimitVal)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ChatMessage
for rows.Next() {
var i ChatMessage
if err := rows.Scan(
&i.ID,
&i.ChatID,
&i.ModelConfigID,
&i.CreatedAt,
&i.Role,
&i.Content,
&i.Visibility,
&i.InputTokens,
&i.OutputTokens,
&i.TotalTokens,
&i.ReasoningTokens,
&i.CacheCreationTokens,
&i.CacheReadTokens,
&i.ContextLimit,
&i.Compressed,
&i.CreatedBy,
&i.ContentVersion,
&i.TotalCostMicros,
&i.RuntimeMs,
&i.Deleted,
&i.ProviderResponseID,
&i.Revision,
&i.ReasoningEffort,
&i.SearchTsv,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
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, deleted, provider_response_id, revision, reasoning_effort, search_tsv
FROM
chat_messages
WHERE
chat_id = $1::uuid
AND CASE
WHEN $2::bigint > 0 THEN id < $2::bigint
ELSE true
END
AND CASE
WHEN $3::bigint > 0 THEN id > $3::bigint
ELSE true
END
AND visibility IN ('user', 'both')
AND deleted = false
ORDER BY
id DESC
LIMIT
COALESCE(NULLIF($4::int, 0), 50)
`
type GetChatMessagesByChatIDDescPaginatedParams struct {
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
BeforeID int64 `db:"before_id" json:"before_id"`
AfterID int64 `db:"after_id" json:"after_id"`
LimitVal int32 `db:"limit_val" json:"limit_val"`
}
func (q *sqlQuerier) GetChatMessagesByChatIDDescPaginated(ctx context.Context, arg GetChatMessagesByChatIDDescPaginatedParams) ([]ChatMessage, error) {
rows, err := q.db.QueryContext(ctx, getChatMessagesByChatIDDescPaginated,
arg.ChatID,
arg.BeforeID,
arg.AfterID,
arg.LimitVal,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ChatMessage
for rows.Next() {
var i ChatMessage
if err := rows.Scan(
&i.ID,
&i.ChatID,
&i.ModelConfigID,
&i.CreatedAt,
&i.Role,
&i.Content,
&i.Visibility,
&i.InputTokens,
&i.OutputTokens,
&i.TotalTokens,
&i.ReasoningTokens,
&i.CacheCreationTokens,
&i.CacheReadTokens,
&i.ContextLimit,
&i.Compressed,
&i.CreatedBy,
&i.ContentVersion,
&i.TotalCostMicros,
&i.RuntimeMs,
&i.Deleted,
&i.ProviderResponseID,
&i.Revision,
&i.ReasoningEffort,
&i.SearchTsv,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getChatMessagesByRevisionForStream = `-- name: GetChatMessagesByRevisionForStream :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, deleted, provider_response_id, revision, reasoning_effort, search_tsv
FROM
chat_messages
WHERE
chat_id = $1::uuid
AND revision > $2::bigint
AND visibility IN ('user', 'both')
ORDER BY
id ASC
`
type GetChatMessagesByRevisionForStreamParams struct {
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
AfterRevision int64 `db:"after_revision" json:"after_revision"`
}
// Stream deltas and reset snapshots must use the same message order.
func (q *sqlQuerier) GetChatMessagesByRevisionForStream(ctx context.Context, arg GetChatMessagesByRevisionForStreamParams) ([]ChatMessage, error) {
rows, err := q.db.QueryContext(ctx, getChatMessagesByRevisionForStream, arg.ChatID, arg.AfterRevision)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ChatMessage
for rows.Next() {
var i ChatMessage
if err := rows.Scan(
&i.ID,
&i.ChatID,
&i.ModelConfigID,
&i.CreatedAt,
&i.Role,
&i.Content,
&i.Visibility,
&i.InputTokens,
&i.OutputTokens,
&i.TotalTokens,
&i.ReasoningTokens,
&i.CacheCreationTokens,
&i.CacheReadTokens,
&i.ContextLimit,
&i.Compressed,
&i.CreatedBy,
&i.ContentVersion,
&i.TotalCostMicros,
&i.RuntimeMs,
&i.Deleted,
&i.ProviderResponseID,
&i.Revision,
&i.ReasoningEffort,
&i.SearchTsv,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getChatMessagesForPromptByChatID = `-- name: GetChatMessagesForPromptByChatID :many
WITH latest_compressed_summary AS (
SELECT
id
FROM
chat_messages
WHERE
chat_id = $1::uuid
AND compressed = TRUE
AND deleted = false
AND visibility = 'model'
ORDER BY
id DESC
LIMIT
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, deleted, provider_response_id, revision, reasoning_effort, search_tsv
FROM
chat_messages
WHERE
chat_id = $1::uuid
AND visibility IN ('model', 'both')
AND deleted = false
AND (
(
role = 'system'
AND compressed = FALSE
)
OR (
compressed = FALSE
AND (
NOT EXISTS (
SELECT
1
FROM
latest_compressed_summary
)
OR id > (
SELECT
id
FROM
latest_compressed_summary
)
)
)
OR id = (
SELECT
id
FROM
latest_compressed_summary
)
)
ORDER BY
id ASC
`
// The compaction boundary and final ordering must use the same key so tool
// results remain after their assistant calls.
func (q *sqlQuerier) GetChatMessagesForPromptByChatID(ctx context.Context, chatID uuid.UUID) ([]ChatMessage, error) {
rows, err := q.db.QueryContext(ctx, getChatMessagesForPromptByChatID, chatID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ChatMessage
for rows.Next() {
var i ChatMessage
if err := rows.Scan(
&i.ID,
&i.ChatID,
&i.ModelConfigID,
&i.CreatedAt,
&i.Role,
&i.Content,
&i.Visibility,
&i.InputTokens,
&i.OutputTokens,
&i.TotalTokens,
&i.ReasoningTokens,
&i.CacheCreationTokens,
&i.CacheReadTokens,
&i.ContextLimit,
&i.Compressed,
&i.CreatedBy,
&i.ContentVersion,
&i.TotalCostMicros,
&i.RuntimeMs,
&i.Deleted,
&i.ProviderResponseID,
&i.Revision,
&i.ReasoningEffort,
&i.SearchTsv,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getChatModelConfigsForTelemetry = `-- name: GetChatModelConfigsForTelemetry :many
SELECT cmc.id, ap.type::text AS provider, cmc.model, cmc.context_limit, cmc.enabled, cmc.is_default
FROM chat_model_configs cmc
JOIN ai_providers ap ON ap.id = cmc.ai_provider_id
WHERE cmc.deleted = false
`
type GetChatModelConfigsForTelemetryRow struct {
ID uuid.UUID `db:"id" json:"id"`
Provider string `db:"provider" json:"provider"`
Model string `db:"model" json:"model"`
ContextLimit int64 `db:"context_limit" json:"context_limit"`
Enabled bool `db:"enabled" json:"enabled"`
IsDefault bool `db:"is_default" json:"is_default"`
}
// Returns all model configurations for telemetry snapshot collection.
// deleted = false guarantees ai_provider_id is non-null, so INNER JOIN is safe.
func (q *sqlQuerier) GetChatModelConfigsForTelemetry(ctx context.Context) ([]GetChatModelConfigsForTelemetryRow, error) {
rows, err := q.db.QueryContext(ctx, getChatModelConfigsForTelemetry)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetChatModelConfigsForTelemetryRow
for rows.Next() {
var i GetChatModelConfigsForTelemetryRow
if err := rows.Scan(
&i.ID,
&i.Provider,
&i.Model,
&i.ContextLimit,
&i.Enabled,
&i.IsDefault,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getChatQueuedMessageByID = `-- name: GetChatQueuedMessageByID :one
SELECT id, chat_id, content, created_at, model_config_id, position, created_by, reasoning_effort FROM chat_queued_messages
WHERE id = $1::bigint AND chat_id = $2::uuid
`
type GetChatQueuedMessageByIDParams struct {
ID int64 `db:"id" json:"id"`
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
}
func (q *sqlQuerier) GetChatQueuedMessageByID(ctx context.Context, arg GetChatQueuedMessageByIDParams) (ChatQueuedMessage, error) {
row := q.db.QueryRowContext(ctx, getChatQueuedMessageByID, arg.ID, arg.ChatID)
var i ChatQueuedMessage
err := row.Scan(
&i.ID,
&i.ChatID,
&i.Content,
&i.CreatedAt,
&i.ModelConfigID,
&i.Position,
&i.CreatedBy,
&i.ReasoningEffort,
)
return i, err
}
const getChatQueuedMessageHead = `-- name: GetChatQueuedMessageHead :one
SELECT id, chat_id, content, created_at, model_config_id, position, created_by, reasoning_effort FROM chat_queued_messages
WHERE chat_id = $1::uuid
ORDER BY position ASC, id ASC
LIMIT 1
`
// Returns the queue head (lowest position, then lowest id).
func (q *sqlQuerier) GetChatQueuedMessageHead(ctx context.Context, chatID uuid.UUID) (ChatQueuedMessage, error) {
row := q.db.QueryRowContext(ctx, getChatQueuedMessageHead, chatID)
var i ChatQueuedMessage
err := row.Scan(
&i.ID,
&i.ChatID,
&i.Content,
&i.CreatedAt,
&i.ModelConfigID,
&i.Position,
&i.CreatedBy,
&i.ReasoningEffort,
)
return i, err
}
const getChatQueuedMessages = `-- name: GetChatQueuedMessages :many
SELECT id, chat_id, content, created_at, model_config_id, position, created_by, reasoning_effort FROM chat_queued_messages
WHERE chat_id = $1
ORDER BY created_at ASC, id ASC
`
func (q *sqlQuerier) GetChatQueuedMessages(ctx context.Context, chatID uuid.UUID) ([]ChatQueuedMessage, error) {
rows, err := q.db.QueryContext(ctx, getChatQueuedMessages, chatID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ChatQueuedMessage
for rows.Next() {
var i ChatQueuedMessage
if err := rows.Scan(
&i.ID,
&i.ChatID,
&i.Content,
&i.CreatedAt,
&i.ModelConfigID,
&i.Position,
&i.CreatedBy,
&i.ReasoningEffort,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getChatQueuedMessagesByPosition = `-- name: GetChatQueuedMessagesByPosition :many
SELECT id, chat_id, content, created_at, model_config_id, position, created_by, reasoning_effort FROM chat_queued_messages
WHERE chat_id = $1::uuid
ORDER BY position ASC, id ASC
`
// Returns queued messages in state-machine order (position ASC, id ASC).
func (q *sqlQuerier) GetChatQueuedMessagesByPosition(ctx context.Context, chatID uuid.UUID) ([]ChatQueuedMessage, error) {
rows, err := q.db.QueryContext(ctx, getChatQueuedMessagesByPosition, chatID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ChatQueuedMessage
for rows.Next() {
var i ChatQueuedMessage
if err := rows.Scan(
&i.ID,
&i.ChatID,
&i.Content,
&i.CreatedAt,
&i.ModelConfigID,
&i.Position,
&i.CreatedBy,
&i.ReasoningEffort,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getChatStreamSyncRows = `-- name: GetChatStreamSyncRows :many
SELECT
id,
snapshot_version,
history_version,
queue_version,
retry_state_version,
generation_attempt,
status,
worker_id
FROM chats
WHERE id = ANY($1::uuid[])
ORDER BY id ASC
`
type GetChatStreamSyncRowsRow struct {
ID uuid.UUID `db:"id" json:"id"`
SnapshotVersion int64 `db:"snapshot_version" json:"snapshot_version"`
HistoryVersion int64 `db:"history_version" json:"history_version"`
QueueVersion int64 `db:"queue_version" json:"queue_version"`
RetryStateVersion int64 `db:"retry_state_version" json:"retry_state_version"`
GenerationAttempt int64 `db:"generation_attempt" json:"generation_attempt"`
Status ChatStatus `db:"status" json:"status"`
WorkerID uuid.NullUUID `db:"worker_id" json:"worker_id"`
}
func (q *sqlQuerier) GetChatStreamSyncRows(ctx context.Context, ids []uuid.UUID) ([]GetChatStreamSyncRowsRow, error) {
rows, err := q.db.QueryContext(ctx, getChatStreamSyncRows, pq.Array(ids))
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetChatStreamSyncRowsRow
for rows.Next() {
var i GetChatStreamSyncRowsRow
if err := rows.Scan(
&i.ID,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
&i.RetryStateVersion,
&i.GenerationAttempt,
&i.Status,
&i.WorkerID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getChatUserPromptsByChatID = `-- name: GetChatUserPromptsByChatID :many
SELECT
cm.id,
string_agg(part->>'text', '' ORDER BY ordinality)::text AS text
FROM
chat_messages cm,
jsonb_array_elements(cm.content) WITH ORDINALITY AS t(part, ordinality)
WHERE
cm.chat_id = $1::uuid
AND cm.role = 'user'
AND cm.deleted = false
AND cm.visibility IN ('user', 'both')
AND jsonb_typeof(cm.content) = 'array'
AND part->>'type' = 'text'
GROUP BY
cm.id
HAVING
string_agg(part->>'text', '') ~ '\S'
ORDER BY
cm.id DESC
LIMIT
COALESCE(NULLIF($2::int, 0), 500)
`
type GetChatUserPromptsByChatIDParams struct {
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
LimitVal int32 `db:"limit_val" json:"limit_val"`
}
type GetChatUserPromptsByChatIDRow struct {
ID int64 `db:"id" json:"id"`
Text string `db:"text" json:"text"`
}
// Returns the concatenated text of each user-visible user prompt in a
// chat, newest first. Used by the composer to populate the up/down
// arrow prompt-history cycle. Non-text parts (tool calls, files,
// attachments, ...) are excluded; messages whose text payload is
// entirely whitespace are dropped so cycling never lands on a blank
// entry. The jsonb_typeof guard skips legacy V0 rows whose content is
// a scalar JSON string (predates migration 000434) so the lateral
// jsonb_array_elements never raises "cannot extract elements from a
// scalar". Backed by idx_chat_messages_user_prompts.
func (q *sqlQuerier) GetChatUserPromptsByChatID(ctx context.Context, arg GetChatUserPromptsByChatIDParams) ([]GetChatUserPromptsByChatIDRow, error) {
rows, err := q.db.QueryContext(ctx, getChatUserPromptsByChatID, arg.ChatID, arg.LimitVal)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetChatUserPromptsByChatIDRow
for rows.Next() {
var i GetChatUserPromptsByChatIDRow
if err := rows.Scan(&i.ID, &i.Text); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getChatWorkerAcquisitionCandidates = `-- name: GetChatWorkerAcquisitionCandidates :many
SELECT
chats_expanded.id, chats_expanded.owner_id, chats_expanded.workspace_id, chats_expanded.title, chats_expanded.status, chats_expanded.worker_id, chats_expanded.started_at, chats_expanded.heartbeat_at, chats_expanded.created_at, chats_expanded.updated_at, chats_expanded.parent_chat_id, chats_expanded.root_chat_id, chats_expanded.last_model_config_id, chats_expanded.last_reasoning_effort, chats_expanded.archived, chats_expanded.last_error, chats_expanded.mode, chats_expanded.mcp_server_ids, chats_expanded.labels, chats_expanded.build_id, chats_expanded.agent_id, chats_expanded.pin_order, chats_expanded.last_read_message_id, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.summary, chats_expanded.summary_generated_at, chats_expanded.snapshot_version, chats_expanded.history_version, chats_expanded.queue_version, chats_expanded.generation_attempt, chats_expanded.retry_state, chats_expanded.retry_state_version, chats_expanded.runner_id, chats_expanded.requires_action_deadline_at, chats_expanded.user_acl, chats_expanded.group_acl, chats_expanded.owner_username, chats_expanded.owner_name, chats_expanded.context_aggregate_hash, chats_expanded.context_dirty_since, chats_expanded.context_dirty_resources, chats_expanded.context_error, chats_expanded.compaction_requested_at,
chat_heartbeats.heartbeat_at AS current_heartbeat_at,
NOT EXISTS (
SELECT 1
FROM chat_heartbeats current_lease
WHERE current_lease.chat_id = chats_expanded.id
AND current_lease.runner_id = chats_expanded.runner_id
AND current_lease.heartbeat_at > NOW() - (INTERVAL '1 second' * $1::int)
) AS heartbeat_stale
FROM chats_expanded
LEFT JOIN chat_heartbeats
ON chat_heartbeats.chat_id = chats_expanded.id
AND chat_heartbeats.runner_id = chats_expanded.runner_id
WHERE
chats_expanded.status IN ('running'::chat_status, 'interrupting'::chat_status, 'requires_action'::chat_status)
AND chats_expanded.archived = false
AND (
chats_expanded.worker_id IS NULL
OR chats_expanded.runner_id IS NULL
OR NOT EXISTS (
SELECT 1
FROM chat_heartbeats current_lease
WHERE current_lease.chat_id = chats_expanded.id
AND current_lease.runner_id = chats_expanded.runner_id
AND current_lease.heartbeat_at > NOW() - (INTERVAL '1 second' * $1::int)
)
)
ORDER BY chats_expanded.updated_at ASC, chats_expanded.id ASC
LIMIT $2::int
`
type GetChatWorkerAcquisitionCandidatesParams struct {
StaleSeconds int32 `db:"stale_seconds" json:"stale_seconds"`
LimitCount int32 `db:"limit_count" json:"limit_count"`
}
type GetChatWorkerAcquisitionCandidatesRow struct {
ID uuid.UUID `db:"id" json:"id"`
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"`
Title string `db:"title" json:"title"`
Status ChatStatus `db:"status" json:"status"`
WorkerID uuid.NullUUID `db:"worker_id" json:"worker_id"`
StartedAt sql.NullTime `db:"started_at" json:"started_at"`
HeartbeatAt sql.NullTime `db:"heartbeat_at" json:"heartbeat_at"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
ParentChatID uuid.NullUUID `db:"parent_chat_id" json:"parent_chat_id"`
RootChatID uuid.NullUUID `db:"root_chat_id" json:"root_chat_id"`
LastModelConfigID uuid.UUID `db:"last_model_config_id" json:"last_model_config_id"`
LastReasoningEffort NullChatReasoningEffort `db:"last_reasoning_effort" json:"last_reasoning_effort"`
Archived bool `db:"archived" json:"archived"`
LastError pqtype.NullRawMessage `db:"last_error" json:"last_error"`
Mode NullChatMode `db:"mode" json:"mode"`
MCPServerIDs []uuid.UUID `db:"mcp_server_ids" json:"mcp_server_ids"`
Labels StringMap `db:"labels" json:"labels"`
BuildID uuid.NullUUID `db:"build_id" json:"build_id"`
AgentID uuid.NullUUID `db:"agent_id" json:"agent_id"`
PinOrder int32 `db:"pin_order" json:"pin_order"`
LastReadMessageID sql.NullInt64 `db:"last_read_message_id" json:"last_read_message_id"`
DynamicTools pqtype.NullRawMessage `db:"dynamic_tools" json:"dynamic_tools"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
PlanMode NullChatPlanMode `db:"plan_mode" json:"plan_mode"`
ClientType ChatClientType `db:"client_type" json:"client_type"`
LastTurnSummary sql.NullString `db:"last_turn_summary" json:"last_turn_summary"`
Summary sql.NullString `db:"summary" json:"summary"`
SummaryGeneratedAt sql.NullTime `db:"summary_generated_at" json:"summary_generated_at"`
SnapshotVersion int64 `db:"snapshot_version" json:"snapshot_version"`
HistoryVersion int64 `db:"history_version" json:"history_version"`
QueueVersion int64 `db:"queue_version" json:"queue_version"`
GenerationAttempt int64 `db:"generation_attempt" json:"generation_attempt"`
RetryState pqtype.NullRawMessage `db:"retry_state" json:"retry_state"`
RetryStateVersion int64 `db:"retry_state_version" json:"retry_state_version"`
RunnerID uuid.NullUUID `db:"runner_id" json:"runner_id"`
RequiresActionDeadlineAt sql.NullTime `db:"requires_action_deadline_at" json:"requires_action_deadline_at"`
UserACL ChatACL `db:"user_acl" json:"user_acl"`
GroupACL ChatACL `db:"group_acl" json:"group_acl"`
OwnerUsername string `db:"owner_username" json:"owner_username"`
OwnerName string `db:"owner_name" json:"owner_name"`
ContextAggregateHash []byte `db:"context_aggregate_hash" json:"context_aggregate_hash"`
ContextDirtySince sql.NullTime `db:"context_dirty_since" json:"context_dirty_since"`
ContextDirtyResources pqtype.NullRawMessage `db:"context_dirty_resources" json:"context_dirty_resources"`
ContextError string `db:"context_error" json:"context_error"`
CompactionRequestedAt sql.NullTime `db:"compaction_requested_at" json:"compaction_requested_at"`
CurrentHeartbeatAt sql.NullTime `db:"current_heartbeat_at" json:"current_heartbeat_at"`
HeartbeatStale bool `db:"heartbeat_stale" json:"heartbeat_stale"`
}
// Returns chats that workers may try to acquire. Candidates must be:
// - in a worker-runnable execution status;
// - unarchived; and
// - missing ownership, carrying inconsistent ownership, or lacking a
// fresh heartbeat for the assigned runner.
//
// Missing ownership is worker_id IS NULL. Inconsistent ownership is
// runner_id IS NULL while worker_id is set. Stale ownership is no
// heartbeat row for (chat_id, runner_id), or one older than
// @stale_seconds by database time. Candidates are ordered by oldest
// updated_at first so workers drain stale runnable chats predictably.
func (q *sqlQuerier) GetChatWorkerAcquisitionCandidates(ctx context.Context, arg GetChatWorkerAcquisitionCandidatesParams) ([]GetChatWorkerAcquisitionCandidatesRow, error) {
rows, err := q.db.QueryContext(ctx, getChatWorkerAcquisitionCandidates, arg.StaleSeconds, arg.LimitCount)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetChatWorkerAcquisitionCandidatesRow
for rows.Next() {
var i GetChatWorkerAcquisitionCandidatesRow
if err := rows.Scan(
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.Title,
&i.Status,
&i.WorkerID,
&i.StartedAt,
&i.HeartbeatAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ParentChatID,
&i.RootChatID,
&i.LastModelConfigID,
&i.LastReasoningEffort,
&i.Archived,
&i.LastError,
&i.Mode,
pq.Array(&i.MCPServerIDs),
&i.Labels,
&i.BuildID,
&i.AgentID,
&i.PinOrder,
&i.LastReadMessageID,
&i.DynamicTools,
&i.OrganizationID,
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
&i.Summary,
&i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
&i.GenerationAttempt,
&i.RetryState,
&i.RetryStateVersion,
&i.RunnerID,
&i.RequiresActionDeadlineAt,
&i.UserACL,
&i.GroupACL,
&i.OwnerUsername,
&i.OwnerName,
&i.ContextAggregateHash,
&i.ContextDirtySince,
&i.ContextDirtyResources,
&i.ContextError,
&i.CompactionRequestedAt,
&i.CurrentHeartbeatAt,
&i.HeartbeatStale,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getChats = `-- name: GetChats :many
WITH cursor_chat AS (
SELECT
pin_order,
updated_at,
id
FROM chats
WHERE id = $7
)
SELECT
chats_expanded.id, chats_expanded.owner_id, chats_expanded.workspace_id, chats_expanded.title, chats_expanded.status, chats_expanded.worker_id, chats_expanded.started_at, chats_expanded.heartbeat_at, chats_expanded.created_at, chats_expanded.updated_at, chats_expanded.parent_chat_id, chats_expanded.root_chat_id, chats_expanded.last_model_config_id, chats_expanded.last_reasoning_effort, chats_expanded.archived, chats_expanded.last_error, chats_expanded.mode, chats_expanded.mcp_server_ids, chats_expanded.labels, chats_expanded.build_id, chats_expanded.agent_id, chats_expanded.pin_order, chats_expanded.last_read_message_id, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.summary, chats_expanded.summary_generated_at, chats_expanded.snapshot_version, chats_expanded.history_version, chats_expanded.queue_version, chats_expanded.generation_attempt, chats_expanded.retry_state, chats_expanded.retry_state_version, chats_expanded.runner_id, chats_expanded.requires_action_deadline_at, chats_expanded.user_acl, chats_expanded.group_acl, chats_expanded.owner_username, chats_expanded.owner_name, chats_expanded.context_aggregate_hash, chats_expanded.context_dirty_since, chats_expanded.context_dirty_resources, chats_expanded.context_error, chats_expanded.compaction_requested_at,
EXISTS (
SELECT 1 FROM chat_messages cm
WHERE cm.chat_id = chats_expanded.id
AND cm.role = 'assistant'
AND cm.deleted = false
AND cm.id > COALESCE(chats_expanded.last_read_message_id, 0)
) AS has_unread
FROM
chats_expanded
WHERE
(
(NOT $1::boolean AND NOT $2::boolean)
OR ($1::boolean AND chats_expanded.owner_id = $3::uuid)
OR (
$2::boolean
AND chats_expanded.owner_id != $3::uuid
AND (
chats_expanded.user_acl ? ($4::uuid)::text
OR chats_expanded.group_acl ?| $5::text[]
)
)
)
AND CASE
WHEN $6 :: boolean IS NULL THEN true
ELSE chats_expanded.archived = $6 :: boolean
END
AND CASE
-- Cursor pagination: the last element on a page acts as the cursor.
-- The 4-tuple matches the ORDER BY below. All columns sort DESC
-- (pin_order is negated so lower values sort first in DESC order),
-- which lets us use a single tuple < comparison.
WHEN $7 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN (
(CASE WHEN chats_expanded.pin_order > 0 THEN 1 ELSE 0 END, -chats_expanded.pin_order, chats_expanded.updated_at, chats_expanded.id) < (
SELECT
CASE WHEN cursor_chat.pin_order > 0 THEN 1 ELSE 0 END,
-cursor_chat.pin_order,
cursor_chat.updated_at,
cursor_chat.id
FROM
cursor_chat
)
)
ELSE true
END
AND CASE
WHEN $8::jsonb IS NOT NULL THEN chats_expanded.labels @> $8::jsonb
ELSE true
END
-- Match chats whose linked diff URL (e.g. a pull request URL)
-- equals the given value, case-insensitively. The URL may live on
-- a delegated sub-agent's diff status, so we surface the root chat
-- when any descendant matches.
AND CASE
WHEN $9::text IS NOT NULL THEN EXISTS (
SELECT 1
FROM chat_diff_statuses cds
JOIN chats c2 ON c2.id = cds.chat_id
WHERE cds.url IS NOT NULL
AND cds.url <> ''
AND LOWER(cds.url) = LOWER($9::text)
AND (c2.id = chats_expanded.id OR c2.root_chat_id = chats_expanded.id)
)
ELSE true
END
-- Filter by title substring (case-insensitive). Applied when the
-- caller provides a non-empty title_query.
AND CASE
WHEN $10 :: text != '' THEN chats_expanded.title ILIKE '%' || $10 || '%'
ELSE true
END
AND CASE
WHEN $11::boolean IS NOT NULL THEN (
EXISTS (
SELECT 1 FROM chat_messages cm
WHERE cm.chat_id = chats_expanded.id
AND cm.role = 'assistant'
AND cm.deleted = false
AND cm.id > COALESCE(chats_expanded.last_read_message_id, 0)
)
) = $11::boolean
ELSE true
END
-- Filter by pull request status. Unlike the diff_url filter above,
-- this intentionally checks only the root chat's own diff status.
-- Child chats share the same workspace and git branch as their
-- parent, so gitsync populates identical PR state on both; traversing
-- descendants would be redundant.
AND CASE
WHEN COALESCE(array_length($12::text[], 1), 0) > 0 THEN EXISTS (
SELECT 1
FROM chat_diff_statuses cds
WHERE cds.chat_id = chats_expanded.id
AND (
CASE
WHEN cds.pull_request_state = 'open' AND cds.pull_request_draft THEN 'draft'
WHEN cds.pull_request_state = 'open' THEN 'open'
ELSE cds.pull_request_state
END
) = ANY($12::text[])
)
ELSE true
END
-- Filter by PR number (exact match on chat's diff status).
AND CASE
WHEN $13::int != 0 THEN EXISTS (
SELECT 1
FROM chat_diff_statuses cds
WHERE cds.chat_id = chats_expanded.id
AND cds.pr_number = $13
)
ELSE true
END
-- Filter by repository (substring match on remote origin or PR URL).
AND CASE
WHEN $14::text != '' THEN EXISTS (
SELECT 1
FROM chat_diff_statuses cds
WHERE cds.chat_id = chats_expanded.id
AND (
cds.git_remote_origin ILIKE '%' || $14 || '%'
OR cds.url ILIKE '%' || $14 || '%'
)
)
ELSE true
END
-- Filter by pull request title (case-insensitive substring).
AND CASE
WHEN $15::text != '' THEN EXISTS (
SELECT 1
FROM chat_diff_statuses cds
WHERE cds.chat_id = chats_expanded.id
AND cds.pull_request_title ILIKE '%' || $15 || '%'
)
ELSE true
END
-- websearch_to_tsquery accepts quoted phrases, OR, and -negation;
-- the 'simple' config folds case and skips stemming.
AND CASE
WHEN $16::text != '' THEN (
-- Served by idx_chats_title_fts.
to_tsvector('simple', chats_expanded.title) @@ websearch_to_tsquery('simple', $16)
-- Served by idx_chat_diff_statuses_pr_title_fts.
OR EXISTS (
SELECT 1
FROM chat_diff_statuses cds
WHERE cds.chat_id = chats_expanded.id
AND to_tsvector('simple', cds.pull_request_title) @@ websearch_to_tsquery('simple', $16)
)
-- The WHERE clause must repeat the predicate of the partial index
-- idx_chat_messages_search_tsv so the planner can use it. Additional
-- filters should still be fine.
OR EXISTS (
SELECT 1
FROM chat_messages cm
WHERE cm.chat_id = chats_expanded.id
AND cm.search_tsv IS NOT NULL
AND cm.deleted = false
AND cm.visibility IN ('user', 'both')
AND cm.role IN ('user', 'assistant')
AND cm.search_tsv @@ websearch_to_tsquery('simple', $16)
)
-- Skip an explicit pr_number lookup unless the search is a valid bigint.
OR CASE
WHEN $16 ~ '^[0-9]{1,18}$' THEN EXISTS (
SELECT 1
FROM chat_diff_statuses cds
WHERE cds.chat_id = chats_expanded.id
AND cds.pr_number IS NOT NULL
AND cds.pr_number = $16::bigint
)
ELSE false
END
)
ELSE true
END
-- Paginate over root chats only. Children are fetched
-- separately via GetChildChatsByParentIDs and embedded under
-- each parent. Other callers that need the full set should
-- use a narrower query (e.g. GetChatsByWorkspaceIDs).
AND chats_expanded.parent_chat_id IS NULL
-- Authorize Filter clause will be injected below in GetAuthorizedChats
-- @authorize_filter
ORDER BY
-- Pinned chats (pin_order > 0) sort before unpinned ones. Within
-- pinned chats, lower pin_order values come first. The negation
-- trick (-pin_order) keeps all sort columns DESC so the cursor
-- tuple < comparison works with uniform direction.
CASE WHEN chats_expanded.pin_order > 0 THEN 1 ELSE 0 END DESC,
-chats_expanded.pin_order DESC,
chats_expanded.updated_at DESC,
chats_expanded.id DESC
OFFSET $17
LIMIT
-- The chat list is unbounded and expected to grow large.
-- Default to 50 to prevent accidental excessively large queries.
COALESCE(NULLIF($18 :: int, 0), 50)
`
type GetChatsParams struct {
OwnedOnly bool `db:"owned_only" json:"owned_only"`
SharedOnly bool `db:"shared_only" json:"shared_only"`
ViewerID uuid.UUID `db:"viewer_id" json:"viewer_id"`
SharedWithUserID uuid.UUID `db:"shared_with_user_id" json:"shared_with_user_id"`
SharedWithGroupIds []string `db:"shared_with_group_ids" json:"shared_with_group_ids"`
Archived sql.NullBool `db:"archived" json:"archived"`
AfterID uuid.UUID `db:"after_id" json:"after_id"`
LabelFilter pqtype.NullRawMessage `db:"label_filter" json:"label_filter"`
DiffURL sql.NullString `db:"diff_url" json:"diff_url"`
TitleQuery string `db:"title_query" json:"title_query"`
HasUnread sql.NullBool `db:"has_unread" json:"has_unread"`
PullRequestStatuses []string `db:"pull_request_statuses" json:"pull_request_statuses"`
PrNumber int32 `db:"pr_number" json:"pr_number"`
RepoQuery string `db:"repo_query" json:"repo_query"`
PrTitleQuery string `db:"pr_title_query" json:"pr_title_query"`
Search string `db:"search" json:"search"`
OffsetOpt int32 `db:"offset_opt" json:"offset_opt"`
LimitOpt int32 `db:"limit_opt" json:"limit_opt"`
}
type GetChatsRow struct {
Chat Chat `db:"chat" json:"chat"`
HasUnread bool `db:"has_unread" json:"has_unread"`
}
func (q *sqlQuerier) GetChats(ctx context.Context, arg GetChatsParams) ([]GetChatsRow, error) {
rows, err := q.db.QueryContext(ctx, getChats,
arg.OwnedOnly,
arg.SharedOnly,
arg.ViewerID,
arg.SharedWithUserID,
pq.Array(arg.SharedWithGroupIds),
arg.Archived,
arg.AfterID,
arg.LabelFilter,
arg.DiffURL,
arg.TitleQuery,
arg.HasUnread,
pq.Array(arg.PullRequestStatuses),
arg.PrNumber,
arg.RepoQuery,
arg.PrTitleQuery,
arg.Search,
arg.OffsetOpt,
arg.LimitOpt,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetChatsRow
for rows.Next() {
var i GetChatsRow
if err := rows.Scan(
&i.Chat.ID,
&i.Chat.OwnerID,
&i.Chat.WorkspaceID,
&i.Chat.Title,
&i.Chat.Status,
&i.Chat.WorkerID,
&i.Chat.StartedAt,
&i.Chat.HeartbeatAt,
&i.Chat.CreatedAt,
&i.Chat.UpdatedAt,
&i.Chat.ParentChatID,
&i.Chat.RootChatID,
&i.Chat.LastModelConfigID,
&i.Chat.LastReasoningEffort,
&i.Chat.Archived,
&i.Chat.LastError,
&i.Chat.Mode,
pq.Array(&i.Chat.MCPServerIDs),
&i.Chat.Labels,
&i.Chat.BuildID,
&i.Chat.AgentID,
&i.Chat.PinOrder,
&i.Chat.LastReadMessageID,
&i.Chat.DynamicTools,
&i.Chat.OrganizationID,
&i.Chat.PlanMode,
&i.Chat.ClientType,
&i.Chat.LastTurnSummary,
&i.Chat.Summary,
&i.Chat.SummaryGeneratedAt,
&i.Chat.SnapshotVersion,
&i.Chat.HistoryVersion,
&i.Chat.QueueVersion,
&i.Chat.GenerationAttempt,
&i.Chat.RetryState,
&i.Chat.RetryStateVersion,
&i.Chat.RunnerID,
&i.Chat.RequiresActionDeadlineAt,
&i.Chat.UserACL,
&i.Chat.GroupACL,
&i.Chat.OwnerUsername,
&i.Chat.OwnerName,
&i.Chat.ContextAggregateHash,
&i.Chat.ContextDirtySince,
&i.Chat.ContextDirtyResources,
&i.Chat.ContextError,
&i.Chat.CompactionRequestedAt,
&i.HasUnread,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getChatsByChatFileID = `-- name: GetChatsByChatFileID :many
SELECT
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, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM
chats_expanded
WHERE
id IN (
SELECT chat_id
FROM chat_file_links
WHERE file_id = $1::uuid
)
-- Authorize Filter clause will be injected below in GetAuthorizedChatsByChatFileID.
-- @authorize_filter
`
func (q *sqlQuerier) GetChatsByChatFileID(ctx context.Context, fileID uuid.UUID) ([]Chat, error) {
rows, err := q.db.QueryContext(ctx, getChatsByChatFileID, fileID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Chat
for rows.Next() {
var i Chat
if err := rows.Scan(
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.Title,
&i.Status,
&i.WorkerID,
&i.StartedAt,
&i.HeartbeatAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ParentChatID,
&i.RootChatID,
&i.LastModelConfigID,
&i.LastReasoningEffort,
&i.Archived,
&i.LastError,
&i.Mode,
pq.Array(&i.MCPServerIDs),
&i.Labels,
&i.BuildID,
&i.AgentID,
&i.PinOrder,
&i.LastReadMessageID,
&i.DynamicTools,
&i.OrganizationID,
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
&i.Summary,
&i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
&i.GenerationAttempt,
&i.RetryState,
&i.RetryStateVersion,
&i.RunnerID,
&i.RequiresActionDeadlineAt,
&i.UserACL,
&i.GroupACL,
&i.OwnerUsername,
&i.OwnerName,
&i.ContextAggregateHash,
&i.ContextDirtySince,
&i.ContextDirtyResources,
&i.ContextError,
&i.CompactionRequestedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getChatsByIDsForRunnerSync = `-- name: GetChatsByIDsForRunnerSync :many
SELECT 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, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
WHERE id = ANY($1::uuid[])
ORDER BY id ASC
`
func (q *sqlQuerier) GetChatsByIDsForRunnerSync(ctx context.Context, ids []uuid.UUID) ([]Chat, error) {
rows, err := q.db.QueryContext(ctx, getChatsByIDsForRunnerSync, pq.Array(ids))
if err != nil {
return nil, err
}
defer rows.Close()
var items []Chat
for rows.Next() {
var i Chat
if err := rows.Scan(
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.Title,
&i.Status,
&i.WorkerID,
&i.StartedAt,
&i.HeartbeatAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ParentChatID,
&i.RootChatID,
&i.LastModelConfigID,
&i.LastReasoningEffort,
&i.Archived,
&i.LastError,
&i.Mode,
pq.Array(&i.MCPServerIDs),
&i.Labels,
&i.BuildID,
&i.AgentID,
&i.PinOrder,
&i.LastReadMessageID,
&i.DynamicTools,
&i.OrganizationID,
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
&i.Summary,
&i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
&i.GenerationAttempt,
&i.RetryState,
&i.RetryStateVersion,
&i.RunnerID,
&i.RequiresActionDeadlineAt,
&i.UserACL,
&i.GroupACL,
&i.OwnerUsername,
&i.OwnerName,
&i.ContextAggregateHash,
&i.ContextDirtySince,
&i.ContextDirtyResources,
&i.ContextError,
&i.CompactionRequestedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getChatsByWorkspaceIDs = `-- name: GetChatsByWorkspaceIDs :many
SELECT 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, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
WHERE archived = false
AND workspace_id = ANY($1::uuid[])
ORDER BY workspace_id, updated_at DESC
`
func (q *sqlQuerier) GetChatsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]Chat, error) {
rows, err := q.db.QueryContext(ctx, getChatsByWorkspaceIDs, pq.Array(ids))
if err != nil {
return nil, err
}
defer rows.Close()
var items []Chat
for rows.Next() {
var i Chat
if err := rows.Scan(
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.Title,
&i.Status,
&i.WorkerID,
&i.StartedAt,
&i.HeartbeatAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ParentChatID,
&i.RootChatID,
&i.LastModelConfigID,
&i.LastReasoningEffort,
&i.Archived,
&i.LastError,
&i.Mode,
pq.Array(&i.MCPServerIDs),
&i.Labels,
&i.BuildID,
&i.AgentID,
&i.PinOrder,
&i.LastReadMessageID,
&i.DynamicTools,
&i.OrganizationID,
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
&i.Summary,
&i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
&i.GenerationAttempt,
&i.RetryState,
&i.RetryStateVersion,
&i.RunnerID,
&i.RequiresActionDeadlineAt,
&i.UserACL,
&i.GroupACL,
&i.OwnerUsername,
&i.OwnerName,
&i.ContextAggregateHash,
&i.ContextDirtySince,
&i.ContextDirtyResources,
&i.ContextError,
&i.CompactionRequestedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getChatsUpdatedAfter = `-- name: GetChatsUpdatedAfter :many
SELECT
c.id, c.owner_id, c.created_at, c.updated_at, c.status,
(c.parent_chat_id IS NOT NULL)::bool AS has_parent,
c.root_chat_id, c.workspace_id,
c.mode, c.archived, c.last_model_config_id, c.client_type,
cds.pull_request_state
FROM chats c
LEFT JOIN chat_diff_statuses cds ON cds.chat_id = c.id
WHERE c.updated_at > $1
`
type GetChatsUpdatedAfterRow struct {
ID uuid.UUID `db:"id" json:"id"`
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Status ChatStatus `db:"status" json:"status"`
HasParent bool `db:"has_parent" json:"has_parent"`
RootChatID uuid.NullUUID `db:"root_chat_id" json:"root_chat_id"`
WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"`
Mode NullChatMode `db:"mode" json:"mode"`
Archived bool `db:"archived" json:"archived"`
LastModelConfigID uuid.UUID `db:"last_model_config_id" json:"last_model_config_id"`
ClientType ChatClientType `db:"client_type" json:"client_type"`
PullRequestState sql.NullString `db:"pull_request_state" json:"pull_request_state"`
}
// Retrieves chats updated after the given timestamp for telemetry
// snapshot collection. Uses updated_at so that long-running chats
// still appear in each snapshot window while they are active.
func (q *sqlQuerier) GetChatsUpdatedAfter(ctx context.Context, updatedAfter time.Time) ([]GetChatsUpdatedAfterRow, error) {
rows, err := q.db.QueryContext(ctx, getChatsUpdatedAfter, updatedAfter)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetChatsUpdatedAfterRow
for rows.Next() {
var i GetChatsUpdatedAfterRow
if err := rows.Scan(
&i.ID,
&i.OwnerID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Status,
&i.HasParent,
&i.RootChatID,
&i.WorkspaceID,
&i.Mode,
&i.Archived,
&i.LastModelConfigID,
&i.ClientType,
&i.PullRequestState,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getChildChatsByParentIDs = `-- name: GetChildChatsByParentIDs :many
SELECT
chats_expanded.id, chats_expanded.owner_id, chats_expanded.workspace_id, chats_expanded.title, chats_expanded.status, chats_expanded.worker_id, chats_expanded.started_at, chats_expanded.heartbeat_at, chats_expanded.created_at, chats_expanded.updated_at, chats_expanded.parent_chat_id, chats_expanded.root_chat_id, chats_expanded.last_model_config_id, chats_expanded.last_reasoning_effort, chats_expanded.archived, chats_expanded.last_error, chats_expanded.mode, chats_expanded.mcp_server_ids, chats_expanded.labels, chats_expanded.build_id, chats_expanded.agent_id, chats_expanded.pin_order, chats_expanded.last_read_message_id, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.summary, chats_expanded.summary_generated_at, chats_expanded.snapshot_version, chats_expanded.history_version, chats_expanded.queue_version, chats_expanded.generation_attempt, chats_expanded.retry_state, chats_expanded.retry_state_version, chats_expanded.runner_id, chats_expanded.requires_action_deadline_at, chats_expanded.user_acl, chats_expanded.group_acl, chats_expanded.owner_username, chats_expanded.owner_name, chats_expanded.context_aggregate_hash, chats_expanded.context_dirty_since, chats_expanded.context_dirty_resources, chats_expanded.context_error, chats_expanded.compaction_requested_at,
EXISTS (
SELECT 1 FROM chat_messages cm
WHERE cm.chat_id = chats_expanded.id
AND cm.role = 'assistant'
AND cm.deleted = false
AND cm.id > COALESCE(chats_expanded.last_read_message_id, 0)
) AS has_unread
FROM
chats_expanded
WHERE
chats_expanded.parent_chat_id = ANY($1 :: uuid[])
AND CASE
WHEN $2 :: boolean IS NULL THEN true
ELSE chats_expanded.archived = $2 :: boolean
END
ORDER BY
chats_expanded.created_at DESC,
chats_expanded.id DESC
`
type GetChildChatsByParentIDsParams struct {
ParentIds []uuid.UUID `db:"parent_ids" json:"parent_ids"`
Archived sql.NullBool `db:"archived" json:"archived"`
}
type GetChildChatsByParentIDsRow struct {
Chat Chat `db:"chat" json:"chat"`
HasUnread bool `db:"has_unread" json:"has_unread"`
}
// Fetches child chats of the given parents, optionally filtered by
// archive state (NULL = all, true/false = match). The archive
// invariant (parent archived implies child archived) is enforced
// at write time, not here.
func (q *sqlQuerier) GetChildChatsByParentIDs(ctx context.Context, arg GetChildChatsByParentIDsParams) ([]GetChildChatsByParentIDsRow, error) {
rows, err := q.db.QueryContext(ctx, getChildChatsByParentIDs, pq.Array(arg.ParentIds), arg.Archived)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetChildChatsByParentIDsRow
for rows.Next() {
var i GetChildChatsByParentIDsRow
if err := rows.Scan(
&i.Chat.ID,
&i.Chat.OwnerID,
&i.Chat.WorkspaceID,
&i.Chat.Title,
&i.Chat.Status,
&i.Chat.WorkerID,
&i.Chat.StartedAt,
&i.Chat.HeartbeatAt,
&i.Chat.CreatedAt,
&i.Chat.UpdatedAt,
&i.Chat.ParentChatID,
&i.Chat.RootChatID,
&i.Chat.LastModelConfigID,
&i.Chat.LastReasoningEffort,
&i.Chat.Archived,
&i.Chat.LastError,
&i.Chat.Mode,
pq.Array(&i.Chat.MCPServerIDs),
&i.Chat.Labels,
&i.Chat.BuildID,
&i.Chat.AgentID,
&i.Chat.PinOrder,
&i.Chat.LastReadMessageID,
&i.Chat.DynamicTools,
&i.Chat.OrganizationID,
&i.Chat.PlanMode,
&i.Chat.ClientType,
&i.Chat.LastTurnSummary,
&i.Chat.Summary,
&i.Chat.SummaryGeneratedAt,
&i.Chat.SnapshotVersion,
&i.Chat.HistoryVersion,
&i.Chat.QueueVersion,
&i.Chat.GenerationAttempt,
&i.Chat.RetryState,
&i.Chat.RetryStateVersion,
&i.Chat.RunnerID,
&i.Chat.RequiresActionDeadlineAt,
&i.Chat.UserACL,
&i.Chat.GroupACL,
&i.Chat.OwnerUsername,
&i.Chat.OwnerName,
&i.Chat.ContextAggregateHash,
&i.Chat.ContextDirtySince,
&i.Chat.ContextDirtyResources,
&i.Chat.ContextError,
&i.Chat.CompactionRequestedAt,
&i.HasUnread,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getDatabaseNow = `-- name: GetDatabaseNow :one
SELECT NOW()::timestamptz AS now
`
// Returns the current database timestamp. Used so transitions that
// record deadlines or heartbeats rely on a clock that is consistent
// with the database rather than the caller's local clock.
func (q *sqlQuerier) GetDatabaseNow(ctx context.Context) (time.Time, error) {
row := q.db.QueryRowContext(ctx, getDatabaseNow)
var now time.Time
err := row.Scan(&now)
return now, err
}
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, deleted, provider_response_id, revision, reasoning_effort, search_tsv
FROM
chat_messages
WHERE
chat_id = $1::uuid
AND role = $2::chat_message_role
AND deleted = false
ORDER BY
id DESC
LIMIT
1
`
type GetLastChatMessageByRoleParams struct {
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
Role ChatMessageRole `db:"role" json:"role"`
}
// The returned id becomes both an AfterID cursor and last_read_message_id, so
// "last" must use id order.
func (q *sqlQuerier) GetLastChatMessageByRole(ctx context.Context, arg GetLastChatMessageByRoleParams) (ChatMessage, error) {
row := q.db.QueryRowContext(ctx, getLastChatMessageByRole, arg.ChatID, arg.Role)
var i ChatMessage
err := row.Scan(
&i.ID,
&i.ChatID,
&i.ModelConfigID,
&i.CreatedAt,
&i.Role,
&i.Content,
&i.Visibility,
&i.InputTokens,
&i.OutputTokens,
&i.TotalTokens,
&i.ReasoningTokens,
&i.CacheCreationTokens,
&i.CacheReadTokens,
&i.ContextLimit,
&i.Compressed,
&i.CreatedBy,
&i.ContentVersion,
&i.TotalCostMicros,
&i.RuntimeMs,
&i.Deleted,
&i.ProviderResponseID,
&i.Revision,
&i.ReasoningEffort,
&i.SearchTsv,
)
return i, err
}
const getStaleChats = `-- name: GetStaleChats :many
SELECT
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, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM
chats_expanded
WHERE
(status = 'running'::chat_status
AND heartbeat_at < $1::timestamptz)
OR (status = 'requires_action'::chat_status
AND updated_at < $1::timestamptz)
OR (status = 'waiting'::chat_status
AND updated_at < $1::timestamptz
AND EXISTS (
SELECT 1 FROM chat_queued_messages cqm
WHERE cqm.chat_id = chats_expanded.id
))
`
// Find chats that appear stuck and need recovery:
// 1. Running chats whose heartbeat has expired (worker crash).
// 2. requires_action chats past the timeout threshold (client
// disappeared).
// 3. Waiting chats with a non-empty queue and stale updated_at
// (deferred-promote stranding when the worker dies before its
// post-cancel cleanup runs).
func (q *sqlQuerier) GetStaleChats(ctx context.Context, staleThreshold time.Time) ([]Chat, error) {
rows, err := q.db.QueryContext(ctx, getStaleChats, staleThreshold)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Chat
for rows.Next() {
var i Chat
if err := rows.Scan(
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.Title,
&i.Status,
&i.WorkerID,
&i.StartedAt,
&i.HeartbeatAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ParentChatID,
&i.RootChatID,
&i.LastModelConfigID,
&i.LastReasoningEffort,
&i.Archived,
&i.LastError,
&i.Mode,
pq.Array(&i.MCPServerIDs),
&i.Labels,
&i.BuildID,
&i.AgentID,
&i.PinOrder,
&i.LastReadMessageID,
&i.DynamicTools,
&i.OrganizationID,
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
&i.Summary,
&i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
&i.GenerationAttempt,
&i.RetryState,
&i.RetryStateVersion,
&i.RunnerID,
&i.RequiresActionDeadlineAt,
&i.UserACL,
&i.GroupACL,
&i.OwnerUsername,
&i.OwnerName,
&i.ContextAggregateHash,
&i.ContextDirtySince,
&i.ContextDirtyResources,
&i.ContextError,
&i.CompactionRequestedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getTotalChatMessageRuntimeMsInRange = `-- name: GetTotalChatMessageRuntimeMsInRange :one
SELECT COALESCE(SUM(cm.runtime_ms), 0)::bigint AS total_runtime_ms
FROM chat_messages cm
WHERE cm.created_at >= $1::timestamptz
AND cm.created_at < $2::timestamptz
AND cm.runtime_ms IS NOT NULL
`
type GetTotalChatMessageRuntimeMsInRangeParams struct {
StartTime time.Time `db:"start_time" json:"start_time"`
EndTime time.Time `db:"end_time" json:"end_time"`
}
// Computes hb_agent_runtime_v1 usage event payloads. Deliberately includes
// soft-deleted messages and messages from all chats.
func (q *sqlQuerier) GetTotalChatMessageRuntimeMsInRange(ctx context.Context, arg GetTotalChatMessageRuntimeMsInRangeParams) (int64, error) {
row := q.db.QueryRowContext(ctx, getTotalChatMessageRuntimeMsInRange, arg.StartTime, arg.EndTime)
var total_runtime_ms int64
err := row.Scan(&total_runtime_ms)
return total_runtime_ms, err
}
const hydrateAgentChatsContext = `-- name: HydrateAgentChatsContext :many
WITH hydrated AS (
UPDATE chats
SET
context_aggregate_hash = $1,
context_error = $2
WHERE agent_id = $3::uuid
AND archived = false
AND context_aggregate_hash IS NULL
RETURNING id
),
copied AS (
INSERT INTO chat_context_resources (
chat_id, source, body_kind, body, content_hash, size_bytes, status, error, source_path
)
SELECT
hydrated.id, r.source, r.body_kind, r.body, r.content_hash,
r.size_bytes, r.status, r.error, r.source_path
FROM hydrated
CROSS JOIN workspace_agent_context_resources r
WHERE r.workspace_agent_id = $3::uuid
ON CONFLICT (chat_id, source) DO UPDATE SET
body_kind = EXCLUDED.body_kind,
body = EXCLUDED.body,
content_hash = EXCLUDED.content_hash,
size_bytes = EXCLUDED.size_bytes,
status = EXCLUDED.status,
error = EXCLUDED.error,
source_path = EXCLUDED.source_path,
updated_at = now()
)
SELECT id FROM hydrated
`
type HydrateAgentChatsContextParams struct {
AggregateHash []byte `db:"aggregate_hash" json:"aggregate_hash"`
ContextError string `db:"context_error" json:"context_error"`
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
}
// Stamps the pinned hash and error on every not-yet-hydrated chat for
// an agent (context_aggregate_hash IS NULL) and copies the agent's
// current context resources onto those chats in the same statement, so
// a chat's pinned hash and pinned bodies are always written together.
// Runs as a side effect of an agent push and of chat-create hydration,
// so chats created before the agent was ready pick up the snapshot
// without a dirty marker. The ON CONFLICT upsert is defensive: a
// not-yet-hydrated chat has no pinned rows, so it normally inserts.
// Does not bump chats.updated_at; the resource upsert's ON CONFLICT branch
// sets chat_context_resources.updated_at on the rows it rewrites.
// Returns the hydrated chat IDs so callers can notify watchers of every
// chat the statement pinned.
func (q *sqlQuerier) HydrateAgentChatsContext(ctx context.Context, arg HydrateAgentChatsContextParams) ([]uuid.UUID, error) {
rows, err := q.db.QueryContext(ctx, hydrateAgentChatsContext, arg.AggregateHash, arg.ContextError, arg.AgentID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []uuid.UUID
for rows.Next() {
var id uuid.UUID
if err := rows.Scan(&id); err != nil {
return nil, err
}
items = append(items, id)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const incrementChatGenerationAttempt = `-- name: IncrementChatGenerationAttempt :one
UPDATE chats
SET generation_attempt = generation_attempt + 1, updated_at = NOW()
WHERE id = $1::uuid
RETURNING generation_attempt
`
// Increments generation_attempt and returns the resulting value.
func (q *sqlQuerier) IncrementChatGenerationAttempt(ctx context.Context, id uuid.UUID) (int64, error) {
row := q.db.QueryRowContext(ctx, incrementChatGenerationAttempt, id)
var generation_attempt int64
err := row.Scan(&generation_attempt)
return generation_attempt, err
}
const insertAgentContextResourcesIntoChat = `-- name: InsertAgentContextResourcesIntoChat :exec
INSERT INTO chat_context_resources (
chat_id, source, body_kind, body, content_hash, size_bytes, status, error, source_path
)
SELECT
$1::uuid, r.source, r.body_kind, r.body, r.content_hash,
r.size_bytes, r.status, r.error, r.source_path
FROM workspace_agent_context_resources r
WHERE r.workspace_agent_id = $2::uuid
`
type InsertAgentContextResourcesIntoChatParams struct {
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
}
// Copies an agent's current context resources onto a single chat. Pair
// with DeleteChatContextResourcesByChatID (clear-then-copy, in a
// transaction) to re-pin a chat to its agent's latest snapshot from the
// refresh endpoint and on agent rebinding.
func (q *sqlQuerier) InsertAgentContextResourcesIntoChat(ctx context.Context, arg InsertAgentContextResourcesIntoChatParams) error {
_, err := q.db.ExecContext(ctx, insertAgentContextResourcesIntoChat, arg.ChatID, arg.AgentID)
return err
}
const insertChat = `-- name: InsertChat :one
WITH inserted_chat AS (
INSERT INTO chats (
id,
organization_id,
owner_id,
workspace_id,
build_id,
agent_id,
parent_chat_id,
root_chat_id,
last_model_config_id,
title,
mode,
plan_mode,
status,
mcp_server_ids,
labels,
dynamic_tools,
client_type
) VALUES (
COALESCE($1::uuid, gen_random_uuid()),
$2::uuid,
$3::uuid,
$4::uuid,
$5::uuid,
$6::uuid,
$7::uuid,
$8::uuid,
$9::uuid,
$10::text,
$11::chat_mode,
$12::chat_plan_mode,
$13::chat_status,
COALESCE($14::uuid[], '{}'::uuid[]),
COALESCE($15::jsonb, '{}'::jsonb),
$16::jsonb,
$17::chat_client_type
)
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, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
),
chats_expanded AS (
SELECT
inserted_chat.id,
inserted_chat.owner_id,
inserted_chat.workspace_id,
inserted_chat.title,
inserted_chat.status,
inserted_chat.worker_id,
inserted_chat.started_at,
inserted_chat.heartbeat_at,
inserted_chat.created_at,
inserted_chat.updated_at,
inserted_chat.parent_chat_id,
inserted_chat.root_chat_id,
inserted_chat.last_model_config_id,
inserted_chat.last_reasoning_effort,
inserted_chat.archived,
inserted_chat.last_error,
inserted_chat.mode,
inserted_chat.mcp_server_ids,
inserted_chat.labels,
inserted_chat.build_id,
inserted_chat.agent_id,
inserted_chat.pin_order,
inserted_chat.last_read_message_id,
inserted_chat.dynamic_tools,
inserted_chat.organization_id,
inserted_chat.plan_mode,
inserted_chat.client_type,
inserted_chat.last_turn_summary,
inserted_chat.summary,
inserted_chat.summary_generated_at,
inserted_chat.snapshot_version,
inserted_chat.history_version,
inserted_chat.queue_version,
inserted_chat.generation_attempt,
inserted_chat.retry_state,
inserted_chat.retry_state_version,
inserted_chat.runner_id,
inserted_chat.requires_action_deadline_at,
COALESCE(root.user_acl, inserted_chat.user_acl) AS user_acl,
COALESCE(root.group_acl, inserted_chat.group_acl) AS group_acl,
owner.username AS owner_username,
owner.name AS owner_name,
inserted_chat.context_aggregate_hash,
inserted_chat.context_dirty_since,
inserted_chat.context_dirty_resources,
inserted_chat.context_error,
inserted_chat.compaction_requested_at
FROM
inserted_chat
LEFT JOIN chats root ON root.id = COALESCE(inserted_chat.root_chat_id, inserted_chat.parent_chat_id)
JOIN visible_users owner ON owner.id = inserted_chat.owner_id
)
SELECT 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, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
`
type InsertChatParams struct {
ID uuid.NullUUID `db:"id" json:"id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"`
BuildID uuid.NullUUID `db:"build_id" json:"build_id"`
AgentID uuid.NullUUID `db:"agent_id" json:"agent_id"`
ParentChatID uuid.NullUUID `db:"parent_chat_id" json:"parent_chat_id"`
RootChatID uuid.NullUUID `db:"root_chat_id" json:"root_chat_id"`
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"`
PlanMode NullChatPlanMode `db:"plan_mode" json:"plan_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"`
DynamicTools pqtype.NullRawMessage `db:"dynamic_tools" json:"dynamic_tools"`
ClientType ChatClientType `db:"client_type" json:"client_type"`
}
func (q *sqlQuerier) InsertChat(ctx context.Context, arg InsertChatParams) (Chat, error) {
row := q.db.QueryRowContext(ctx, insertChat,
arg.ID,
arg.OrganizationID,
arg.OwnerID,
arg.WorkspaceID,
arg.BuildID,
arg.AgentID,
arg.ParentChatID,
arg.RootChatID,
arg.LastModelConfigID,
arg.Title,
arg.Mode,
arg.PlanMode,
arg.Status,
pq.Array(arg.MCPServerIDs),
arg.Labels,
arg.DynamicTools,
arg.ClientType,
)
var i Chat
err := row.Scan(
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.Title,
&i.Status,
&i.WorkerID,
&i.StartedAt,
&i.HeartbeatAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ParentChatID,
&i.RootChatID,
&i.LastModelConfigID,
&i.LastReasoningEffort,
&i.Archived,
&i.LastError,
&i.Mode,
pq.Array(&i.MCPServerIDs),
&i.Labels,
&i.BuildID,
&i.AgentID,
&i.PinOrder,
&i.LastReadMessageID,
&i.DynamicTools,
&i.OrganizationID,
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
&i.Summary,
&i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
&i.GenerationAttempt,
&i.RetryState,
&i.RetryStateVersion,
&i.RunnerID,
&i.RequiresActionDeadlineAt,
&i.UserACL,
&i.GroupACL,
&i.OwnerUsername,
&i.OwnerName,
&i.ContextAggregateHash,
&i.ContextDirtySince,
&i.ContextDirtyResources,
&i.ContextError,
&i.CompactionRequestedAt,
)
return i, err
}
const insertChatMessages = `-- name: InsertChatMessages :many
WITH batch AS (
SELECT
(
SELECT val
FROM UNNEST($1::uuid[])
WITH ORDINALITY AS t(val, ord)
WHERE val != '00000000-0000-0000-0000-000000000000'::uuid
ORDER BY ord DESC
LIMIT 1
) AS last_model_config_id,
(
SELECT NULLIF(val, '')::chat_reasoning_effort
FROM UNNEST($2::text[])
WITH ORDINALITY AS t(val, ord)
WHERE val != ''
ORDER BY ord DESC
LIMIT 1
) AS last_reasoning_effort
),
updated_chat AS (
UPDATE
chats
SET
last_model_config_id = COALESCE(batch.last_model_config_id, chats.last_model_config_id),
last_reasoning_effort = COALESCE(batch.last_reasoning_effort, chats.last_reasoning_effort)
FROM batch
WHERE
chats.id = $3::uuid
AND (
chats.last_model_config_id IS DISTINCT FROM COALESCE(batch.last_model_config_id, chats.last_model_config_id)
OR chats.last_reasoning_effort IS DISTINCT FROM COALESCE(batch.last_reasoning_effort, chats.last_reasoning_effort)
)
),
allocated AS MATERIALIZED (
-- Numbering the ids by value, rather than by the order nextval produced
-- them, is what makes ordinal k always the k-th smallest id. MATERIALIZED
-- is redundant while nextval is volatile, and pins that if it changes.
SELECT
id,
(ROW_NUMBER() OVER (ORDER BY id))::int AS ord
FROM (
SELECT nextval('chat_messages_id_seq') AS id
FROM generate_series(1, cardinality($4::chat_message_role[]))
) s
),
inserted AS (
INSERT INTO chat_messages (
id,
chat_id,
created_by,
model_config_id,
reasoning_effort,
role,
content,
content_version,
visibility,
input_tokens,
output_tokens,
total_tokens,
reasoning_tokens,
cache_creation_tokens,
cache_read_tokens,
context_limit,
compressed,
runtime_ms
)
SELECT
allocated.id,
$3::uuid,
NULLIF(($5::uuid[])[allocated.ord], '00000000-0000-0000-0000-000000000000'::uuid),
NULLIF(($1::uuid[])[allocated.ord], '00000000-0000-0000-0000-000000000000'::uuid),
NULLIF(($2::text[])[allocated.ord], '')::chat_reasoning_effort,
($4::chat_message_role[])[allocated.ord],
($6::text[])[allocated.ord]::jsonb,
($7::smallint[])[allocated.ord],
($8::chat_message_visibility[])[allocated.ord],
NULLIF(($9::bigint[])[allocated.ord], 0),
NULLIF(($10::bigint[])[allocated.ord], 0),
NULLIF(($11::bigint[])[allocated.ord], 0),
NULLIF(($12::bigint[])[allocated.ord], 0),
NULLIF(($13::bigint[])[allocated.ord], 0),
NULLIF(($14::bigint[])[allocated.ord], 0),
NULLIF(($15::bigint[])[allocated.ord], 0),
($16::boolean[])[allocated.ord],
NULLIF(($17::bigint[])[allocated.ord], 0)
FROM allocated
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, deleted, provider_response_id, revision, reasoning_effort, search_tsv
)
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, deleted, provider_response_id, revision, reasoning_effort, search_tsv
FROM inserted
ORDER BY id
`
type InsertChatMessagesParams struct {
ModelConfigID []uuid.UUID `db:"model_config_id" json:"model_config_id"`
ReasoningEffort []string `db:"reasoning_effort" json:"reasoning_effort"`
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
Role []ChatMessageRole `db:"role" json:"role"`
CreatedBy []uuid.UUID `db:"created_by" json:"created_by"`
Content []string `db:"content" json:"content"`
ContentVersion []int16 `db:"content_version" json:"content_version"`
Visibility []ChatMessageVisibility `db:"visibility" json:"visibility"`
InputTokens []int64 `db:"input_tokens" json:"input_tokens"`
OutputTokens []int64 `db:"output_tokens" json:"output_tokens"`
TotalTokens []int64 `db:"total_tokens" json:"total_tokens"`
ReasoningTokens []int64 `db:"reasoning_tokens" json:"reasoning_tokens"`
CacheCreationTokens []int64 `db:"cache_creation_tokens" json:"cache_creation_tokens"`
CacheReadTokens []int64 `db:"cache_read_tokens" json:"cache_read_tokens"`
ContextLimit []int64 `db:"context_limit" json:"context_limit"`
Compressed []bool `db:"compressed" json:"compressed"`
RuntimeMs []int64 `db:"runtime_ms" json:"runtime_ms"`
}
type InsertChatMessagesRow struct {
ID int64 `db:"id" json:"id"`
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
ModelConfigID uuid.NullUUID `db:"model_config_id" json:"model_config_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
Role ChatMessageRole `db:"role" json:"role"`
Content pqtype.NullRawMessage `db:"content" json:"content"`
Visibility ChatMessageVisibility `db:"visibility" json:"visibility"`
InputTokens sql.NullInt64 `db:"input_tokens" json:"input_tokens"`
OutputTokens sql.NullInt64 `db:"output_tokens" json:"output_tokens"`
TotalTokens sql.NullInt64 `db:"total_tokens" json:"total_tokens"`
ReasoningTokens sql.NullInt64 `db:"reasoning_tokens" json:"reasoning_tokens"`
CacheCreationTokens sql.NullInt64 `db:"cache_creation_tokens" json:"cache_creation_tokens"`
CacheReadTokens sql.NullInt64 `db:"cache_read_tokens" json:"cache_read_tokens"`
ContextLimit sql.NullInt64 `db:"context_limit" json:"context_limit"`
Compressed bool `db:"compressed" json:"compressed"`
CreatedBy uuid.NullUUID `db:"created_by" json:"created_by"`
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"`
ProviderResponseID sql.NullString `db:"provider_response_id" json:"provider_response_id"`
Revision int64 `db:"revision" json:"revision"`
ReasoningEffort NullChatReasoningEffort `db:"reasoning_effort" json:"reasoning_effort"`
SearchTsv interface{} `db:"search_tsv" json:"search_tsv"`
}
// Returns the inserted rows in input array order. Ids are allocated before the
// insert and the k-th smallest is assigned to input index k, so callers may
// index the result positionally.
func (q *sqlQuerier) InsertChatMessages(ctx context.Context, arg InsertChatMessagesParams) ([]InsertChatMessagesRow, error) {
rows, err := q.db.QueryContext(ctx, insertChatMessages,
pq.Array(arg.ModelConfigID),
pq.Array(arg.ReasoningEffort),
arg.ChatID,
pq.Array(arg.Role),
pq.Array(arg.CreatedBy),
pq.Array(arg.Content),
pq.Array(arg.ContentVersion),
pq.Array(arg.Visibility),
pq.Array(arg.InputTokens),
pq.Array(arg.OutputTokens),
pq.Array(arg.TotalTokens),
pq.Array(arg.ReasoningTokens),
pq.Array(arg.CacheCreationTokens),
pq.Array(arg.CacheReadTokens),
pq.Array(arg.ContextLimit),
pq.Array(arg.Compressed),
pq.Array(arg.RuntimeMs),
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []InsertChatMessagesRow
for rows.Next() {
var i InsertChatMessagesRow
if err := rows.Scan(
&i.ID,
&i.ChatID,
&i.ModelConfigID,
&i.CreatedAt,
&i.Role,
&i.Content,
&i.Visibility,
&i.InputTokens,
&i.OutputTokens,
&i.TotalTokens,
&i.ReasoningTokens,
&i.CacheCreationTokens,
&i.CacheReadTokens,
&i.ContextLimit,
&i.Compressed,
&i.CreatedBy,
&i.ContentVersion,
&i.TotalCostMicros,
&i.RuntimeMs,
&i.Deleted,
&i.ProviderResponseID,
&i.Revision,
&i.ReasoningEffort,
&i.SearchTsv,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertChatQueuedMessage = `-- name: InsertChatQueuedMessage :one
INSERT INTO chat_queued_messages (chat_id, content, model_config_id, reasoning_effort, created_by)
SELECT
$1::uuid,
$2::jsonb,
$3::uuid,
$4::chat_reasoning_effort,
chats.owner_id
FROM chats
WHERE chats.id = $1::uuid
RETURNING id, chat_id, content, created_at, model_config_id, position, created_by, reasoning_effort
`
type InsertChatQueuedMessageParams struct {
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
Content json.RawMessage `db:"content" json:"content"`
ModelConfigID uuid.NullUUID `db:"model_config_id" json:"model_config_id"`
ReasoningEffort NullChatReasoningEffort `db:"reasoning_effort" json:"reasoning_effort"`
}
// Legacy queue insertion path. When no caller-supplied creator exists,
// preserve the created_by invariant by attributing the queued row to the
// chat owner.
func (q *sqlQuerier) InsertChatQueuedMessage(ctx context.Context, arg InsertChatQueuedMessageParams) (ChatQueuedMessage, error) {
row := q.db.QueryRowContext(ctx, insertChatQueuedMessage,
arg.ChatID,
arg.Content,
arg.ModelConfigID,
arg.ReasoningEffort,
)
var i ChatQueuedMessage
err := row.Scan(
&i.ID,
&i.ChatID,
&i.Content,
&i.CreatedAt,
&i.ModelConfigID,
&i.Position,
&i.CreatedBy,
&i.ReasoningEffort,
)
return i, err
}
const insertChatQueuedMessageWithCreator = `-- name: InsertChatQueuedMessageWithCreator :one
INSERT INTO chat_queued_messages (chat_id, content, model_config_id, reasoning_effort, created_by)
VALUES (
$1::uuid,
$2::jsonb,
$3::uuid,
$4::chat_reasoning_effort,
$5::uuid
)
RETURNING id, chat_id, content, created_at, model_config_id, position, created_by, reasoning_effort
`
type InsertChatQueuedMessageWithCreatorParams struct {
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
Content json.RawMessage `db:"content" json:"content"`
ModelConfigID uuid.NullUUID `db:"model_config_id" json:"model_config_id"`
ReasoningEffort NullChatReasoningEffort `db:"reasoning_effort" json:"reasoning_effort"`
CreatedBy uuid.UUID `db:"created_by" json:"created_by"`
}
// Inserts a queued message that carries a position (from the default
// sequence) and an explicit created_by reference. Use this when the
// queued-message creator differs from the chat owner.
func (q *sqlQuerier) InsertChatQueuedMessageWithCreator(ctx context.Context, arg InsertChatQueuedMessageWithCreatorParams) (ChatQueuedMessage, error) {
row := q.db.QueryRowContext(ctx, insertChatQueuedMessageWithCreator,
arg.ChatID,
arg.Content,
arg.ModelConfigID,
arg.ReasoningEffort,
arg.CreatedBy,
)
var i ChatQueuedMessage
err := row.Scan(
&i.ID,
&i.ChatID,
&i.Content,
&i.CreatedAt,
&i.ModelConfigID,
&i.Position,
&i.CreatedBy,
&i.ReasoningEffort,
)
return i, err
}
const isChatHeartbeatStale = `-- name: IsChatHeartbeatStale :one
SELECT NOT EXISTS (
SELECT 1 FROM chat_heartbeats
WHERE chat_id = $1::uuid
AND runner_id = $2::uuid
AND heartbeat_at > NOW() - (INTERVAL '1 second' * $3::int)
) AS stale
`
type IsChatHeartbeatStaleParams struct {
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
RunnerID uuid.UUID `db:"runner_id" json:"runner_id"`
StaleSeconds int32 `db:"stale_seconds" json:"stale_seconds"`
}
// Returns true when there is no heartbeat row for (chat_id, runner_id)
// or the existing row is older than @stale_seconds seconds by database
// time. chatstate calls this in a single query so the staleness check
// is atomic and does not depend on the caller's local clock.
func (q *sqlQuerier) IsChatHeartbeatStale(ctx context.Context, arg IsChatHeartbeatStaleParams) (bool, error) {
row := q.db.QueryRowContext(ctx, isChatHeartbeatStale, arg.ChatID, arg.RunnerID, arg.StaleSeconds)
var stale bool
err := row.Scan(&stale)
return stale, err
}
const linkChatFilesAfterLock = `-- name: LinkChatFilesAfterLock :one
WITH current AS (
SELECT COUNT(*) AS cnt
FROM chat_file_links
WHERE chat_id = $1::uuid
),
new_links AS (
SELECT DISTINCT $1::uuid AS chat_id, unnest($2::uuid[]) AS file_id
),
genuinely_new AS (
SELECT nl.chat_id, nl.file_id
FROM new_links nl
WHERE NOT EXISTS (
SELECT 1 FROM chat_file_links cfl
WHERE cfl.chat_id = nl.chat_id AND cfl.file_id = nl.file_id
)
),
inserted AS (
INSERT INTO chat_file_links (chat_id, file_id)
SELECT gn.chat_id, gn.file_id
FROM genuinely_new gn, current c
WHERE c.cnt + (SELECT COUNT(*) FROM genuinely_new) <= $3::int
ON CONFLICT (chat_id, file_id) DO NOTHING
RETURNING file_id
)
SELECT
(SELECT COUNT(*)::int FROM genuinely_new) -
(SELECT COUNT(*)::int FROM inserted) AS rejected_new_files
`
type LinkChatFilesAfterLockParams struct {
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
FileIds []uuid.UUID `db:"file_ids" json:"file_ids"`
MaxFileLinks int32 `db:"max_file_links" json:"max_file_links"`
}
// LinkChatFilesAfterLock requires the chat row lock.
// The lock serializes cap checks. The result counts rejected new links.
func (q *sqlQuerier) LinkChatFilesAfterLock(ctx context.Context, arg LinkChatFilesAfterLockParams) (int32, error) {
row := q.db.QueryRowContext(ctx, linkChatFilesAfterLock, arg.ChatID, pq.Array(arg.FileIds), arg.MaxFileLinks)
var rejected_new_files int32
err := row.Scan(&rejected_new_files)
return rejected_new_files, err
}
const listChatContextResourcesByChatID = `-- name: ListChatContextResourcesByChatID :many
SELECT chat_id, source, body_kind, body, content_hash, size_bytes, status, error, source_path, created_at, updated_at FROM chat_context_resources
WHERE chat_id = $1::uuid
ORDER BY source ASC
`
// Lists a chat's pinned context resources, ordered deterministically by
// source.
func (q *sqlQuerier) ListChatContextResourcesByChatID(ctx context.Context, chatID uuid.UUID) ([]ChatContextResource, error) {
rows, err := q.db.QueryContext(ctx, listChatContextResourcesByChatID, chatID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ChatContextResource
for rows.Next() {
var i ChatContextResource
if err := rows.Scan(
&i.ChatID,
&i.Source,
&i.BodyKind,
&i.Body,
&i.ContentHash,
&i.SizeBytes,
&i.Status,
&i.Error,
&i.SourcePath,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const lockChatAndBumpSnapshotVersion = `-- name: LockChatAndBumpSnapshotVersion :one
WITH bumped_chat AS (
UPDATE chats
SET snapshot_version = snapshot_version + 1
WHERE id = (
SELECT id FROM chats
WHERE id = $1::uuid
FOR UPDATE
)
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, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
),
chats_expanded AS (
SELECT
bumped_chat.id,
bumped_chat.owner_id,
bumped_chat.workspace_id,
bumped_chat.title,
bumped_chat.status,
bumped_chat.worker_id,
bumped_chat.started_at,
bumped_chat.heartbeat_at,
bumped_chat.created_at,
bumped_chat.updated_at,
bumped_chat.parent_chat_id,
bumped_chat.root_chat_id,
bumped_chat.last_model_config_id,
bumped_chat.last_reasoning_effort,
bumped_chat.archived,
bumped_chat.last_error,
bumped_chat.mode,
bumped_chat.mcp_server_ids,
bumped_chat.labels,
bumped_chat.build_id,
bumped_chat.agent_id,
bumped_chat.pin_order,
bumped_chat.last_read_message_id,
bumped_chat.dynamic_tools,
bumped_chat.organization_id,
bumped_chat.plan_mode,
bumped_chat.client_type,
bumped_chat.last_turn_summary,
bumped_chat.summary,
bumped_chat.summary_generated_at,
bumped_chat.snapshot_version,
bumped_chat.history_version,
bumped_chat.queue_version,
bumped_chat.generation_attempt,
bumped_chat.retry_state,
bumped_chat.retry_state_version,
bumped_chat.runner_id,
bumped_chat.requires_action_deadline_at,
COALESCE(root.user_acl, bumped_chat.user_acl) AS user_acl,
COALESCE(root.group_acl, bumped_chat.group_acl) AS group_acl,
owner.username AS owner_username,
owner.name AS owner_name,
bumped_chat.context_aggregate_hash,
bumped_chat.context_dirty_since,
bumped_chat.context_dirty_resources,
bumped_chat.context_error,
bumped_chat.compaction_requested_at
FROM bumped_chat
LEFT JOIN chats root ON root.id = COALESCE(bumped_chat.root_chat_id, bumped_chat.parent_chat_id)
JOIN visible_users owner ON owner.id = bumped_chat.owner_id
)
SELECT 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, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
`
// Locks the chat row with FOR UPDATE and atomically increments its
// snapshot_version, returning the post-bump chat. This is the single
// entry point ChatMachine.Update uses to acquire the row lock and
// allocate a new snapshot version in one round trip.
func (q *sqlQuerier) LockChatAndBumpSnapshotVersion(ctx context.Context, id uuid.UUID) (Chat, error) {
row := q.db.QueryRowContext(ctx, lockChatAndBumpSnapshotVersion, id)
var i Chat
err := row.Scan(
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.Title,
&i.Status,
&i.WorkerID,
&i.StartedAt,
&i.HeartbeatAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ParentChatID,
&i.RootChatID,
&i.LastModelConfigID,
&i.LastReasoningEffort,
&i.Archived,
&i.LastError,
&i.Mode,
pq.Array(&i.MCPServerIDs),
&i.Labels,
&i.BuildID,
&i.AgentID,
&i.PinOrder,
&i.LastReadMessageID,
&i.DynamicTools,
&i.OrganizationID,
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
&i.Summary,
&i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
&i.GenerationAttempt,
&i.RetryState,
&i.RetryStateVersion,
&i.RunnerID,
&i.RequiresActionDeadlineAt,
&i.UserACL,
&i.GroupACL,
&i.OwnerUsername,
&i.OwnerName,
&i.ContextAggregateHash,
&i.ContextDirtySince,
&i.ContextDirtyResources,
&i.ContextError,
&i.CompactionRequestedAt,
)
return i, err
}
const lockChatByID = `-- name: LockChatByID :one
SELECT id
FROM chats
WHERE id = $1::uuid
FOR UPDATE
`
func (q *sqlQuerier) LockChatByID(ctx context.Context, id uuid.UUID) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, lockChatByID, id)
var id_2 uuid.UUID
err := row.Scan(&id_2)
return id_2, err
}
const markChatsContextDirtyByAgent = `-- name: MarkChatsContextDirtyByAgent :many
UPDATE chats
SET context_dirty_since = $1
WHERE agent_id = $2::uuid
AND archived = false
AND status IN ('waiting', 'running', 'requires_action')
AND context_aggregate_hash IS NOT NULL
AND context_aggregate_hash IS DISTINCT FROM $3
AND context_dirty_since IS NULL
RETURNING id, owner_id
`
type MarkChatsContextDirtyByAgentParams struct {
DirtySince sql.NullTime `db:"dirty_since" json:"dirty_since"`
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
AggregateHash []byte `db:"aggregate_hash" json:"aggregate_hash"`
}
type MarkChatsContextDirtyByAgentRow struct {
ID uuid.UUID `db:"id" json:"id"`
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
}
// Flips active, already-hydrated chats for an agent to dirty when the
// agent's latest snapshot hash differs from the chat's pinned hash. The
// pinned hash is intentionally left untouched; the refresh endpoint
// re-pins it. Returns the chats that transitioned so the caller can
// emit watch events after the transaction commits.
func (q *sqlQuerier) MarkChatsContextDirtyByAgent(ctx context.Context, arg MarkChatsContextDirtyByAgentParams) ([]MarkChatsContextDirtyByAgentRow, error) {
rows, err := q.db.QueryContext(ctx, markChatsContextDirtyByAgent, arg.DirtySince, arg.AgentID, arg.AggregateHash)
if err != nil {
return nil, err
}
defer rows.Close()
var items []MarkChatsContextDirtyByAgentRow
for rows.Next() {
var i MarkChatsContextDirtyByAgentRow
if err := rows.Scan(&i.ID, &i.OwnerID); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const pinChatByID = `-- name: PinChatByID :exec
WITH target_chat AS (
SELECT
id,
owner_id
FROM
chats
WHERE
id = $1::uuid
),
ranked AS (
SELECT
c.id,
ROW_NUMBER() OVER (ORDER BY c.pin_order ASC, c.id ASC) :: integer AS next_pin_order
FROM
chats c
JOIN
target_chat ON c.owner_id = target_chat.owner_id
WHERE
c.pin_order > 0
AND c.archived = FALSE
AND c.id <> target_chat.id
),
updates AS (
SELECT
ranked.id,
ranked.next_pin_order AS pin_order
FROM
ranked
UNION ALL
SELECT
target_chat.id,
COALESCE((
SELECT
MAX(ranked.next_pin_order)
FROM
ranked
), 0) + 1 AS pin_order
FROM
target_chat
)
UPDATE
chats c
SET
pin_order = updates.pin_order
FROM
updates
WHERE
c.id = updates.id
`
// Under READ COMMITTED, concurrent pin operations for the same
// owner may momentarily produce duplicate pin_order values because
// each CTE snapshot does not see the other's writes. The next
// pin/unpin/reorder operation's ROW_NUMBER() self-heals the
// sequence, so this is acceptable.
func (q *sqlQuerier) PinChatByID(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, pinChatByID, id)
return err
}
const popNextQueuedMessage = `-- name: PopNextQueuedMessage :one
DELETE FROM chat_queued_messages
WHERE id = (
SELECT cqm.id FROM chat_queued_messages cqm
WHERE cqm.chat_id = $1
ORDER BY cqm.created_at ASC, cqm.id ASC
LIMIT 1
)
RETURNING id, chat_id, content, created_at, model_config_id, position, created_by, reasoning_effort
`
func (q *sqlQuerier) PopNextQueuedMessage(ctx context.Context, chatID uuid.UUID) (ChatQueuedMessage, error) {
row := q.db.QueryRowContext(ctx, popNextQueuedMessage, chatID)
var i ChatQueuedMessage
err := row.Scan(
&i.ID,
&i.ChatID,
&i.Content,
&i.CreatedAt,
&i.ModelConfigID,
&i.Position,
&i.CreatedBy,
&i.ReasoningEffort,
)
return i, err
}
const reorderChatQueuedMessageToFront = `-- name: ReorderChatQueuedMessageToFront :execrows
UPDATE chat_queued_messages AS target
SET created_at = (
SELECT MIN(inner_cqm.created_at) - INTERVAL '1 microsecond'
FROM chat_queued_messages AS inner_cqm
WHERE inner_cqm.chat_id = $1
)
WHERE target.id = $2 AND target.chat_id = $1
`
type ReorderChatQueuedMessageToFrontParams struct {
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
TargetID int64 `db:"target_id" json:"target_id"`
}
// Mutates only created_at on the target row; ids are unchanged so
// consumers can keep tracking queued messages by id.
func (q *sqlQuerier) ReorderChatQueuedMessageToFront(ctx context.Context, arg ReorderChatQueuedMessageToFrontParams) (int64, error) {
result, err := q.db.ExecContext(ctx, reorderChatQueuedMessageToFront, arg.ChatID, arg.TargetID)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const reorderChatQueuedMessageToHead = `-- name: ReorderChatQueuedMessageToHead :execrows
UPDATE chat_queued_messages AS target
SET position = COALESCE(
(SELECT MIN(position) FROM chat_queued_messages WHERE chat_id = $1::uuid),
0
) - 1
WHERE target.id = $2::bigint
AND target.chat_id = $1::uuid
AND target.position > COALESCE(
(SELECT MIN(position) FROM chat_queued_messages WHERE chat_id = $1::uuid),
target.position
)
`
type ReorderChatQueuedMessageToHeadParams struct {
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
ID int64 `db:"id" json:"id"`
}
// Sets the target queued message's position to one less than the
// current minimum position for that chat, moving it to the head.
func (q *sqlQuerier) ReorderChatQueuedMessageToHead(ctx context.Context, arg ReorderChatQueuedMessageToHeadParams) (int64, error) {
result, err := q.db.ExecContext(ctx, reorderChatQueuedMessageToHead, arg.ChatID, arg.ID)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const setChatContextSnapshot = `-- name: SetChatContextSnapshot :exec
UPDATE chats
SET
context_aggregate_hash = $1,
context_error = $2,
context_dirty_since = NULL
WHERE id = $3::uuid
`
type SetChatContextSnapshotParams struct {
AggregateHash []byte `db:"aggregate_hash" json:"aggregate_hash"`
ContextError string `db:"context_error" json:"context_error"`
ID uuid.UUID `db:"id" json:"id"`
}
// Pins a single chat to the supplied context snapshot hash and error
// and clears any dirty marker. Used by chat-create hydration and the
// refresh endpoint. Does not bump updated_at: context pinning is
// background state and must not reorder chat lists.
func (q *sqlQuerier) SetChatContextSnapshot(ctx context.Context, arg SetChatContextSnapshotParams) error {
_, err := q.db.ExecContext(ctx, setChatContextSnapshot, arg.AggregateHash, arg.ContextError, arg.ID)
return 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 softDeleteContextFileMessages = `-- name: SoftDeleteContextFileMessages :exec
UPDATE chat_messages SET deleted = true
WHERE chat_id = $1::uuid
AND deleted = false
AND content::jsonb @> '[{"type": "context-file"}]'
`
func (q *sqlQuerier) SoftDeleteContextFileMessages(ctx context.Context, chatID uuid.UUID) error {
_, err := q.db.ExecContext(ctx, softDeleteContextFileMessages, chatID)
return err
}
const unarchiveChatByID = `-- name: UnarchiveChatByID :many
WITH updated_chats AS (
UPDATE chats SET
archived = false,
updated_at = NOW()
WHERE id = $1::uuid OR root_chat_id = $1::uuid
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, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
),
chats_expanded AS (
SELECT
updated_chats.id,
updated_chats.owner_id,
updated_chats.workspace_id,
updated_chats.title,
updated_chats.status,
updated_chats.worker_id,
updated_chats.started_at,
updated_chats.heartbeat_at,
updated_chats.created_at,
updated_chats.updated_at,
updated_chats.parent_chat_id,
updated_chats.root_chat_id,
updated_chats.last_model_config_id,
updated_chats.last_reasoning_effort,
updated_chats.archived,
updated_chats.last_error,
updated_chats.mode,
updated_chats.mcp_server_ids,
updated_chats.labels,
updated_chats.build_id,
updated_chats.agent_id,
updated_chats.pin_order,
updated_chats.last_read_message_id,
updated_chats.dynamic_tools,
updated_chats.organization_id,
updated_chats.plan_mode,
updated_chats.client_type,
updated_chats.last_turn_summary,
updated_chats.summary,
updated_chats.summary_generated_at,
updated_chats.snapshot_version,
updated_chats.history_version,
updated_chats.queue_version,
updated_chats.generation_attempt,
updated_chats.retry_state,
updated_chats.retry_state_version,
updated_chats.runner_id,
updated_chats.requires_action_deadline_at,
COALESCE(root.user_acl, updated_chats.user_acl) AS user_acl,
COALESCE(root.group_acl, updated_chats.group_acl) AS group_acl,
owner.username AS owner_username,
owner.name AS owner_name,
updated_chats.context_aggregate_hash,
updated_chats.context_dirty_since,
updated_chats.context_dirty_resources,
updated_chats.context_error,
updated_chats.compaction_requested_at
FROM
updated_chats
LEFT JOIN chats root ON root.id = COALESCE(updated_chats.root_chat_id, updated_chats.parent_chat_id)
JOIN visible_users owner ON owner.id = updated_chats.owner_id
)
SELECT 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, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
ORDER BY (chats_expanded.id = $1::uuid) DESC, chats_expanded.created_at ASC, chats_expanded.id ASC
`
func (q *sqlQuerier) UnarchiveChatByID(ctx context.Context, id uuid.UUID) ([]Chat, error) {
rows, err := q.db.QueryContext(ctx, unarchiveChatByID, id)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Chat
for rows.Next() {
var i Chat
if err := rows.Scan(
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.Title,
&i.Status,
&i.WorkerID,
&i.StartedAt,
&i.HeartbeatAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ParentChatID,
&i.RootChatID,
&i.LastModelConfigID,
&i.LastReasoningEffort,
&i.Archived,
&i.LastError,
&i.Mode,
pq.Array(&i.MCPServerIDs),
&i.Labels,
&i.BuildID,
&i.AgentID,
&i.PinOrder,
&i.LastReadMessageID,
&i.DynamicTools,
&i.OrganizationID,
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
&i.Summary,
&i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
&i.GenerationAttempt,
&i.RetryState,
&i.RetryStateVersion,
&i.RunnerID,
&i.RequiresActionDeadlineAt,
&i.UserACL,
&i.GroupACL,
&i.OwnerUsername,
&i.OwnerName,
&i.ContextAggregateHash,
&i.ContextDirtySince,
&i.ContextDirtyResources,
&i.ContextError,
&i.CompactionRequestedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const unpinChatByID = `-- name: UnpinChatByID :exec
WITH target_chat AS (
SELECT
id,
owner_id
FROM
chats
WHERE
id = $1::uuid
),
ranked AS (
SELECT
c.id,
ROW_NUMBER() OVER (ORDER BY c.pin_order ASC, c.id ASC) :: integer AS current_position
FROM
chats c
JOIN
target_chat ON c.owner_id = target_chat.owner_id
WHERE
c.pin_order > 0
AND c.archived = FALSE
),
target AS (
SELECT
ranked.id,
ranked.current_position
FROM
ranked
WHERE
ranked.id = $1::uuid
),
updates AS (
SELECT
ranked.id,
CASE
WHEN ranked.id = target.id THEN 0
WHEN ranked.current_position > target.current_position THEN ranked.current_position - 1
ELSE ranked.current_position
END AS pin_order
FROM
ranked
CROSS JOIN
target
)
UPDATE
chats c
SET
pin_order = updates.pin_order
FROM
updates
WHERE
c.id = updates.id
`
func (q *sqlQuerier) UnpinChatByID(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, unpinChatByID, id)
return err
}
const updateChatACLByID = `-- name: UpdateChatACLByID :exec
UPDATE
chats
SET
user_acl = $1,
group_acl = $2
WHERE
id = $3::uuid
`
type UpdateChatACLByIDParams struct {
UserACL ChatACL `db:"user_acl" json:"user_acl"`
GroupACL ChatACL `db:"group_acl" json:"group_acl"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateChatACLByID(ctx context.Context, arg UpdateChatACLByIDParams) error {
_, err := q.db.ExecContext(ctx, updateChatACLByID, arg.UserACL, arg.GroupACL, arg.ID)
return err
}
const updateChatBuildAgentBinding = `-- name: UpdateChatBuildAgentBinding :one
WITH updated_chat AS (
UPDATE chats SET
build_id = $1::uuid,
agent_id = $2::uuid,
updated_at = NOW()
WHERE
id = $3::uuid
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, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
),
chats_expanded AS (
SELECT
updated_chat.id,
updated_chat.owner_id,
updated_chat.workspace_id,
updated_chat.title,
updated_chat.status,
updated_chat.worker_id,
updated_chat.started_at,
updated_chat.heartbeat_at,
updated_chat.created_at,
updated_chat.updated_at,
updated_chat.parent_chat_id,
updated_chat.root_chat_id,
updated_chat.last_model_config_id,
updated_chat.last_reasoning_effort,
updated_chat.archived,
updated_chat.last_error,
updated_chat.mode,
updated_chat.mcp_server_ids,
updated_chat.labels,
updated_chat.build_id,
updated_chat.agent_id,
updated_chat.pin_order,
updated_chat.last_read_message_id,
updated_chat.dynamic_tools,
updated_chat.organization_id,
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
updated_chat.summary,
updated_chat.summary_generated_at,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
updated_chat.generation_attempt,
updated_chat.retry_state,
updated_chat.retry_state_version,
updated_chat.runner_id,
updated_chat.requires_action_deadline_at,
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
owner.username AS owner_username,
owner.name AS owner_name,
updated_chat.context_aggregate_hash,
updated_chat.context_dirty_since,
updated_chat.context_dirty_resources,
updated_chat.context_error,
updated_chat.compaction_requested_at
FROM
updated_chat
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
JOIN visible_users owner ON owner.id = updated_chat.owner_id
)
SELECT 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, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
`
type UpdateChatBuildAgentBindingParams struct {
BuildID uuid.NullUUID `db:"build_id" json:"build_id"`
AgentID uuid.NullUUID `db:"agent_id" json:"agent_id"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateChatBuildAgentBinding(ctx context.Context, arg UpdateChatBuildAgentBindingParams) (Chat, error) {
row := q.db.QueryRowContext(ctx, updateChatBuildAgentBinding, arg.BuildID, arg.AgentID, arg.ID)
var i Chat
err := row.Scan(
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.Title,
&i.Status,
&i.WorkerID,
&i.StartedAt,
&i.HeartbeatAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ParentChatID,
&i.RootChatID,
&i.LastModelConfigID,
&i.LastReasoningEffort,
&i.Archived,
&i.LastError,
&i.Mode,
pq.Array(&i.MCPServerIDs),
&i.Labels,
&i.BuildID,
&i.AgentID,
&i.PinOrder,
&i.LastReadMessageID,
&i.DynamicTools,
&i.OrganizationID,
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
&i.Summary,
&i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
&i.GenerationAttempt,
&i.RetryState,
&i.RetryStateVersion,
&i.RunnerID,
&i.RequiresActionDeadlineAt,
&i.UserACL,
&i.GroupACL,
&i.OwnerUsername,
&i.OwnerName,
&i.ContextAggregateHash,
&i.ContextDirtySince,
&i.ContextDirtyResources,
&i.ContextError,
&i.CompactionRequestedAt,
)
return i, err
}
const updateChatByID = `-- name: UpdateChatByID :one
WITH updated_chat AS (
UPDATE
chats
SET
title = $1::text,
updated_at = NOW()
WHERE
id = $2::uuid
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, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
),
chats_expanded AS (
SELECT
updated_chat.id,
updated_chat.owner_id,
updated_chat.workspace_id,
updated_chat.title,
updated_chat.status,
updated_chat.worker_id,
updated_chat.started_at,
updated_chat.heartbeat_at,
updated_chat.created_at,
updated_chat.updated_at,
updated_chat.parent_chat_id,
updated_chat.root_chat_id,
updated_chat.last_model_config_id,
updated_chat.last_reasoning_effort,
updated_chat.archived,
updated_chat.last_error,
updated_chat.mode,
updated_chat.mcp_server_ids,
updated_chat.labels,
updated_chat.build_id,
updated_chat.agent_id,
updated_chat.pin_order,
updated_chat.last_read_message_id,
updated_chat.dynamic_tools,
updated_chat.organization_id,
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
updated_chat.summary,
updated_chat.summary_generated_at,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
updated_chat.generation_attempt,
updated_chat.retry_state,
updated_chat.retry_state_version,
updated_chat.runner_id,
updated_chat.requires_action_deadline_at,
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
owner.username AS owner_username,
owner.name AS owner_name,
updated_chat.context_aggregate_hash,
updated_chat.context_dirty_since,
updated_chat.context_dirty_resources,
updated_chat.context_error,
updated_chat.compaction_requested_at
FROM
updated_chat
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
JOIN visible_users owner ON owner.id = updated_chat.owner_id
)
SELECT 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, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
`
type UpdateChatByIDParams struct {
Title string `db:"title" json:"title"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateChatByID(ctx context.Context, arg UpdateChatByIDParams) (Chat, error) {
row := q.db.QueryRowContext(ctx, updateChatByID, arg.Title, arg.ID)
var i Chat
err := row.Scan(
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.Title,
&i.Status,
&i.WorkerID,
&i.StartedAt,
&i.HeartbeatAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ParentChatID,
&i.RootChatID,
&i.LastModelConfigID,
&i.LastReasoningEffort,
&i.Archived,
&i.LastError,
&i.Mode,
pq.Array(&i.MCPServerIDs),
&i.Labels,
&i.BuildID,
&i.AgentID,
&i.PinOrder,
&i.LastReadMessageID,
&i.DynamicTools,
&i.OrganizationID,
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
&i.Summary,
&i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
&i.GenerationAttempt,
&i.RetryState,
&i.RetryStateVersion,
&i.RunnerID,
&i.RequiresActionDeadlineAt,
&i.UserACL,
&i.GroupACL,
&i.OwnerUsername,
&i.OwnerName,
&i.ContextAggregateHash,
&i.ContextDirtySince,
&i.ContextDirtyResources,
&i.ContextError,
&i.CompactionRequestedAt,
)
return i, err
}
const updateChatExecutionState = `-- name: UpdateChatExecutionState :one
WITH updated_chat AS (
UPDATE chats
SET
status = $1::chat_status,
archived = $2::boolean,
worker_id = $3::uuid,
runner_id = $4::uuid,
last_error = $5::jsonb,
requires_action_deadline_at = $6::timestamptz,
compaction_requested_at = $7::timestamptz,
history_version = CASE WHEN $8::boolean THEN snapshot_version ELSE history_version END,
generation_attempt = CASE WHEN $8::boolean THEN 0 ELSE generation_attempt END,
retry_state = CASE WHEN $8::boolean THEN NULL ELSE retry_state END,
pin_order = CASE WHEN $2::boolean THEN 0 ELSE pin_order END,
updated_at = NOW()
WHERE id = $9::uuid
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, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
),
chats_expanded AS (
SELECT
updated_chat.id,
updated_chat.owner_id,
updated_chat.workspace_id,
updated_chat.title,
updated_chat.status,
updated_chat.worker_id,
updated_chat.started_at,
updated_chat.heartbeat_at,
updated_chat.created_at,
updated_chat.updated_at,
updated_chat.parent_chat_id,
updated_chat.root_chat_id,
updated_chat.last_model_config_id,
updated_chat.last_reasoning_effort,
updated_chat.archived,
updated_chat.last_error,
updated_chat.mode,
updated_chat.mcp_server_ids,
updated_chat.labels,
updated_chat.build_id,
updated_chat.agent_id,
updated_chat.pin_order,
updated_chat.last_read_message_id,
updated_chat.dynamic_tools,
updated_chat.organization_id,
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
updated_chat.summary,
updated_chat.summary_generated_at,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
updated_chat.generation_attempt,
updated_chat.retry_state,
updated_chat.retry_state_version,
updated_chat.runner_id,
updated_chat.requires_action_deadline_at,
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
owner.username AS owner_username,
owner.name AS owner_name,
updated_chat.context_aggregate_hash,
updated_chat.context_dirty_since,
updated_chat.context_dirty_resources,
updated_chat.context_error,
updated_chat.compaction_requested_at
FROM updated_chat
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
JOIN visible_users owner ON owner.id = updated_chat.owner_id
)
SELECT 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, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
`
type UpdateChatExecutionStateParams struct {
Status ChatStatus `db:"status" json:"status"`
Archived bool `db:"archived" json:"archived"`
WorkerID uuid.NullUUID `db:"worker_id" json:"worker_id"`
RunnerID uuid.NullUUID `db:"runner_id" json:"runner_id"`
LastError pqtype.NullRawMessage `db:"last_error" json:"last_error"`
RequiresActionDeadlineAt sql.NullTime `db:"requires_action_deadline_at" json:"requires_action_deadline_at"`
CompactionRequestedAt sql.NullTime `db:"compaction_requested_at" json:"compaction_requested_at"`
GrantHistoryEpoch bool `db:"grant_history_epoch" json:"grant_history_epoch"`
ID uuid.UUID `db:"id" json:"id"`
}
// Atomically updates the execution-state-managed fields on a chat:
// status, archived, last_error, ownership identifiers, the
// requires-action deadline, and the manual compaction request marker.
// Callers compose this with transition mutations inside a single
// ChatMachine.Update transaction.
//
// grant_history_epoch gives a turn that inserts no history the same
// fresh retry budget and message part episode keys a history change
// would grant, mirroring the chat_messages trigger postcondition.
func (q *sqlQuerier) UpdateChatExecutionState(ctx context.Context, arg UpdateChatExecutionStateParams) (Chat, error) {
row := q.db.QueryRowContext(ctx, updateChatExecutionState,
arg.Status,
arg.Archived,
arg.WorkerID,
arg.RunnerID,
arg.LastError,
arg.RequiresActionDeadlineAt,
arg.CompactionRequestedAt,
arg.GrantHistoryEpoch,
arg.ID,
)
var i Chat
err := row.Scan(
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.Title,
&i.Status,
&i.WorkerID,
&i.StartedAt,
&i.HeartbeatAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ParentChatID,
&i.RootChatID,
&i.LastModelConfigID,
&i.LastReasoningEffort,
&i.Archived,
&i.LastError,
&i.Mode,
pq.Array(&i.MCPServerIDs),
&i.Labels,
&i.BuildID,
&i.AgentID,
&i.PinOrder,
&i.LastReadMessageID,
&i.DynamicTools,
&i.OrganizationID,
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
&i.Summary,
&i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
&i.GenerationAttempt,
&i.RetryState,
&i.RetryStateVersion,
&i.RunnerID,
&i.RequiresActionDeadlineAt,
&i.UserACL,
&i.GroupACL,
&i.OwnerUsername,
&i.OwnerName,
&i.ContextAggregateHash,
&i.ContextDirtySince,
&i.ContextDirtyResources,
&i.ContextError,
&i.CompactionRequestedAt,
)
return i, err
}
const updateChatHeartbeats = `-- name: UpdateChatHeartbeats :many
UPDATE
chats
SET
heartbeat_at = $1::timestamptz
WHERE
id = ANY($2::uuid[])
AND worker_id = $3::uuid
AND status = 'running'::chat_status
RETURNING id
`
type UpdateChatHeartbeatsParams struct {
Now time.Time `db:"now" json:"now"`
IDs []uuid.UUID `db:"ids" json:"ids"`
WorkerID uuid.UUID `db:"worker_id" json:"worker_id"`
}
// Bumps the heartbeat timestamp for the given set of chat IDs,
// provided they are still running and owned by the specified
// worker. Returns the IDs that were actually updated so the
// caller can detect stolen or completed chats via set-difference.
func (q *sqlQuerier) UpdateChatHeartbeats(ctx context.Context, arg UpdateChatHeartbeatsParams) ([]uuid.UUID, error) {
rows, err := q.db.QueryContext(ctx, updateChatHeartbeats, arg.Now, pq.Array(arg.IDs), arg.WorkerID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []uuid.UUID
for rows.Next() {
var id uuid.UUID
if err := rows.Scan(&id); err != nil {
return nil, err
}
items = append(items, id)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateChatLabelsByID = `-- name: UpdateChatLabelsByID :one
WITH updated_chat AS (
UPDATE
chats
SET
labels = $1::jsonb,
updated_at = NOW()
WHERE
id = $2::uuid
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, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
),
chats_expanded AS (
SELECT
updated_chat.id,
updated_chat.owner_id,
updated_chat.workspace_id,
updated_chat.title,
updated_chat.status,
updated_chat.worker_id,
updated_chat.started_at,
updated_chat.heartbeat_at,
updated_chat.created_at,
updated_chat.updated_at,
updated_chat.parent_chat_id,
updated_chat.root_chat_id,
updated_chat.last_model_config_id,
updated_chat.last_reasoning_effort,
updated_chat.archived,
updated_chat.last_error,
updated_chat.mode,
updated_chat.mcp_server_ids,
updated_chat.labels,
updated_chat.build_id,
updated_chat.agent_id,
updated_chat.pin_order,
updated_chat.last_read_message_id,
updated_chat.dynamic_tools,
updated_chat.organization_id,
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
updated_chat.summary,
updated_chat.summary_generated_at,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
updated_chat.generation_attempt,
updated_chat.retry_state,
updated_chat.retry_state_version,
updated_chat.runner_id,
updated_chat.requires_action_deadline_at,
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
owner.username AS owner_username,
owner.name AS owner_name,
updated_chat.context_aggregate_hash,
updated_chat.context_dirty_since,
updated_chat.context_dirty_resources,
updated_chat.context_error,
updated_chat.compaction_requested_at
FROM
updated_chat
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
JOIN visible_users owner ON owner.id = updated_chat.owner_id
)
SELECT 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, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
`
type UpdateChatLabelsByIDParams struct {
Labels json.RawMessage `db:"labels" json:"labels"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateChatLabelsByID(ctx context.Context, arg UpdateChatLabelsByIDParams) (Chat, error) {
row := q.db.QueryRowContext(ctx, updateChatLabelsByID, arg.Labels, arg.ID)
var i Chat
err := row.Scan(
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.Title,
&i.Status,
&i.WorkerID,
&i.StartedAt,
&i.HeartbeatAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ParentChatID,
&i.RootChatID,
&i.LastModelConfigID,
&i.LastReasoningEffort,
&i.Archived,
&i.LastError,
&i.Mode,
pq.Array(&i.MCPServerIDs),
&i.Labels,
&i.BuildID,
&i.AgentID,
&i.PinOrder,
&i.LastReadMessageID,
&i.DynamicTools,
&i.OrganizationID,
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
&i.Summary,
&i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
&i.GenerationAttempt,
&i.RetryState,
&i.RetryStateVersion,
&i.RunnerID,
&i.RequiresActionDeadlineAt,
&i.UserACL,
&i.GroupACL,
&i.OwnerUsername,
&i.OwnerName,
&i.ContextAggregateHash,
&i.ContextDirtySince,
&i.ContextDirtyResources,
&i.ContextError,
&i.CompactionRequestedAt,
)
return i, err
}
const updateChatLastModelConfigByID = `-- name: UpdateChatLastModelConfigByID :one
WITH updated_chat AS (
UPDATE
chats
SET
-- NOTE: updated_at is intentionally NOT touched here to avoid changing list ordering.
last_model_config_id = $1::uuid
WHERE
id = $2::uuid
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, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
),
chats_expanded AS (
SELECT
updated_chat.id,
updated_chat.owner_id,
updated_chat.workspace_id,
updated_chat.title,
updated_chat.status,
updated_chat.worker_id,
updated_chat.started_at,
updated_chat.heartbeat_at,
updated_chat.created_at,
updated_chat.updated_at,
updated_chat.parent_chat_id,
updated_chat.root_chat_id,
updated_chat.last_model_config_id,
updated_chat.last_reasoning_effort,
updated_chat.archived,
updated_chat.last_error,
updated_chat.mode,
updated_chat.mcp_server_ids,
updated_chat.labels,
updated_chat.build_id,
updated_chat.agent_id,
updated_chat.pin_order,
updated_chat.last_read_message_id,
updated_chat.dynamic_tools,
updated_chat.organization_id,
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
updated_chat.summary,
updated_chat.summary_generated_at,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
updated_chat.generation_attempt,
updated_chat.retry_state,
updated_chat.retry_state_version,
updated_chat.runner_id,
updated_chat.requires_action_deadline_at,
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
owner.username AS owner_username,
owner.name AS owner_name,
updated_chat.context_aggregate_hash,
updated_chat.context_dirty_since,
updated_chat.context_dirty_resources,
updated_chat.context_error,
updated_chat.compaction_requested_at
FROM
updated_chat
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
JOIN visible_users owner ON owner.id = updated_chat.owner_id
)
SELECT 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, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
`
type UpdateChatLastModelConfigByIDParams struct {
LastModelConfigID uuid.UUID `db:"last_model_config_id" json:"last_model_config_id"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateChatLastModelConfigByID(ctx context.Context, arg UpdateChatLastModelConfigByIDParams) (Chat, error) {
row := q.db.QueryRowContext(ctx, updateChatLastModelConfigByID, arg.LastModelConfigID, arg.ID)
var i Chat
err := row.Scan(
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.Title,
&i.Status,
&i.WorkerID,
&i.StartedAt,
&i.HeartbeatAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ParentChatID,
&i.RootChatID,
&i.LastModelConfigID,
&i.LastReasoningEffort,
&i.Archived,
&i.LastError,
&i.Mode,
pq.Array(&i.MCPServerIDs),
&i.Labels,
&i.BuildID,
&i.AgentID,
&i.PinOrder,
&i.LastReadMessageID,
&i.DynamicTools,
&i.OrganizationID,
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
&i.Summary,
&i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
&i.GenerationAttempt,
&i.RetryState,
&i.RetryStateVersion,
&i.RunnerID,
&i.RequiresActionDeadlineAt,
&i.UserACL,
&i.GroupACL,
&i.OwnerUsername,
&i.OwnerName,
&i.ContextAggregateHash,
&i.ContextDirtySince,
&i.ContextDirtyResources,
&i.ContextError,
&i.CompactionRequestedAt,
)
return i, err
}
const updateChatLastReadMessageID = `-- name: UpdateChatLastReadMessageID :exec
UPDATE chats
SET last_read_message_id = $1::bigint
WHERE id = $2::uuid
`
type UpdateChatLastReadMessageIDParams struct {
LastReadMessageID int64 `db:"last_read_message_id" json:"last_read_message_id"`
ID uuid.UUID `db:"id" json:"id"`
}
// Updates the last read message ID for a chat. This is used to track
// which messages the owner has seen, enabling unread indicators.
func (q *sqlQuerier) UpdateChatLastReadMessageID(ctx context.Context, arg UpdateChatLastReadMessageIDParams) error {
_, err := q.db.ExecContext(ctx, updateChatLastReadMessageID, arg.LastReadMessageID, arg.ID)
return err
}
const updateChatLastTurnSummary = `-- name: UpdateChatLastTurnSummary :execrows
UPDATE chats
SET
last_turn_summary = NULLIF(REGEXP_REPLACE(
$1::text, '^[[:space:]]+|[[:space:]]+$', '', 'g'
), '')
WHERE
id = $2::uuid
AND history_version = $3::bigint
`
type UpdateChatLastTurnSummaryParams struct {
LastTurnSummary sql.NullString `db:"last_turn_summary" json:"last_turn_summary"`
ID uuid.UUID `db:"id" json:"id"`
ExpectedHistoryVersion int64 `db:"expected_history_version" json:"expected_history_version"`
}
// Updates the cached last completed turn summary for sidebar display.
// Empty or whitespace-only summaries are stored as NULL here so direct
// query callers cannot accidentally persist blank sidebar text.
// This intentionally preserves updated_at. The staleness guard uses
// history_version so worker lifecycle transitions that do not change the
// active message history cannot reject final turn summary writes.
// Two summary workers using the same freshness marker are last-write-wins.
func (q *sqlQuerier) UpdateChatLastTurnSummary(ctx context.Context, arg UpdateChatLastTurnSummaryParams) (int64, error) {
result, err := q.db.ExecContext(ctx, updateChatLastTurnSummary, arg.LastTurnSummary, arg.ID, arg.ExpectedHistoryVersion)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const updateChatMCPServerIDs = `-- name: UpdateChatMCPServerIDs :one
WITH updated_chat AS (
UPDATE
chats
SET
mcp_server_ids = $1::uuid[],
updated_at = NOW()
WHERE
id = $2::uuid
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, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
),
chats_expanded AS (
SELECT
updated_chat.id,
updated_chat.owner_id,
updated_chat.workspace_id,
updated_chat.title,
updated_chat.status,
updated_chat.worker_id,
updated_chat.started_at,
updated_chat.heartbeat_at,
updated_chat.created_at,
updated_chat.updated_at,
updated_chat.parent_chat_id,
updated_chat.root_chat_id,
updated_chat.last_model_config_id,
updated_chat.last_reasoning_effort,
updated_chat.archived,
updated_chat.last_error,
updated_chat.mode,
updated_chat.mcp_server_ids,
updated_chat.labels,
updated_chat.build_id,
updated_chat.agent_id,
updated_chat.pin_order,
updated_chat.last_read_message_id,
updated_chat.dynamic_tools,
updated_chat.organization_id,
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
updated_chat.summary,
updated_chat.summary_generated_at,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
updated_chat.generation_attempt,
updated_chat.retry_state,
updated_chat.retry_state_version,
updated_chat.runner_id,
updated_chat.requires_action_deadline_at,
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
owner.username AS owner_username,
owner.name AS owner_name,
updated_chat.context_aggregate_hash,
updated_chat.context_dirty_since,
updated_chat.context_dirty_resources,
updated_chat.context_error,
updated_chat.compaction_requested_at
FROM
updated_chat
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
JOIN visible_users owner ON owner.id = updated_chat.owner_id
)
SELECT 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, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
`
type UpdateChatMCPServerIDsParams struct {
MCPServerIDs []uuid.UUID `db:"mcp_server_ids" json:"mcp_server_ids"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateChatMCPServerIDs(ctx context.Context, arg UpdateChatMCPServerIDsParams) (Chat, error) {
row := q.db.QueryRowContext(ctx, updateChatMCPServerIDs, pq.Array(arg.MCPServerIDs), arg.ID)
var i Chat
err := row.Scan(
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.Title,
&i.Status,
&i.WorkerID,
&i.StartedAt,
&i.HeartbeatAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ParentChatID,
&i.RootChatID,
&i.LastModelConfigID,
&i.LastReasoningEffort,
&i.Archived,
&i.LastError,
&i.Mode,
pq.Array(&i.MCPServerIDs),
&i.Labels,
&i.BuildID,
&i.AgentID,
&i.PinOrder,
&i.LastReadMessageID,
&i.DynamicTools,
&i.OrganizationID,
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
&i.Summary,
&i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
&i.GenerationAttempt,
&i.RetryState,
&i.RetryStateVersion,
&i.RunnerID,
&i.RequiresActionDeadlineAt,
&i.UserACL,
&i.GroupACL,
&i.OwnerUsername,
&i.OwnerName,
&i.ContextAggregateHash,
&i.ContextDirtySince,
&i.ContextDirtyResources,
&i.ContextError,
&i.CompactionRequestedAt,
)
return i, err
}
const updateChatPinOrder = `-- name: UpdateChatPinOrder :exec
WITH target_chat AS (
SELECT
id,
owner_id
FROM
chats
WHERE
id = $1::uuid
),
ranked AS (
SELECT
c.id,
ROW_NUMBER() OVER (ORDER BY c.pin_order ASC, c.id ASC) :: integer AS current_position,
COUNT(*) OVER () :: integer AS pinned_count
FROM
chats c
JOIN
target_chat ON c.owner_id = target_chat.owner_id
WHERE
c.pin_order > 0
AND c.archived = FALSE
),
target AS (
SELECT
ranked.id,
ranked.current_position,
LEAST(GREATEST($2::integer, 1), ranked.pinned_count) AS desired_position
FROM
ranked
WHERE
ranked.id = $1::uuid
),
updates AS (
SELECT
ranked.id,
CASE
WHEN ranked.id = target.id THEN target.desired_position
WHEN target.desired_position < target.current_position
AND ranked.current_position >= target.desired_position
AND ranked.current_position < target.current_position THEN ranked.current_position + 1
WHEN target.desired_position > target.current_position
AND ranked.current_position > target.current_position
AND ranked.current_position <= target.desired_position THEN ranked.current_position - 1
ELSE ranked.current_position
END AS pin_order
FROM
ranked
CROSS JOIN
target
)
UPDATE
chats c
SET
pin_order = updates.pin_order
FROM
updates
WHERE
c.id = updates.id
`
type UpdateChatPinOrderParams struct {
ID uuid.UUID `db:"id" json:"id"`
PinOrder int32 `db:"pin_order" json:"pin_order"`
}
func (q *sqlQuerier) UpdateChatPinOrder(ctx context.Context, arg UpdateChatPinOrderParams) error {
_, err := q.db.ExecContext(ctx, updateChatPinOrder, arg.ID, arg.PinOrder)
return err
}
const updateChatPlanModeByID = `-- name: UpdateChatPlanModeByID :one
WITH updated_chat AS (
UPDATE
chats
SET
-- NOTE: updated_at is intentionally NOT touched here to avoid changing list ordering.
plan_mode = $1::chat_plan_mode
WHERE
id = $2::uuid
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, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
),
chats_expanded AS (
SELECT
updated_chat.id,
updated_chat.owner_id,
updated_chat.workspace_id,
updated_chat.title,
updated_chat.status,
updated_chat.worker_id,
updated_chat.started_at,
updated_chat.heartbeat_at,
updated_chat.created_at,
updated_chat.updated_at,
updated_chat.parent_chat_id,
updated_chat.root_chat_id,
updated_chat.last_model_config_id,
updated_chat.last_reasoning_effort,
updated_chat.archived,
updated_chat.last_error,
updated_chat.mode,
updated_chat.mcp_server_ids,
updated_chat.labels,
updated_chat.build_id,
updated_chat.agent_id,
updated_chat.pin_order,
updated_chat.last_read_message_id,
updated_chat.dynamic_tools,
updated_chat.organization_id,
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
updated_chat.summary,
updated_chat.summary_generated_at,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
updated_chat.generation_attempt,
updated_chat.retry_state,
updated_chat.retry_state_version,
updated_chat.runner_id,
updated_chat.requires_action_deadline_at,
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
owner.username AS owner_username,
owner.name AS owner_name,
updated_chat.context_aggregate_hash,
updated_chat.context_dirty_since,
updated_chat.context_dirty_resources,
updated_chat.context_error,
updated_chat.compaction_requested_at
FROM
updated_chat
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
JOIN visible_users owner ON owner.id = updated_chat.owner_id
)
SELECT 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, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
`
type UpdateChatPlanModeByIDParams struct {
PlanMode NullChatPlanMode `db:"plan_mode" json:"plan_mode"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateChatPlanModeByID(ctx context.Context, arg UpdateChatPlanModeByIDParams) (Chat, error) {
row := q.db.QueryRowContext(ctx, updateChatPlanModeByID, arg.PlanMode, arg.ID)
var i Chat
err := row.Scan(
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.Title,
&i.Status,
&i.WorkerID,
&i.StartedAt,
&i.HeartbeatAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ParentChatID,
&i.RootChatID,
&i.LastModelConfigID,
&i.LastReasoningEffort,
&i.Archived,
&i.LastError,
&i.Mode,
pq.Array(&i.MCPServerIDs),
&i.Labels,
&i.BuildID,
&i.AgentID,
&i.PinOrder,
&i.LastReadMessageID,
&i.DynamicTools,
&i.OrganizationID,
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
&i.Summary,
&i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
&i.GenerationAttempt,
&i.RetryState,
&i.RetryStateVersion,
&i.RunnerID,
&i.RequiresActionDeadlineAt,
&i.UserACL,
&i.GroupACL,
&i.OwnerUsername,
&i.OwnerName,
&i.ContextAggregateHash,
&i.ContextDirtySince,
&i.ContextDirtyResources,
&i.ContextError,
&i.CompactionRequestedAt,
)
return i, err
}
const updateChatRetryState = `-- name: UpdateChatRetryState :one
WITH updated_chat AS (
UPDATE chats
SET
retry_state = $1::jsonb,
updated_at = NOW()
WHERE id = $2::uuid
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, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
),
chats_expanded AS (
SELECT
updated_chat.id,
updated_chat.owner_id,
updated_chat.workspace_id,
updated_chat.title,
updated_chat.status,
updated_chat.worker_id,
updated_chat.started_at,
updated_chat.heartbeat_at,
updated_chat.created_at,
updated_chat.updated_at,
updated_chat.parent_chat_id,
updated_chat.root_chat_id,
updated_chat.last_model_config_id,
updated_chat.last_reasoning_effort,
updated_chat.archived,
updated_chat.last_error,
updated_chat.mode,
updated_chat.mcp_server_ids,
updated_chat.labels,
updated_chat.build_id,
updated_chat.agent_id,
updated_chat.pin_order,
updated_chat.last_read_message_id,
updated_chat.dynamic_tools,
updated_chat.organization_id,
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
updated_chat.summary,
updated_chat.summary_generated_at,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
updated_chat.generation_attempt,
updated_chat.retry_state,
updated_chat.retry_state_version,
updated_chat.runner_id,
updated_chat.requires_action_deadline_at,
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
owner.username AS owner_username,
owner.name AS owner_name,
updated_chat.context_aggregate_hash,
updated_chat.context_dirty_since,
updated_chat.context_dirty_resources,
updated_chat.context_error,
updated_chat.compaction_requested_at
FROM updated_chat
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
JOIN visible_users owner ON owner.id = updated_chat.owner_id
)
SELECT 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, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
`
type UpdateChatRetryStateParams struct {
RetryState json.RawMessage `db:"retry_state" json:"retry_state"`
ID uuid.UUID `db:"id" json:"id"`
}
// Stores the client-visible retry payload. retry_state_version is
// assigned by trigger from the current snapshot_version.
func (q *sqlQuerier) UpdateChatRetryState(ctx context.Context, arg UpdateChatRetryStateParams) (Chat, error) {
row := q.db.QueryRowContext(ctx, updateChatRetryState, arg.RetryState, arg.ID)
var i Chat
err := row.Scan(
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.Title,
&i.Status,
&i.WorkerID,
&i.StartedAt,
&i.HeartbeatAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ParentChatID,
&i.RootChatID,
&i.LastModelConfigID,
&i.LastReasoningEffort,
&i.Archived,
&i.LastError,
&i.Mode,
pq.Array(&i.MCPServerIDs),
&i.Labels,
&i.BuildID,
&i.AgentID,
&i.PinOrder,
&i.LastReadMessageID,
&i.DynamicTools,
&i.OrganizationID,
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
&i.Summary,
&i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
&i.GenerationAttempt,
&i.RetryState,
&i.RetryStateVersion,
&i.RunnerID,
&i.RequiresActionDeadlineAt,
&i.UserACL,
&i.GroupACL,
&i.OwnerUsername,
&i.OwnerName,
&i.ContextAggregateHash,
&i.ContextDirtySince,
&i.ContextDirtyResources,
&i.ContextError,
&i.CompactionRequestedAt,
)
return i, err
}
const updateChatStatus = `-- name: UpdateChatStatus :one
WITH updated_chat AS (
UPDATE
chats
SET
status = $1::chat_status,
worker_id = $2::uuid,
started_at = $3::timestamptz,
heartbeat_at = $4::timestamptz,
last_error = $5::jsonb,
updated_at = NOW()
WHERE
id = $6::uuid
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, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
),
chats_expanded AS (
SELECT
updated_chat.id,
updated_chat.owner_id,
updated_chat.workspace_id,
updated_chat.title,
updated_chat.status,
updated_chat.worker_id,
updated_chat.started_at,
updated_chat.heartbeat_at,
updated_chat.created_at,
updated_chat.updated_at,
updated_chat.parent_chat_id,
updated_chat.root_chat_id,
updated_chat.last_model_config_id,
updated_chat.last_reasoning_effort,
updated_chat.archived,
updated_chat.last_error,
updated_chat.mode,
updated_chat.mcp_server_ids,
updated_chat.labels,
updated_chat.build_id,
updated_chat.agent_id,
updated_chat.pin_order,
updated_chat.last_read_message_id,
updated_chat.dynamic_tools,
updated_chat.organization_id,
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
updated_chat.summary,
updated_chat.summary_generated_at,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
updated_chat.generation_attempt,
updated_chat.retry_state,
updated_chat.retry_state_version,
updated_chat.runner_id,
updated_chat.requires_action_deadline_at,
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
owner.username AS owner_username,
owner.name AS owner_name,
updated_chat.context_aggregate_hash,
updated_chat.context_dirty_since,
updated_chat.context_dirty_resources,
updated_chat.context_error,
updated_chat.compaction_requested_at
FROM
updated_chat
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
JOIN visible_users owner ON owner.id = updated_chat.owner_id
)
SELECT 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, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
`
type UpdateChatStatusParams struct {
Status ChatStatus `db:"status" json:"status"`
WorkerID uuid.NullUUID `db:"worker_id" json:"worker_id"`
StartedAt sql.NullTime `db:"started_at" json:"started_at"`
HeartbeatAt sql.NullTime `db:"heartbeat_at" json:"heartbeat_at"`
LastError pqtype.NullRawMessage `db:"last_error" json:"last_error"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateChatStatus(ctx context.Context, arg UpdateChatStatusParams) (Chat, error) {
row := q.db.QueryRowContext(ctx, updateChatStatus,
arg.Status,
arg.WorkerID,
arg.StartedAt,
arg.HeartbeatAt,
arg.LastError,
arg.ID,
)
var i Chat
err := row.Scan(
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.Title,
&i.Status,
&i.WorkerID,
&i.StartedAt,
&i.HeartbeatAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ParentChatID,
&i.RootChatID,
&i.LastModelConfigID,
&i.LastReasoningEffort,
&i.Archived,
&i.LastError,
&i.Mode,
pq.Array(&i.MCPServerIDs),
&i.Labels,
&i.BuildID,
&i.AgentID,
&i.PinOrder,
&i.LastReadMessageID,
&i.DynamicTools,
&i.OrganizationID,
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
&i.Summary,
&i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
&i.GenerationAttempt,
&i.RetryState,
&i.RetryStateVersion,
&i.RunnerID,
&i.RequiresActionDeadlineAt,
&i.UserACL,
&i.GroupACL,
&i.OwnerUsername,
&i.OwnerName,
&i.ContextAggregateHash,
&i.ContextDirtySince,
&i.ContextDirtyResources,
&i.ContextError,
&i.CompactionRequestedAt,
)
return i, err
}
const updateChatSummary = `-- name: UpdateChatSummary :execrows
UPDATE chats
SET
summary = $1::text,
summary_generated_at = NOW()
WHERE
id = $2::uuid
AND history_version = $3::bigint
`
type UpdateChatSummaryParams struct {
Summary sql.NullString `db:"summary" json:"summary"`
ID uuid.UUID `db:"id" json:"id"`
ExpectedHistoryVersion int64 `db:"expected_history_version" json:"expected_history_version"`
}
// The history_version fence lets background summary writes ignore worker-only
// updates while losing to newer message history.
func (q *sqlQuerier) UpdateChatSummary(ctx context.Context, arg UpdateChatSummaryParams) (int64, error) {
result, err := q.db.ExecContext(ctx, updateChatSummary, arg.Summary, arg.ID, arg.ExpectedHistoryVersion)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const updateChatTitleByID = `-- name: UpdateChatTitleByID :one
WITH updated_chat AS (
UPDATE
chats
SET
-- NOTE: updated_at is intentionally NOT touched here to avoid
-- changing list ordering when a user renames an older chat
-- out-of-band.
title = $1::text
WHERE
id = $2::uuid
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, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
),
chats_expanded AS (
SELECT
updated_chat.id,
updated_chat.owner_id,
updated_chat.workspace_id,
updated_chat.title,
updated_chat.status,
updated_chat.worker_id,
updated_chat.started_at,
updated_chat.heartbeat_at,
updated_chat.created_at,
updated_chat.updated_at,
updated_chat.parent_chat_id,
updated_chat.root_chat_id,
updated_chat.last_model_config_id,
updated_chat.last_reasoning_effort,
updated_chat.archived,
updated_chat.last_error,
updated_chat.mode,
updated_chat.mcp_server_ids,
updated_chat.labels,
updated_chat.build_id,
updated_chat.agent_id,
updated_chat.pin_order,
updated_chat.last_read_message_id,
updated_chat.dynamic_tools,
updated_chat.organization_id,
updated_chat.plan_mode,
updated_chat.client_type,
updated_chat.last_turn_summary,
updated_chat.summary,
updated_chat.summary_generated_at,
updated_chat.snapshot_version,
updated_chat.history_version,
updated_chat.queue_version,
updated_chat.generation_attempt,
updated_chat.retry_state,
updated_chat.retry_state_version,
updated_chat.runner_id,
updated_chat.requires_action_deadline_at,
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
owner.username AS owner_username,
owner.name AS owner_name,
updated_chat.context_aggregate_hash,
updated_chat.context_dirty_since,
updated_chat.context_dirty_resources,
updated_chat.context_error,
updated_chat.compaction_requested_at
FROM
updated_chat
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
JOIN visible_users owner ON owner.id = updated_chat.owner_id
)
SELECT 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, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
`
type UpdateChatTitleByIDParams struct {
Title string `db:"title" json:"title"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateChatTitleByID(ctx context.Context, arg UpdateChatTitleByIDParams) (Chat, error) {
row := q.db.QueryRowContext(ctx, updateChatTitleByID, arg.Title, arg.ID)
var i Chat
err := row.Scan(
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.Title,
&i.Status,
&i.WorkerID,
&i.StartedAt,
&i.HeartbeatAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ParentChatID,
&i.RootChatID,
&i.LastModelConfigID,
&i.LastReasoningEffort,
&i.Archived,
&i.LastError,
&i.Mode,
pq.Array(&i.MCPServerIDs),
&i.Labels,
&i.BuildID,
&i.AgentID,
&i.PinOrder,
&i.LastReadMessageID,
&i.DynamicTools,
&i.OrganizationID,
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
&i.Summary,
&i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
&i.GenerationAttempt,
&i.RetryState,
&i.RetryStateVersion,
&i.RunnerID,
&i.RequiresActionDeadlineAt,
&i.UserACL,
&i.GroupACL,
&i.OwnerUsername,
&i.OwnerName,
&i.ContextAggregateHash,
&i.ContextDirtySince,
&i.ContextDirtyResources,
&i.ContextError,
&i.CompactionRequestedAt,
)
return i, err
}
const updateChatWorkspaceBinding = `-- name: UpdateChatWorkspaceBinding :one
WITH current_chat AS (
SELECT 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, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
FROM chats
WHERE id = $1::uuid
),
binding_changed AS (
SELECT
workspace_id IS DISTINCT FROM $2::uuid
OR build_id IS DISTINCT FROM $3::uuid
OR agent_id IS DISTINCT FROM $4::uuid AS changed
FROM current_chat
),
changed_chat AS (
UPDATE chats SET
workspace_id = $2::uuid,
build_id = $3::uuid,
agent_id = $4::uuid,
updated_at = NOW()
WHERE id = $1::uuid
AND (SELECT changed FROM binding_changed)
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, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
),
result_chat AS (
SELECT 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, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
FROM changed_chat
UNION ALL
SELECT 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, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, last_reasoning_effort, compaction_requested_at, summary, summary_generated_at
FROM current_chat
WHERE NOT (SELECT changed FROM binding_changed)
),
chats_expanded AS (
SELECT
result_chat.id,
result_chat.owner_id,
result_chat.workspace_id,
result_chat.title,
result_chat.status,
result_chat.worker_id,
result_chat.started_at,
result_chat.heartbeat_at,
result_chat.created_at,
result_chat.updated_at,
result_chat.parent_chat_id,
result_chat.root_chat_id,
result_chat.last_model_config_id,
result_chat.last_reasoning_effort,
result_chat.archived,
result_chat.last_error,
result_chat.mode,
result_chat.mcp_server_ids,
result_chat.labels,
result_chat.build_id,
result_chat.agent_id,
result_chat.pin_order,
result_chat.last_read_message_id,
result_chat.dynamic_tools,
result_chat.organization_id,
result_chat.plan_mode,
result_chat.client_type,
result_chat.last_turn_summary,
result_chat.summary,
result_chat.summary_generated_at,
result_chat.snapshot_version,
result_chat.history_version,
result_chat.queue_version,
result_chat.generation_attempt,
result_chat.retry_state,
result_chat.retry_state_version,
result_chat.runner_id,
result_chat.requires_action_deadline_at,
COALESCE(root.user_acl, result_chat.user_acl) AS user_acl,
COALESCE(root.group_acl, result_chat.group_acl) AS group_acl,
owner.username AS owner_username,
owner.name AS owner_name,
result_chat.context_aggregate_hash,
result_chat.context_dirty_since,
result_chat.context_dirty_resources,
result_chat.context_error,
result_chat.compaction_requested_at
FROM
result_chat
LEFT JOIN chats root ON root.id = COALESCE(result_chat.root_chat_id, result_chat.parent_chat_id)
JOIN visible_users owner ON owner.id = result_chat.owner_id
)
SELECT 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, last_reasoning_effort, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, summary, summary_generated_at, snapshot_version, history_version, queue_version, generation_attempt, retry_state, retry_state_version, runner_id, requires_action_deadline_at, user_acl, group_acl, owner_username, owner_name, context_aggregate_hash, context_dirty_since, context_dirty_resources, context_error, compaction_requested_at
FROM chats_expanded
`
type UpdateChatWorkspaceBindingParams struct {
ID uuid.UUID `db:"id" json:"id"`
WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"`
BuildID uuid.NullUUID `db:"build_id" json:"build_id"`
AgentID uuid.NullUUID `db:"agent_id" json:"agent_id"`
}
func (q *sqlQuerier) UpdateChatWorkspaceBinding(ctx context.Context, arg UpdateChatWorkspaceBindingParams) (Chat, error) {
row := q.db.QueryRowContext(ctx, updateChatWorkspaceBinding,
arg.ID,
arg.WorkspaceID,
arg.BuildID,
arg.AgentID,
)
var i Chat
err := row.Scan(
&i.ID,
&i.OwnerID,
&i.WorkspaceID,
&i.Title,
&i.Status,
&i.WorkerID,
&i.StartedAt,
&i.HeartbeatAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ParentChatID,
&i.RootChatID,
&i.LastModelConfigID,
&i.LastReasoningEffort,
&i.Archived,
&i.LastError,
&i.Mode,
pq.Array(&i.MCPServerIDs),
&i.Labels,
&i.BuildID,
&i.AgentID,
&i.PinOrder,
&i.LastReadMessageID,
&i.DynamicTools,
&i.OrganizationID,
&i.PlanMode,
&i.ClientType,
&i.LastTurnSummary,
&i.Summary,
&i.SummaryGeneratedAt,
&i.SnapshotVersion,
&i.HistoryVersion,
&i.QueueVersion,
&i.GenerationAttempt,
&i.RetryState,
&i.RetryStateVersion,
&i.RunnerID,
&i.RequiresActionDeadlineAt,
&i.UserACL,
&i.GroupACL,
&i.OwnerUsername,
&i.OwnerName,
&i.ContextAggregateHash,
&i.ContextDirtySince,
&i.ContextDirtyResources,
&i.ContextError,
&i.CompactionRequestedAt,
)
return i, err
}
const upsertChatDiffStatus = `-- name: UpsertChatDiffStatus :one
INSERT INTO chat_diff_statuses (
chat_id,
url,
pull_request_state,
pull_request_title,
pull_request_draft,
changes_requested,
additions,
deletions,
changed_files,
author_login,
author_avatar_url,
base_branch,
head_branch,
pr_number,
commits,
approved,
reviewer_count,
refreshed_at,
stale_at
) VALUES (
$1::uuid,
$2::text,
$3::text,
$4::text,
$5::boolean,
$6::boolean,
$7::integer,
$8::integer,
$9::integer,
$10::text,
$11::text,
$12::text,
$13::text,
$14::integer,
$15::integer,
$16::boolean,
$17::integer,
$18::timestamptz,
$19::timestamptz
)
ON CONFLICT (chat_id) DO UPDATE
SET
url = EXCLUDED.url,
pull_request_state = EXCLUDED.pull_request_state,
pull_request_title = EXCLUDED.pull_request_title,
pull_request_draft = EXCLUDED.pull_request_draft,
changes_requested = EXCLUDED.changes_requested,
additions = EXCLUDED.additions,
deletions = EXCLUDED.deletions,
changed_files = EXCLUDED.changed_files,
author_login = EXCLUDED.author_login,
author_avatar_url = EXCLUDED.author_avatar_url,
base_branch = EXCLUDED.base_branch,
head_branch = EXCLUDED.head_branch,
pr_number = EXCLUDED.pr_number,
commits = EXCLUDED.commits,
approved = EXCLUDED.approved,
reviewer_count = EXCLUDED.reviewer_count,
refreshed_at = EXCLUDED.refreshed_at,
stale_at = EXCLUDED.stale_at,
updated_at = NOW()
RETURNING
chat_id, url, pull_request_state, changes_requested, additions, deletions, changed_files, refreshed_at, stale_at, created_at, updated_at, git_branch, git_remote_origin, pull_request_title, pull_request_draft, author_login, author_avatar_url, base_branch, pr_number, commits, approved, reviewer_count, head_branch
`
type UpsertChatDiffStatusParams struct {
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
Url sql.NullString `db:"url" json:"url"`
PullRequestState sql.NullString `db:"pull_request_state" json:"pull_request_state"`
PullRequestTitle string `db:"pull_request_title" json:"pull_request_title"`
PullRequestDraft bool `db:"pull_request_draft" json:"pull_request_draft"`
ChangesRequested bool `db:"changes_requested" json:"changes_requested"`
Additions int32 `db:"additions" json:"additions"`
Deletions int32 `db:"deletions" json:"deletions"`
ChangedFiles int32 `db:"changed_files" json:"changed_files"`
AuthorLogin sql.NullString `db:"author_login" json:"author_login"`
AuthorAvatarUrl sql.NullString `db:"author_avatar_url" json:"author_avatar_url"`
BaseBranch sql.NullString `db:"base_branch" json:"base_branch"`
HeadBranch sql.NullString `db:"head_branch" json:"head_branch"`
PrNumber sql.NullInt32 `db:"pr_number" json:"pr_number"`
Commits sql.NullInt32 `db:"commits" json:"commits"`
Approved sql.NullBool `db:"approved" json:"approved"`
ReviewerCount sql.NullInt32 `db:"reviewer_count" json:"reviewer_count"`
RefreshedAt time.Time `db:"refreshed_at" json:"refreshed_at"`
StaleAt time.Time `db:"stale_at" json:"stale_at"`
}
func (q *sqlQuerier) UpsertChatDiffStatus(ctx context.Context, arg UpsertChatDiffStatusParams) (ChatDiffStatus, error) {
row := q.db.QueryRowContext(ctx, upsertChatDiffStatus,
arg.ChatID,
arg.Url,
arg.PullRequestState,
arg.PullRequestTitle,
arg.PullRequestDraft,
arg.ChangesRequested,
arg.Additions,
arg.Deletions,
arg.ChangedFiles,
arg.AuthorLogin,
arg.AuthorAvatarUrl,
arg.BaseBranch,
arg.HeadBranch,
arg.PrNumber,
arg.Commits,
arg.Approved,
arg.ReviewerCount,
arg.RefreshedAt,
arg.StaleAt,
)
var i ChatDiffStatus
err := row.Scan(
&i.ChatID,
&i.Url,
&i.PullRequestState,
&i.ChangesRequested,
&i.Additions,
&i.Deletions,
&i.ChangedFiles,
&i.RefreshedAt,
&i.StaleAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.GitBranch,
&i.GitRemoteOrigin,
&i.PullRequestTitle,
&i.PullRequestDraft,
&i.AuthorLogin,
&i.AuthorAvatarUrl,
&i.BaseBranch,
&i.PrNumber,
&i.Commits,
&i.Approved,
&i.ReviewerCount,
&i.HeadBranch,
)
return i, err
}
const upsertChatDiffStatusReference = `-- name: UpsertChatDiffStatusReference :one
INSERT INTO chat_diff_statuses (
chat_id,
url,
git_branch,
git_remote_origin,
stale_at
) VALUES (
$1::uuid,
$2::text,
$3::text,
$4::text,
$5::timestamptz
)
ON CONFLICT (chat_id) DO UPDATE
SET
url = CASE
WHEN EXCLUDED.url IS NOT NULL THEN EXCLUDED.url
ELSE chat_diff_statuses.url
END,
git_branch = CASE
WHEN EXCLUDED.git_branch != '' THEN EXCLUDED.git_branch
ELSE chat_diff_statuses.git_branch
END,
git_remote_origin = CASE
WHEN EXCLUDED.git_remote_origin != '' THEN EXCLUDED.git_remote_origin
ELSE chat_diff_statuses.git_remote_origin
END,
stale_at = EXCLUDED.stale_at,
updated_at = NOW()
RETURNING
chat_id, url, pull_request_state, changes_requested, additions, deletions, changed_files, refreshed_at, stale_at, created_at, updated_at, git_branch, git_remote_origin, pull_request_title, pull_request_draft, author_login, author_avatar_url, base_branch, pr_number, commits, approved, reviewer_count, head_branch
`
type UpsertChatDiffStatusReferenceParams struct {
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
Url sql.NullString `db:"url" json:"url"`
GitBranch string `db:"git_branch" json:"git_branch"`
GitRemoteOrigin string `db:"git_remote_origin" json:"git_remote_origin"`
StaleAt time.Time `db:"stale_at" json:"stale_at"`
}
func (q *sqlQuerier) UpsertChatDiffStatusReference(ctx context.Context, arg UpsertChatDiffStatusReferenceParams) (ChatDiffStatus, error) {
row := q.db.QueryRowContext(ctx, upsertChatDiffStatusReference,
arg.ChatID,
arg.Url,
arg.GitBranch,
arg.GitRemoteOrigin,
arg.StaleAt,
)
var i ChatDiffStatus
err := row.Scan(
&i.ChatID,
&i.Url,
&i.PullRequestState,
&i.ChangesRequested,
&i.Additions,
&i.Deletions,
&i.ChangedFiles,
&i.RefreshedAt,
&i.StaleAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.GitBranch,
&i.GitRemoteOrigin,
&i.PullRequestTitle,
&i.PullRequestDraft,
&i.AuthorLogin,
&i.AuthorAvatarUrl,
&i.BaseBranch,
&i.PrNumber,
&i.Commits,
&i.Approved,
&i.ReviewerCount,
&i.HeadBranch,
)
return i, err
}
const upsertChatHeartbeat = `-- name: UpsertChatHeartbeat :exec
INSERT INTO chat_heartbeats (chat_id, runner_id, heartbeat_at)
VALUES ($1::uuid, $2::uuid, NOW())
ON CONFLICT (chat_id, runner_id) DO UPDATE
SET heartbeat_at = EXCLUDED.heartbeat_at
`
type UpsertChatHeartbeatParams struct {
ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
RunnerID uuid.UUID `db:"runner_id" json:"runner_id"`
}
// Upserts a heartbeat row for the (chat_id, runner_id) lease. Uses
// database time so callers do not depend on a local clock.
func (q *sqlQuerier) UpsertChatHeartbeat(ctx context.Context, arg UpsertChatHeartbeatParams) error {
_, err := q.db.ExecContext(ctx, upsertChatHeartbeat, arg.ChatID, arg.RunnerID)
return err
}
const batchUpsertConnectionLogs = `-- name: BatchUpsertConnectionLogs :exec
INSERT INTO connection_logs (
id, connect_time, organization_id, workspace_owner_id, workspace_id,
workspace_name, agent_name, type, code, ip, user_agent, user_id,
slug_or_port, connection_id, disconnect_reason, disconnect_time
)
SELECT
u.id,
u.connect_time,
u.organization_id,
u.workspace_owner_id,
u.workspace_id,
u.workspace_name,
u.agent_name,
u.type,
-- Use the validity flag to distinguish "no code" (NULL) from a
-- legitimate zero exit code.
CASE WHEN u.code_valid THEN u.code ELSE NULL END,
u.ip,
NULLIF(u.user_agent, ''),
NULLIF(u.user_id, '00000000-0000-0000-0000-000000000000'::uuid),
NULLIF(u.slug_or_port, ''),
NULLIF(u.connection_id, '00000000-0000-0000-0000-000000000000'::uuid),
NULLIF(u.disconnect_reason, ''),
NULLIF(u.disconnect_time, '0001-01-01 00:00:00Z'::timestamptz)
FROM (
SELECT
unnest($1::uuid[]) AS id,
unnest($2::timestamptz[]) AS connect_time,
unnest($3::uuid[]) AS organization_id,
unnest($4::uuid[]) AS workspace_owner_id,
unnest($5::uuid[]) AS workspace_id,
unnest($6::text[]) AS workspace_name,
unnest($7::text[]) AS agent_name,
unnest($8::connection_type[]) AS type,
unnest($9::int4[]) AS code,
unnest($10::bool[]) AS code_valid,
unnest($11::inet[]) AS ip,
unnest($12::text[]) AS user_agent,
unnest($13::uuid[]) AS user_id,
unnest($14::text[]) AS slug_or_port,
unnest($15::uuid[]) AS connection_id,
unnest($16::text[]) AS disconnect_reason,
unnest($17::timestamptz[]) AS disconnect_time
) AS u
ON CONFLICT (connection_id, workspace_id, agent_name)
DO UPDATE SET
-- Pick the earliest real connect_time. The zero sentinel
-- ('0001-01-01') means the batch didn't know the connect_time
-- (e.g. a pure disconnect event), so we keep the existing value.
connect_time = CASE
WHEN EXCLUDED.connect_time = '0001-01-01 00:00:00Z'::timestamptz
THEN connection_logs.connect_time
WHEN connection_logs.connect_time = '0001-01-01 00:00:00Z'::timestamptz
THEN EXCLUDED.connect_time
ELSE LEAST(connection_logs.connect_time, EXCLUDED.connect_time)
END,
disconnect_time = CASE
WHEN connection_logs.disconnect_time IS NULL
THEN EXCLUDED.disconnect_time
ELSE connection_logs.disconnect_time
END,
disconnect_reason = CASE
WHEN connection_logs.disconnect_reason IS NULL
THEN EXCLUDED.disconnect_reason
ELSE connection_logs.disconnect_reason
END,
code = CASE
WHEN connection_logs.code IS NULL
THEN EXCLUDED.code
ELSE connection_logs.code
END
`
type BatchUpsertConnectionLogsParams struct {
ID []uuid.UUID `db:"id" json:"id"`
ConnectTime []time.Time `db:"connect_time" json:"connect_time"`
OrganizationID []uuid.UUID `db:"organization_id" json:"organization_id"`
WorkspaceOwnerID []uuid.UUID `db:"workspace_owner_id" json:"workspace_owner_id"`
WorkspaceID []uuid.UUID `db:"workspace_id" json:"workspace_id"`
WorkspaceName []string `db:"workspace_name" json:"workspace_name"`
AgentName []string `db:"agent_name" json:"agent_name"`
Type []ConnectionType `db:"type" json:"type"`
Code []int32 `db:"code" json:"code"`
CodeValid []bool `db:"code_valid" json:"code_valid"`
Ip []pqtype.Inet `db:"ip" json:"ip"`
UserAgent []string `db:"user_agent" json:"user_agent"`
UserID []uuid.UUID `db:"user_id" json:"user_id"`
SlugOrPort []string `db:"slug_or_port" json:"slug_or_port"`
ConnectionID []uuid.UUID `db:"connection_id" json:"connection_id"`
DisconnectReason []string `db:"disconnect_reason" json:"disconnect_reason"`
DisconnectTime []time.Time `db:"disconnect_time" json:"disconnect_time"`
}
func (q *sqlQuerier) BatchUpsertConnectionLogs(ctx context.Context, arg BatchUpsertConnectionLogsParams) error {
_, err := q.db.ExecContext(ctx, batchUpsertConnectionLogs,
pq.Array(arg.ID),
pq.Array(arg.ConnectTime),
pq.Array(arg.OrganizationID),
pq.Array(arg.WorkspaceOwnerID),
pq.Array(arg.WorkspaceID),
pq.Array(arg.WorkspaceName),
pq.Array(arg.AgentName),
pq.Array(arg.Type),
pq.Array(arg.Code),
pq.Array(arg.CodeValid),
pq.Array(arg.Ip),
pq.Array(arg.UserAgent),
pq.Array(arg.UserID),
pq.Array(arg.SlugOrPort),
pq.Array(arg.ConnectionID),
pq.Array(arg.DisconnectReason),
pq.Array(arg.DisconnectTime),
)
return err
}
const countConnectionLogs = `-- name: CountConnectionLogs :one
SELECT COUNT(*) AS count FROM (
SELECT 1
FROM
connection_logs
JOIN users AS workspace_owner ON
connection_logs.workspace_owner_id = workspace_owner.id
LEFT JOIN users ON
connection_logs.user_id = users.id
JOIN organizations ON
connection_logs.organization_id = organizations.id
WHERE
-- Filter organization_id
CASE
WHEN $1 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
connection_logs.organization_id = $1
ELSE true
END
-- Filter by workspace owner username
AND CASE
WHEN $2 :: text != '' THEN
workspace_owner_id = (
SELECT id FROM users
WHERE lower(username) = lower($2) AND deleted = false
)
ELSE true
END
-- Filter by workspace_owner_id
AND CASE
WHEN $3 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
workspace_owner_id = $3
ELSE true
END
-- Filter by workspace_owner_email
AND CASE
WHEN $4 :: text != '' THEN
workspace_owner_id = (
SELECT id FROM users
WHERE email = $4 AND deleted = false
)
ELSE true
END
-- Filter by type
AND CASE
WHEN $5 :: text != '' THEN
type = $5 :: connection_type
ELSE true
END
-- Filter by user_id
AND CASE
WHEN $6 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
user_id = $6
ELSE true
END
-- Filter by username
AND CASE
WHEN $7 :: text != '' THEN
user_id = (
SELECT id FROM users
WHERE lower(username) = lower($7) AND deleted = false
)
ELSE true
END
-- Filter by user_email
AND CASE
WHEN $8 :: text != '' THEN
users.email = $8
ELSE true
END
-- Filter by connected_after
AND CASE
WHEN $9 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
connect_time >= $9
ELSE true
END
-- Filter by connected_before
AND CASE
WHEN $10 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
connect_time <= $10
ELSE true
END
-- Filter by workspace_id
AND CASE
WHEN $11 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
connection_logs.workspace_id = $11
ELSE true
END
-- Filter by connection_id
AND CASE
WHEN $12 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
connection_logs.connection_id = $12
ELSE true
END
-- Filter by whether the session has a disconnect_time
AND CASE
WHEN $13 :: text != '' THEN
(($13 = 'ongoing' AND disconnect_time IS NULL) OR
($13 = 'completed' AND disconnect_time IS NOT NULL)) AND
-- Exclude point-in-time events reported by coderd, since we
-- don't know their close time.
"type" NOT IN ('workspace_app', 'port_forwarding', 'tunnel')
ELSE true
END
-- Authorize Filter clause will be injected below in
-- CountAuthorizedConnectionLogs
-- @authorize_filter
-- NOTE: See the CountAuditLogs LIMIT note.
LIMIT NULLIF($14::int, 0) + 1
) AS limited_count
`
type CountConnectionLogsParams struct {
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
WorkspaceOwner string `db:"workspace_owner" json:"workspace_owner"`
WorkspaceOwnerID uuid.UUID `db:"workspace_owner_id" json:"workspace_owner_id"`
WorkspaceOwnerEmail string `db:"workspace_owner_email" json:"workspace_owner_email"`
Type string `db:"type" json:"type"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
Username string `db:"username" json:"username"`
UserEmail string `db:"user_email" json:"user_email"`
ConnectedAfter time.Time `db:"connected_after" json:"connected_after"`
ConnectedBefore time.Time `db:"connected_before" json:"connected_before"`
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
ConnectionID uuid.UUID `db:"connection_id" json:"connection_id"`
Status string `db:"status" json:"status"`
CountCap int32 `db:"count_cap" json:"count_cap"`
}
func (q *sqlQuerier) CountConnectionLogs(ctx context.Context, arg CountConnectionLogsParams) (int64, error) {
row := q.db.QueryRowContext(ctx, countConnectionLogs,
arg.OrganizationID,
arg.WorkspaceOwner,
arg.WorkspaceOwnerID,
arg.WorkspaceOwnerEmail,
arg.Type,
arg.UserID,
arg.Username,
arg.UserEmail,
arg.ConnectedAfter,
arg.ConnectedBefore,
arg.WorkspaceID,
arg.ConnectionID,
arg.Status,
arg.CountCap,
)
var count int64
err := row.Scan(&count)
return count, err
}
const deleteOldConnectionLogs = `-- name: DeleteOldConnectionLogs :execrows
WITH old_logs AS (
SELECT id
FROM connection_logs
WHERE connect_time < $1::timestamp with time zone
ORDER BY connect_time ASC
LIMIT $2
)
DELETE FROM connection_logs
USING old_logs
WHERE connection_logs.id = old_logs.id
`
type DeleteOldConnectionLogsParams struct {
BeforeTime time.Time `db:"before_time" json:"before_time"`
LimitCount int32 `db:"limit_count" json:"limit_count"`
}
func (q *sqlQuerier) DeleteOldConnectionLogs(ctx context.Context, arg DeleteOldConnectionLogsParams) (int64, error) {
result, err := q.db.ExecContext(ctx, deleteOldConnectionLogs, arg.BeforeTime, arg.LimitCount)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const getConnectionLogsOffset = `-- name: GetConnectionLogsOffset :many
SELECT
connection_logs.id, connection_logs.connect_time, connection_logs.organization_id, connection_logs.workspace_owner_id, connection_logs.workspace_id, connection_logs.workspace_name, connection_logs.agent_name, connection_logs.type, connection_logs.ip, connection_logs.code, connection_logs.user_agent, connection_logs.user_id, connection_logs.slug_or_port, connection_logs.connection_id, connection_logs.disconnect_time, connection_logs.disconnect_reason,
-- sqlc.embed(users) would be nice but it does not seem to play well with
-- left joins. This user metadata is necessary for parity with the audit logs
-- API.
users.username AS user_username,
users.name AS user_name,
users.email AS user_email,
users.created_at AS user_created_at,
users.updated_at AS user_updated_at,
users.last_seen_at AS user_last_seen_at,
users.status AS user_status,
users.login_type AS user_login_type,
users.rbac_roles AS user_roles,
users.avatar_url AS user_avatar_url,
users.deleted AS user_deleted,
users.quiet_hours_schedule AS user_quiet_hours_schedule,
workspace_owner.username AS workspace_owner_username,
organizations.name AS organization_name,
organizations.display_name AS organization_display_name,
organizations.icon AS organization_icon
FROM
connection_logs
JOIN users AS workspace_owner ON
connection_logs.workspace_owner_id = workspace_owner.id
LEFT JOIN users ON
connection_logs.user_id = users.id
JOIN organizations ON
connection_logs.organization_id = organizations.id
WHERE
-- Filter organization_id
CASE
WHEN $1 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
connection_logs.organization_id = $1
ELSE true
END
-- Filter by workspace owner username
AND CASE
WHEN $2 :: text != '' THEN
workspace_owner_id = (
SELECT id FROM users
WHERE lower(username) = lower($2) AND deleted = false
)
ELSE true
END
-- Filter by workspace_owner_id
AND CASE
WHEN $3 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
workspace_owner_id = $3
ELSE true
END
-- Filter by workspace_owner_email
AND CASE
WHEN $4 :: text != '' THEN
workspace_owner_id = (
SELECT id FROM users
WHERE email = $4 AND deleted = false
)
ELSE true
END
-- Filter by type
AND CASE
WHEN $5 :: text != '' THEN
type = $5 :: connection_type
ELSE true
END
-- Filter by user_id
AND CASE
WHEN $6 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
user_id = $6
ELSE true
END
-- Filter by username
AND CASE
WHEN $7 :: text != '' THEN
user_id = (
SELECT id FROM users
WHERE lower(username) = lower($7) AND deleted = false
)
ELSE true
END
-- Filter by user_email
AND CASE
WHEN $8 :: text != '' THEN
users.email = $8
ELSE true
END
-- Filter by connected_after
AND CASE
WHEN $9 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
connect_time >= $9
ELSE true
END
-- Filter by connected_before
AND CASE
WHEN $10 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
connect_time <= $10
ELSE true
END
-- Filter by workspace_id
AND CASE
WHEN $11 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
connection_logs.workspace_id = $11
ELSE true
END
-- Filter by connection_id
AND CASE
WHEN $12 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
connection_logs.connection_id = $12
ELSE true
END
-- Filter by whether the session has a disconnect_time
AND CASE
WHEN $13 :: text != '' THEN
(($13 = 'ongoing' AND disconnect_time IS NULL) OR
($13 = 'completed' AND disconnect_time IS NOT NULL)) AND
-- Exclude point-in-time events reported by coderd, since we
-- don't know their close time.
"type" NOT IN ('workspace_app', 'port_forwarding', 'tunnel')
ELSE true
END
-- Authorize Filter clause will be injected below in
-- GetAuthorizedConnectionLogsOffset
-- @authorize_filter
ORDER BY
connect_time DESC
LIMIT
-- a limit of 0 means "no limit". The connection log table is unbounded
-- in size, and is expected to be quite large. Implement a default
-- limit of 100 to prevent accidental excessively large queries.
COALESCE(NULLIF($15 :: int, 0), 100)
OFFSET
$14
`
type GetConnectionLogsOffsetParams struct {
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
WorkspaceOwner string `db:"workspace_owner" json:"workspace_owner"`
WorkspaceOwnerID uuid.UUID `db:"workspace_owner_id" json:"workspace_owner_id"`
WorkspaceOwnerEmail string `db:"workspace_owner_email" json:"workspace_owner_email"`
Type string `db:"type" json:"type"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
Username string `db:"username" json:"username"`
UserEmail string `db:"user_email" json:"user_email"`
ConnectedAfter time.Time `db:"connected_after" json:"connected_after"`
ConnectedBefore time.Time `db:"connected_before" json:"connected_before"`
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
ConnectionID uuid.UUID `db:"connection_id" json:"connection_id"`
Status string `db:"status" json:"status"`
OffsetOpt int32 `db:"offset_opt" json:"offset_opt"`
LimitOpt int32 `db:"limit_opt" json:"limit_opt"`
}
type GetConnectionLogsOffsetRow struct {
ConnectionLog ConnectionLog `db:"connection_log" json:"connection_log"`
UserUsername sql.NullString `db:"user_username" json:"user_username"`
UserName sql.NullString `db:"user_name" json:"user_name"`
UserEmail sql.NullString `db:"user_email" json:"user_email"`
UserCreatedAt sql.NullTime `db:"user_created_at" json:"user_created_at"`
UserUpdatedAt sql.NullTime `db:"user_updated_at" json:"user_updated_at"`
UserLastSeenAt sql.NullTime `db:"user_last_seen_at" json:"user_last_seen_at"`
UserStatus NullUserStatus `db:"user_status" json:"user_status"`
UserLoginType NullLoginType `db:"user_login_type" json:"user_login_type"`
UserRoles pq.StringArray `db:"user_roles" json:"user_roles"`
UserAvatarUrl sql.NullString `db:"user_avatar_url" json:"user_avatar_url"`
UserDeleted sql.NullBool `db:"user_deleted" json:"user_deleted"`
UserQuietHoursSchedule sql.NullString `db:"user_quiet_hours_schedule" json:"user_quiet_hours_schedule"`
WorkspaceOwnerUsername string `db:"workspace_owner_username" json:"workspace_owner_username"`
OrganizationName string `db:"organization_name" json:"organization_name"`
OrganizationDisplayName string `db:"organization_display_name" json:"organization_display_name"`
OrganizationIcon string `db:"organization_icon" json:"organization_icon"`
}
func (q *sqlQuerier) GetConnectionLogsOffset(ctx context.Context, arg GetConnectionLogsOffsetParams) ([]GetConnectionLogsOffsetRow, error) {
rows, err := q.db.QueryContext(ctx, getConnectionLogsOffset,
arg.OrganizationID,
arg.WorkspaceOwner,
arg.WorkspaceOwnerID,
arg.WorkspaceOwnerEmail,
arg.Type,
arg.UserID,
arg.Username,
arg.UserEmail,
arg.ConnectedAfter,
arg.ConnectedBefore,
arg.WorkspaceID,
arg.ConnectionID,
arg.Status,
arg.OffsetOpt,
arg.LimitOpt,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetConnectionLogsOffsetRow
for rows.Next() {
var i GetConnectionLogsOffsetRow
if err := rows.Scan(
&i.ConnectionLog.ID,
&i.ConnectionLog.ConnectTime,
&i.ConnectionLog.OrganizationID,
&i.ConnectionLog.WorkspaceOwnerID,
&i.ConnectionLog.WorkspaceID,
&i.ConnectionLog.WorkspaceName,
&i.ConnectionLog.AgentName,
&i.ConnectionLog.Type,
&i.ConnectionLog.Ip,
&i.ConnectionLog.Code,
&i.ConnectionLog.UserAgent,
&i.ConnectionLog.UserID,
&i.ConnectionLog.SlugOrPort,
&i.ConnectionLog.ConnectionID,
&i.ConnectionLog.DisconnectTime,
&i.ConnectionLog.DisconnectReason,
&i.UserUsername,
&i.UserName,
&i.UserEmail,
&i.UserCreatedAt,
&i.UserUpdatedAt,
&i.UserLastSeenAt,
&i.UserStatus,
&i.UserLoginType,
&i.UserRoles,
&i.UserAvatarUrl,
&i.UserDeleted,
&i.UserQuietHoursSchedule,
&i.WorkspaceOwnerUsername,
&i.OrganizationName,
&i.OrganizationDisplayName,
&i.OrganizationIcon,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const deleteCryptoKey = `-- name: DeleteCryptoKey :one
UPDATE crypto_keys
SET secret = NULL, secret_key_id = NULL
WHERE feature = $1 AND sequence = $2 RETURNING feature, sequence, secret, secret_key_id, starts_at, deletes_at
`
type DeleteCryptoKeyParams struct {
Feature CryptoKeyFeature `db:"feature" json:"feature"`
Sequence int32 `db:"sequence" json:"sequence"`
}
func (q *sqlQuerier) DeleteCryptoKey(ctx context.Context, arg DeleteCryptoKeyParams) (CryptoKey, error) {
row := q.db.QueryRowContext(ctx, deleteCryptoKey, arg.Feature, arg.Sequence)
var i CryptoKey
err := row.Scan(
&i.Feature,
&i.Sequence,
&i.Secret,
&i.SecretKeyID,
&i.StartsAt,
&i.DeletesAt,
)
return i, err
}
const getCryptoKeyByFeatureAndSequence = `-- name: GetCryptoKeyByFeatureAndSequence :one
SELECT feature, sequence, secret, secret_key_id, starts_at, deletes_at
FROM crypto_keys
WHERE feature = $1
AND sequence = $2
AND secret IS NOT NULL
`
type GetCryptoKeyByFeatureAndSequenceParams struct {
Feature CryptoKeyFeature `db:"feature" json:"feature"`
Sequence int32 `db:"sequence" json:"sequence"`
}
func (q *sqlQuerier) GetCryptoKeyByFeatureAndSequence(ctx context.Context, arg GetCryptoKeyByFeatureAndSequenceParams) (CryptoKey, error) {
row := q.db.QueryRowContext(ctx, getCryptoKeyByFeatureAndSequence, arg.Feature, arg.Sequence)
var i CryptoKey
err := row.Scan(
&i.Feature,
&i.Sequence,
&i.Secret,
&i.SecretKeyID,
&i.StartsAt,
&i.DeletesAt,
)
return i, err
}
const getCryptoKeys = `-- name: GetCryptoKeys :many
SELECT feature, sequence, secret, secret_key_id, starts_at, deletes_at
FROM crypto_keys
WHERE secret IS NOT NULL
`
func (q *sqlQuerier) GetCryptoKeys(ctx context.Context) ([]CryptoKey, error) {
rows, err := q.db.QueryContext(ctx, getCryptoKeys)
if err != nil {
return nil, err
}
defer rows.Close()
var items []CryptoKey
for rows.Next() {
var i CryptoKey
if err := rows.Scan(
&i.Feature,
&i.Sequence,
&i.Secret,
&i.SecretKeyID,
&i.StartsAt,
&i.DeletesAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getCryptoKeysByFeature = `-- name: GetCryptoKeysByFeature :many
SELECT feature, sequence, secret, secret_key_id, starts_at, deletes_at
FROM crypto_keys
WHERE feature = $1
AND secret IS NOT NULL
ORDER BY sequence DESC
`
func (q *sqlQuerier) GetCryptoKeysByFeature(ctx context.Context, feature CryptoKeyFeature) ([]CryptoKey, error) {
rows, err := q.db.QueryContext(ctx, getCryptoKeysByFeature, feature)
if err != nil {
return nil, err
}
defer rows.Close()
var items []CryptoKey
for rows.Next() {
var i CryptoKey
if err := rows.Scan(
&i.Feature,
&i.Sequence,
&i.Secret,
&i.SecretKeyID,
&i.StartsAt,
&i.DeletesAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getLatestCryptoKeyByFeature = `-- name: GetLatestCryptoKeyByFeature :one
SELECT feature, sequence, secret, secret_key_id, starts_at, deletes_at
FROM crypto_keys
WHERE feature = $1
ORDER BY sequence DESC
LIMIT 1
`
func (q *sqlQuerier) GetLatestCryptoKeyByFeature(ctx context.Context, feature CryptoKeyFeature) (CryptoKey, error) {
row := q.db.QueryRowContext(ctx, getLatestCryptoKeyByFeature, feature)
var i CryptoKey
err := row.Scan(
&i.Feature,
&i.Sequence,
&i.Secret,
&i.SecretKeyID,
&i.StartsAt,
&i.DeletesAt,
)
return i, err
}
const insertCryptoKey = `-- name: InsertCryptoKey :one
INSERT INTO crypto_keys (
feature,
sequence,
secret,
starts_at,
secret_key_id
) VALUES (
$1,
$2,
$3,
$4,
$5
) RETURNING feature, sequence, secret, secret_key_id, starts_at, deletes_at
`
type InsertCryptoKeyParams struct {
Feature CryptoKeyFeature `db:"feature" json:"feature"`
Sequence int32 `db:"sequence" json:"sequence"`
Secret sql.NullString `db:"secret" json:"secret"`
StartsAt time.Time `db:"starts_at" json:"starts_at"`
SecretKeyID sql.NullString `db:"secret_key_id" json:"secret_key_id"`
}
func (q *sqlQuerier) InsertCryptoKey(ctx context.Context, arg InsertCryptoKeyParams) (CryptoKey, error) {
row := q.db.QueryRowContext(ctx, insertCryptoKey,
arg.Feature,
arg.Sequence,
arg.Secret,
arg.StartsAt,
arg.SecretKeyID,
)
var i CryptoKey
err := row.Scan(
&i.Feature,
&i.Sequence,
&i.Secret,
&i.SecretKeyID,
&i.StartsAt,
&i.DeletesAt,
)
return i, err
}
const updateCryptoKeyDeletesAt = `-- name: UpdateCryptoKeyDeletesAt :one
UPDATE crypto_keys
SET deletes_at = $3
WHERE feature = $1 AND sequence = $2 RETURNING feature, sequence, secret, secret_key_id, starts_at, deletes_at
`
type UpdateCryptoKeyDeletesAtParams struct {
Feature CryptoKeyFeature `db:"feature" json:"feature"`
Sequence int32 `db:"sequence" json:"sequence"`
DeletesAt sql.NullTime `db:"deletes_at" json:"deletes_at"`
}
func (q *sqlQuerier) UpdateCryptoKeyDeletesAt(ctx context.Context, arg UpdateCryptoKeyDeletesAtParams) (CryptoKey, error) {
row := q.db.QueryRowContext(ctx, updateCryptoKeyDeletesAt, arg.Feature, arg.Sequence, arg.DeletesAt)
var i CryptoKey
err := row.Scan(
&i.Feature,
&i.Sequence,
&i.Secret,
&i.SecretKeyID,
&i.StartsAt,
&i.DeletesAt,
)
return i, err
}
const getDBCryptKeys = `-- name: GetDBCryptKeys :many
SELECT number, active_key_digest, revoked_key_digest, created_at, revoked_at, test FROM dbcrypt_keys ORDER BY number ASC
`
func (q *sqlQuerier) GetDBCryptKeys(ctx context.Context) ([]DBCryptKey, error) {
rows, err := q.db.QueryContext(ctx, getDBCryptKeys)
if err != nil {
return nil, err
}
defer rows.Close()
var items []DBCryptKey
for rows.Next() {
var i DBCryptKey
if err := rows.Scan(
&i.Number,
&i.ActiveKeyDigest,
&i.RevokedKeyDigest,
&i.CreatedAt,
&i.RevokedAt,
&i.Test,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertDBCryptKey = `-- name: InsertDBCryptKey :exec
INSERT INTO dbcrypt_keys
(number, active_key_digest, created_at, test)
VALUES ($1::int, $2::text, CURRENT_TIMESTAMP, $3::text)
`
type InsertDBCryptKeyParams struct {
Number int32 `db:"number" json:"number"`
ActiveKeyDigest string `db:"active_key_digest" json:"active_key_digest"`
Test string `db:"test" json:"test"`
}
func (q *sqlQuerier) InsertDBCryptKey(ctx context.Context, arg InsertDBCryptKeyParams) error {
_, err := q.db.ExecContext(ctx, insertDBCryptKey, arg.Number, arg.ActiveKeyDigest, arg.Test)
return err
}
const revokeDBCryptKey = `-- name: RevokeDBCryptKey :exec
UPDATE dbcrypt_keys
SET
revoked_key_digest = active_key_digest,
active_key_digest = revoked_key_digest,
revoked_at = CURRENT_TIMESTAMP
WHERE
active_key_digest = $1::text
AND
revoked_key_digest IS NULL
`
func (q *sqlQuerier) RevokeDBCryptKey(ctx context.Context, activeKeyDigest string) error {
_, err := q.db.ExecContext(ctx, revokeDBCryptKey, activeKeyDigest)
return err
}
const deleteExternalAuthLink = `-- name: DeleteExternalAuthLink :exec
DELETE FROM external_auth_links WHERE provider_id = $1 AND user_id = $2
`
type DeleteExternalAuthLinkParams struct {
ProviderID string `db:"provider_id" json:"provider_id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
}
func (q *sqlQuerier) DeleteExternalAuthLink(ctx context.Context, arg DeleteExternalAuthLinkParams) error {
_, err := q.db.ExecContext(ctx, deleteExternalAuthLink, arg.ProviderID, arg.UserID)
return err
}
const getExternalAuthLink = `-- name: GetExternalAuthLink :one
SELECT provider_id, user_id, created_at, updated_at, oauth_access_token, oauth_refresh_token, oauth_expiry, oauth_access_token_key_id, oauth_refresh_token_key_id, oauth_extra, oauth_refresh_failure_reason FROM external_auth_links WHERE provider_id = $1 AND user_id = $2
`
type GetExternalAuthLinkParams struct {
ProviderID string `db:"provider_id" json:"provider_id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
}
func (q *sqlQuerier) GetExternalAuthLink(ctx context.Context, arg GetExternalAuthLinkParams) (ExternalAuthLink, error) {
row := q.db.QueryRowContext(ctx, getExternalAuthLink, arg.ProviderID, arg.UserID)
var i ExternalAuthLink
err := row.Scan(
&i.ProviderID,
&i.UserID,
&i.CreatedAt,
&i.UpdatedAt,
&i.OAuthAccessToken,
&i.OAuthRefreshToken,
&i.OAuthExpiry,
&i.OAuthAccessTokenKeyID,
&i.OAuthRefreshTokenKeyID,
&i.OAuthExtra,
&i.OauthRefreshFailureReason,
)
return i, err
}
const getExternalAuthLinksByUserID = `-- name: GetExternalAuthLinksByUserID :many
SELECT provider_id, user_id, created_at, updated_at, oauth_access_token, oauth_refresh_token, oauth_expiry, oauth_access_token_key_id, oauth_refresh_token_key_id, oauth_extra, oauth_refresh_failure_reason FROM external_auth_links WHERE user_id = $1
`
func (q *sqlQuerier) GetExternalAuthLinksByUserID(ctx context.Context, userID uuid.UUID) ([]ExternalAuthLink, error) {
rows, err := q.db.QueryContext(ctx, getExternalAuthLinksByUserID, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ExternalAuthLink
for rows.Next() {
var i ExternalAuthLink
if err := rows.Scan(
&i.ProviderID,
&i.UserID,
&i.CreatedAt,
&i.UpdatedAt,
&i.OAuthAccessToken,
&i.OAuthRefreshToken,
&i.OAuthExpiry,
&i.OAuthAccessTokenKeyID,
&i.OAuthRefreshTokenKeyID,
&i.OAuthExtra,
&i.OauthRefreshFailureReason,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertExternalAuthLink = `-- name: InsertExternalAuthLink :one
INSERT INTO external_auth_links (
provider_id,
user_id,
created_at,
updated_at,
oauth_access_token,
oauth_access_token_key_id,
oauth_refresh_token,
oauth_refresh_token_key_id,
oauth_expiry,
oauth_extra
) VALUES (
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
$9,
$10
) RETURNING provider_id, user_id, created_at, updated_at, oauth_access_token, oauth_refresh_token, oauth_expiry, oauth_access_token_key_id, oauth_refresh_token_key_id, oauth_extra, oauth_refresh_failure_reason
`
type InsertExternalAuthLinkParams struct {
ProviderID string `db:"provider_id" json:"provider_id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
OAuthAccessToken string `db:"oauth_access_token" json:"oauth_access_token"`
OAuthAccessTokenKeyID sql.NullString `db:"oauth_access_token_key_id" json:"oauth_access_token_key_id"`
OAuthRefreshToken string `db:"oauth_refresh_token" json:"oauth_refresh_token"`
OAuthRefreshTokenKeyID sql.NullString `db:"oauth_refresh_token_key_id" json:"oauth_refresh_token_key_id"`
OAuthExpiry time.Time `db:"oauth_expiry" json:"oauth_expiry"`
OAuthExtra pqtype.NullRawMessage `db:"oauth_extra" json:"oauth_extra"`
}
func (q *sqlQuerier) InsertExternalAuthLink(ctx context.Context, arg InsertExternalAuthLinkParams) (ExternalAuthLink, error) {
row := q.db.QueryRowContext(ctx, insertExternalAuthLink,
arg.ProviderID,
arg.UserID,
arg.CreatedAt,
arg.UpdatedAt,
arg.OAuthAccessToken,
arg.OAuthAccessTokenKeyID,
arg.OAuthRefreshToken,
arg.OAuthRefreshTokenKeyID,
arg.OAuthExpiry,
arg.OAuthExtra,
)
var i ExternalAuthLink
err := row.Scan(
&i.ProviderID,
&i.UserID,
&i.CreatedAt,
&i.UpdatedAt,
&i.OAuthAccessToken,
&i.OAuthRefreshToken,
&i.OAuthExpiry,
&i.OAuthAccessTokenKeyID,
&i.OAuthRefreshTokenKeyID,
&i.OAuthExtra,
&i.OauthRefreshFailureReason,
)
return i, err
}
const updateExternalAuthLink = `-- name: UpdateExternalAuthLink :one
UPDATE external_auth_links SET
updated_at = $3,
oauth_access_token = $4,
oauth_access_token_key_id = $5,
oauth_refresh_token = $6,
oauth_refresh_token_key_id = $7,
oauth_expiry = $8,
oauth_extra = $9,
-- Only 'UpdateExternalAuthLinkRefreshToken' supports updating the oauth_refresh_failure_reason.
-- Any updates to the external auth link, will be assumed to change the state and clear
-- any cached errors.
oauth_refresh_failure_reason = ''
WHERE provider_id = $1 AND user_id = $2 RETURNING provider_id, user_id, created_at, updated_at, oauth_access_token, oauth_refresh_token, oauth_expiry, oauth_access_token_key_id, oauth_refresh_token_key_id, oauth_extra, oauth_refresh_failure_reason
`
type UpdateExternalAuthLinkParams struct {
ProviderID string `db:"provider_id" json:"provider_id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
OAuthAccessToken string `db:"oauth_access_token" json:"oauth_access_token"`
OAuthAccessTokenKeyID sql.NullString `db:"oauth_access_token_key_id" json:"oauth_access_token_key_id"`
OAuthRefreshToken string `db:"oauth_refresh_token" json:"oauth_refresh_token"`
OAuthRefreshTokenKeyID sql.NullString `db:"oauth_refresh_token_key_id" json:"oauth_refresh_token_key_id"`
OAuthExpiry time.Time `db:"oauth_expiry" json:"oauth_expiry"`
OAuthExtra pqtype.NullRawMessage `db:"oauth_extra" json:"oauth_extra"`
}
func (q *sqlQuerier) UpdateExternalAuthLink(ctx context.Context, arg UpdateExternalAuthLinkParams) (ExternalAuthLink, error) {
row := q.db.QueryRowContext(ctx, updateExternalAuthLink,
arg.ProviderID,
arg.UserID,
arg.UpdatedAt,
arg.OAuthAccessToken,
arg.OAuthAccessTokenKeyID,
arg.OAuthRefreshToken,
arg.OAuthRefreshTokenKeyID,
arg.OAuthExpiry,
arg.OAuthExtra,
)
var i ExternalAuthLink
err := row.Scan(
&i.ProviderID,
&i.UserID,
&i.CreatedAt,
&i.UpdatedAt,
&i.OAuthAccessToken,
&i.OAuthRefreshToken,
&i.OAuthExpiry,
&i.OAuthAccessTokenKeyID,
&i.OAuthRefreshTokenKeyID,
&i.OAuthExtra,
&i.OauthRefreshFailureReason,
)
return i, err
}
const updateExternalAuthLinkRefreshToken = `-- name: UpdateExternalAuthLinkRefreshToken :exec
UPDATE
external_auth_links
SET
-- oauth_refresh_failure_reason can be set to cache the failure reason
-- for subsequent refresh attempts.
oauth_refresh_failure_reason = $1,
oauth_refresh_token = $2,
updated_at = $3
WHERE
provider_id = $4
AND
user_id = $5
AND
oauth_refresh_token = $6
AND
-- Required for sqlc to generate a parameter for the oauth_refresh_token_key_id
$7 :: text = $7 :: text
`
type UpdateExternalAuthLinkRefreshTokenParams struct {
OauthRefreshFailureReason string `db:"oauth_refresh_failure_reason" json:"oauth_refresh_failure_reason"`
OAuthRefreshToken string `db:"oauth_refresh_token" json:"oauth_refresh_token"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
ProviderID string `db:"provider_id" json:"provider_id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
OldOauthRefreshToken string `db:"old_oauth_refresh_token" json:"old_oauth_refresh_token"`
OAuthRefreshTokenKeyID string `db:"oauth_refresh_token_key_id" json:"oauth_refresh_token_key_id"`
}
// Optimistic lock: only update the row if the refresh token in the database
// still matches the one we read before attempting the refresh. This prevents
// a concurrent caller that lost a token-refresh race from overwriting a valid
// token stored by the winner.
func (q *sqlQuerier) UpdateExternalAuthLinkRefreshToken(ctx context.Context, arg UpdateExternalAuthLinkRefreshTokenParams) error {
_, err := q.db.ExecContext(ctx, updateExternalAuthLinkRefreshToken,
arg.OauthRefreshFailureReason,
arg.OAuthRefreshToken,
arg.UpdatedAt,
arg.ProviderID,
arg.UserID,
arg.OldOauthRefreshToken,
arg.OAuthRefreshTokenKeyID,
)
return err
}
const getFileByHashAndCreator = `-- name: GetFileByHashAndCreator :one
SELECT
hash, created_at, created_by, mimetype, data, id
FROM
files
WHERE
hash = $1
AND
created_by = $2
LIMIT
1
`
type GetFileByHashAndCreatorParams struct {
Hash string `db:"hash" json:"hash"`
CreatedBy uuid.UUID `db:"created_by" json:"created_by"`
}
func (q *sqlQuerier) GetFileByHashAndCreator(ctx context.Context, arg GetFileByHashAndCreatorParams) (File, error) {
row := q.db.QueryRowContext(ctx, getFileByHashAndCreator, arg.Hash, arg.CreatedBy)
var i File
err := row.Scan(
&i.Hash,
&i.CreatedAt,
&i.CreatedBy,
&i.Mimetype,
&i.Data,
&i.ID,
)
return i, err
}
const getFileByID = `-- name: GetFileByID :one
SELECT
hash, created_at, created_by, mimetype, data, id
FROM
files
WHERE
id = $1
LIMIT
1
`
func (q *sqlQuerier) GetFileByID(ctx context.Context, id uuid.UUID) (File, error) {
row := q.db.QueryRowContext(ctx, getFileByID, id)
var i File
err := row.Scan(
&i.Hash,
&i.CreatedAt,
&i.CreatedBy,
&i.Mimetype,
&i.Data,
&i.ID,
)
return i, err
}
const getFileTemplates = `-- name: GetFileTemplates :many
SELECT
files.id AS file_id,
files.created_by AS file_created_by,
templates.id AS template_id,
templates.organization_id AS template_organization_id,
templates.created_by AS template_created_by,
templates.user_acl,
templates.group_acl
FROM
templates
INNER JOIN
template_versions
ON templates.id = template_versions.template_id
INNER JOIN
provisioner_jobs
ON job_id = provisioner_jobs.id
INNER JOIN
files
ON files.id = provisioner_jobs.file_id
WHERE
-- Only fetch template version associated files.
storage_method = 'file'
AND provisioner_jobs.type = 'template_version_import'
AND file_id = $1
`
type GetFileTemplatesRow struct {
FileID uuid.UUID `db:"file_id" json:"file_id"`
FileCreatedBy uuid.UUID `db:"file_created_by" json:"file_created_by"`
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
TemplateOrganizationID uuid.UUID `db:"template_organization_id" json:"template_organization_id"`
TemplateCreatedBy uuid.UUID `db:"template_created_by" json:"template_created_by"`
UserACL TemplateACL `db:"user_acl" json:"user_acl"`
GroupACL TemplateACL `db:"group_acl" json:"group_acl"`
}
// Get all templates that use a file.
func (q *sqlQuerier) GetFileTemplates(ctx context.Context, fileID uuid.UUID) ([]GetFileTemplatesRow, error) {
rows, err := q.db.QueryContext(ctx, getFileTemplates, fileID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetFileTemplatesRow
for rows.Next() {
var i GetFileTemplatesRow
if err := rows.Scan(
&i.FileID,
&i.FileCreatedBy,
&i.TemplateID,
&i.TemplateOrganizationID,
&i.TemplateCreatedBy,
&i.UserACL,
&i.GroupACL,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertFile = `-- name: InsertFile :one
INSERT INTO
files (id, hash, created_at, created_by, mimetype, "data")
VALUES
($1, $2, $3, $4, $5, $6) RETURNING hash, created_at, created_by, mimetype, data, id
`
type InsertFileParams struct {
ID uuid.UUID `db:"id" json:"id"`
Hash string `db:"hash" json:"hash"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
CreatedBy uuid.UUID `db:"created_by" json:"created_by"`
Mimetype string `db:"mimetype" json:"mimetype"`
Data []byte `db:"data" json:"data"`
}
func (q *sqlQuerier) InsertFile(ctx context.Context, arg InsertFileParams) (File, error) {
row := q.db.QueryRowContext(ctx, insertFile,
arg.ID,
arg.Hash,
arg.CreatedAt,
arg.CreatedBy,
arg.Mimetype,
arg.Data,
)
var i File
err := row.Scan(
&i.Hash,
&i.CreatedAt,
&i.CreatedBy,
&i.Mimetype,
&i.Data,
&i.ID,
)
return i, err
}
const getGitSSHKey = `-- name: GetGitSSHKey :one
SELECT
user_id, created_at, updated_at, private_key, public_key, private_key_key_id
FROM
gitsshkeys
WHERE
user_id = $1
`
func (q *sqlQuerier) GetGitSSHKey(ctx context.Context, userID uuid.UUID) (GitSSHKey, error) {
row := q.db.QueryRowContext(ctx, getGitSSHKey, userID)
var i GitSSHKey
err := row.Scan(
&i.UserID,
&i.CreatedAt,
&i.UpdatedAt,
&i.PrivateKey,
&i.PublicKey,
&i.PrivateKeyKeyID,
)
return i, err
}
const insertGitSSHKey = `-- name: InsertGitSSHKey :one
INSERT INTO
gitsshkeys (
user_id,
created_at,
updated_at,
private_key,
private_key_key_id,
public_key
)
VALUES
($1, $2, $3, $4, $5, $6) RETURNING user_id, created_at, updated_at, private_key, public_key, private_key_key_id
`
type InsertGitSSHKeyParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
PrivateKey string `db:"private_key" json:"private_key"`
PrivateKeyKeyID sql.NullString `db:"private_key_key_id" json:"private_key_key_id"`
PublicKey string `db:"public_key" json:"public_key"`
}
func (q *sqlQuerier) InsertGitSSHKey(ctx context.Context, arg InsertGitSSHKeyParams) (GitSSHKey, error) {
row := q.db.QueryRowContext(ctx, insertGitSSHKey,
arg.UserID,
arg.CreatedAt,
arg.UpdatedAt,
arg.PrivateKey,
arg.PrivateKeyKeyID,
arg.PublicKey,
)
var i GitSSHKey
err := row.Scan(
&i.UserID,
&i.CreatedAt,
&i.UpdatedAt,
&i.PrivateKey,
&i.PublicKey,
&i.PrivateKeyKeyID,
)
return i, err
}
const updateGitSSHKey = `-- name: UpdateGitSSHKey :one
UPDATE
gitsshkeys
SET
updated_at = $2,
private_key = $3,
private_key_key_id = $4,
public_key = $5
WHERE
user_id = $1
RETURNING
user_id, created_at, updated_at, private_key, public_key, private_key_key_id
`
type UpdateGitSSHKeyParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
PrivateKey string `db:"private_key" json:"private_key"`
PrivateKeyKeyID sql.NullString `db:"private_key_key_id" json:"private_key_key_id"`
PublicKey string `db:"public_key" json:"public_key"`
}
func (q *sqlQuerier) UpdateGitSSHKey(ctx context.Context, arg UpdateGitSSHKeyParams) (GitSSHKey, error) {
row := q.db.QueryRowContext(ctx, updateGitSSHKey,
arg.UserID,
arg.UpdatedAt,
arg.PrivateKey,
arg.PrivateKeyKeyID,
arg.PublicKey,
)
var i GitSSHKey
err := row.Scan(
&i.UserID,
&i.CreatedAt,
&i.UpdatedAt,
&i.PrivateKey,
&i.PublicKey,
&i.PrivateKeyKeyID,
)
return i, err
}
const deleteGroupMemberFromGroup = `-- name: DeleteGroupMemberFromGroup :exec
DELETE FROM
group_members
WHERE
user_id = $1 AND
group_id = $2
`
type DeleteGroupMemberFromGroupParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
GroupID uuid.UUID `db:"group_id" json:"group_id"`
}
func (q *sqlQuerier) DeleteGroupMemberFromGroup(ctx context.Context, arg DeleteGroupMemberFromGroupParams) error {
_, err := q.db.ExecContext(ctx, deleteGroupMemberFromGroup, arg.UserID, arg.GroupID)
return err
}
const getGroupMembers = `-- name: GetGroupMembers :many
SELECT user_id, user_email, user_username, user_hashed_password, user_created_at, user_updated_at, user_status, user_rbac_roles, user_login_type, user_avatar_url, user_deleted, user_last_seen_at, user_quiet_hours_schedule, user_name, user_github_com_user_id, user_is_system, user_is_service_account, organization_id, group_name, group_id FROM group_members_expanded
WHERE CASE
WHEN $1::bool THEN TRUE
ELSE
user_is_system = false
END
`
func (q *sqlQuerier) GetGroupMembers(ctx context.Context, includeSystem bool) ([]GroupMember, error) {
rows, err := q.db.QueryContext(ctx, getGroupMembers, includeSystem)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GroupMember
for rows.Next() {
var i GroupMember
if err := rows.Scan(
&i.UserID,
&i.UserEmail,
&i.UserUsername,
&i.UserHashedPassword,
&i.UserCreatedAt,
&i.UserUpdatedAt,
&i.UserStatus,
pq.Array(&i.UserRbacRoles),
&i.UserLoginType,
&i.UserAvatarUrl,
&i.UserDeleted,
&i.UserLastSeenAt,
&i.UserQuietHoursSchedule,
&i.UserName,
&i.UserGithubComUserID,
&i.UserIsSystem,
&i.UserIsServiceAccount,
&i.OrganizationID,
&i.GroupName,
&i.GroupID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getGroupMembersByGroupID = `-- name: GetGroupMembersByGroupID :many
SELECT user_id, user_email, user_username, user_hashed_password, user_created_at, user_updated_at, user_status, user_rbac_roles, user_login_type, user_avatar_url, user_deleted, user_last_seen_at, user_quiet_hours_schedule, user_name, user_github_com_user_id, user_is_system, user_is_service_account, organization_id, group_name, group_id
FROM group_members_expanded
WHERE group_id = $1
-- Filter by system type
AND CASE
WHEN $2::bool THEN TRUE
ELSE
user_is_system = false
END
`
type GetGroupMembersByGroupIDParams struct {
GroupID uuid.UUID `db:"group_id" json:"group_id"`
IncludeSystem bool `db:"include_system" json:"include_system"`
}
func (q *sqlQuerier) GetGroupMembersByGroupID(ctx context.Context, arg GetGroupMembersByGroupIDParams) ([]GroupMember, error) {
rows, err := q.db.QueryContext(ctx, getGroupMembersByGroupID, arg.GroupID, arg.IncludeSystem)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GroupMember
for rows.Next() {
var i GroupMember
if err := rows.Scan(
&i.UserID,
&i.UserEmail,
&i.UserUsername,
&i.UserHashedPassword,
&i.UserCreatedAt,
&i.UserUpdatedAt,
&i.UserStatus,
pq.Array(&i.UserRbacRoles),
&i.UserLoginType,
&i.UserAvatarUrl,
&i.UserDeleted,
&i.UserLastSeenAt,
&i.UserQuietHoursSchedule,
&i.UserName,
&i.UserGithubComUserID,
&i.UserIsSystem,
&i.UserIsServiceAccount,
&i.OrganizationID,
&i.GroupName,
&i.GroupID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getGroupMembersByGroupIDPaginated = `-- name: GetGroupMembersByGroupIDPaginated :many
SELECT
user_id, user_email, user_username, user_hashed_password, user_created_at, user_updated_at, user_status, user_rbac_roles, user_login_type, user_avatar_url, user_deleted, user_last_seen_at, user_quiet_hours_schedule, user_name, user_github_com_user_id, user_is_system, user_is_service_account, organization_id, group_name, group_id, COUNT(*) OVER() AS count
FROM
group_members_expanded
WHERE
group_members_expanded.group_id = $1
AND CASE
-- This allows using the last element on a page as effectively a cursor.
-- This is an important option for scripts that need to paginate without
-- duplicating or missing data.
WHEN $2 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN (
-- The pagination cursor is the last ID of the previous page.
-- The query is ordered by the username field, so select all
-- rows after the cursor.
(LOWER(user_username)) > (
SELECT
LOWER(user_username)
FROM
group_members_expanded
WHERE
group_id = $1
AND user_id = $2
)
)
ELSE true
END
-- Start filters
-- Filter by email, username, or name (display name)
AND CASE
WHEN $3 :: text != '' THEN (
user_email ILIKE concat('%', $3, '%')
OR user_username ILIKE concat('%', $3, '%')
OR user_name ILIKE concat('%', $3, '%')
)
ELSE true
END
-- Filter by name (display name)
AND CASE
WHEN $4 :: text != '' THEN
user_name ILIKE concat('%', $4, '%')
ELSE true
END
-- Filter by exact username
AND CASE
WHEN $5 :: text != '' THEN
lower(user_username) = lower($5)
ELSE true
END
-- Filter by exact email
AND CASE
WHEN $6 :: text != '' THEN
lower(user_email) = lower($6)
ELSE true
END
-- Filter by status
AND CASE
-- @status needs to be a text because it can be empty, If it was
-- user_status enum, it would not.
WHEN cardinality($7 :: user_status[]) > 0 THEN
user_status = ANY($7 :: user_status[])
ELSE true
END
-- Filter by rbac_roles
AND CASE
-- @rbac_role allows filtering by rbac roles. If 'member' is included, show everyone, as
-- everyone is a member.
WHEN cardinality($8 :: text[]) > 0 AND 'member' != ANY($8 :: text[]) THEN
user_rbac_roles && $8 :: text[]
ELSE true
END
-- Filter by last_seen
AND CASE
WHEN $9 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
user_last_seen_at <= $9
ELSE true
END
AND CASE
WHEN $10 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
user_last_seen_at >= $10
ELSE true
END
-- Filter by created_at
AND CASE
WHEN $11 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
user_created_at <= $11
ELSE true
END
AND CASE
WHEN $12 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
user_created_at >= $12
ELSE true
END
-- Filter by system type
AND CASE
WHEN $13::bool THEN TRUE
ELSE user_is_system = false
END
-- Filter by github.com user ID
AND CASE
WHEN $14 :: bigint != 0 THEN
user_github_com_user_id = $14
ELSE true
END
-- Filter by login_type
AND CASE
WHEN cardinality($15 :: login_type[]) > 0 THEN
user_login_type = ANY($15 :: login_type[])
ELSE true
END
-- Filter by service account.
AND CASE
WHEN $16 :: boolean IS NOT NULL THEN
user_is_service_account = $16 :: boolean
ELSE true
END
-- End of filters
ORDER BY
-- Deterministic and consistent ordering of all users. This is to ensure consistent pagination.
LOWER(user_username) ASC OFFSET $17
LIMIT
-- A null limit means "no limit", so 0 means return all
NULLIF($18 :: int, 0)
`
type GetGroupMembersByGroupIDPaginatedParams struct {
GroupID uuid.UUID `db:"group_id" json:"group_id"`
AfterID uuid.UUID `db:"after_id" json:"after_id"`
Search string `db:"search" json:"search"`
Name string `db:"name" json:"name"`
ExactUsername string `db:"exact_username" json:"exact_username"`
ExactEmail string `db:"exact_email" json:"exact_email"`
Status []UserStatus `db:"status" json:"status"`
RbacRole []string `db:"rbac_role" json:"rbac_role"`
LastSeenBefore time.Time `db:"last_seen_before" json:"last_seen_before"`
LastSeenAfter time.Time `db:"last_seen_after" json:"last_seen_after"`
CreatedBefore time.Time `db:"created_before" json:"created_before"`
CreatedAfter time.Time `db:"created_after" json:"created_after"`
IncludeSystem bool `db:"include_system" json:"include_system"`
GithubComUserID int64 `db:"github_com_user_id" json:"github_com_user_id"`
LoginType []LoginType `db:"login_type" json:"login_type"`
IsServiceAccount sql.NullBool `db:"is_service_account" json:"is_service_account"`
OffsetOpt int32 `db:"offset_opt" json:"offset_opt"`
LimitOpt int32 `db:"limit_opt" json:"limit_opt"`
}
type GetGroupMembersByGroupIDPaginatedRow struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
UserEmail string `db:"user_email" json:"user_email"`
UserUsername string `db:"user_username" json:"user_username"`
UserHashedPassword []byte `db:"user_hashed_password" json:"user_hashed_password"`
UserCreatedAt time.Time `db:"user_created_at" json:"user_created_at"`
UserUpdatedAt time.Time `db:"user_updated_at" json:"user_updated_at"`
UserStatus UserStatus `db:"user_status" json:"user_status"`
UserRbacRoles []string `db:"user_rbac_roles" json:"user_rbac_roles"`
UserLoginType LoginType `db:"user_login_type" json:"user_login_type"`
UserAvatarUrl string `db:"user_avatar_url" json:"user_avatar_url"`
UserDeleted bool `db:"user_deleted" json:"user_deleted"`
UserLastSeenAt time.Time `db:"user_last_seen_at" json:"user_last_seen_at"`
UserQuietHoursSchedule string `db:"user_quiet_hours_schedule" json:"user_quiet_hours_schedule"`
UserName string `db:"user_name" json:"user_name"`
UserGithubComUserID sql.NullInt64 `db:"user_github_com_user_id" json:"user_github_com_user_id"`
UserIsSystem bool `db:"user_is_system" json:"user_is_system"`
UserIsServiceAccount bool `db:"user_is_service_account" json:"user_is_service_account"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
GroupName string `db:"group_name" json:"group_name"`
GroupID uuid.UUID `db:"group_id" json:"group_id"`
Count int64 `db:"count" json:"count"`
}
func (q *sqlQuerier) GetGroupMembersByGroupIDPaginated(ctx context.Context, arg GetGroupMembersByGroupIDPaginatedParams) ([]GetGroupMembersByGroupIDPaginatedRow, error) {
rows, err := q.db.QueryContext(ctx, getGroupMembersByGroupIDPaginated,
arg.GroupID,
arg.AfterID,
arg.Search,
arg.Name,
arg.ExactUsername,
arg.ExactEmail,
pq.Array(arg.Status),
pq.Array(arg.RbacRole),
arg.LastSeenBefore,
arg.LastSeenAfter,
arg.CreatedBefore,
arg.CreatedAfter,
arg.IncludeSystem,
arg.GithubComUserID,
pq.Array(arg.LoginType),
arg.IsServiceAccount,
arg.OffsetOpt,
arg.LimitOpt,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetGroupMembersByGroupIDPaginatedRow
for rows.Next() {
var i GetGroupMembersByGroupIDPaginatedRow
if err := rows.Scan(
&i.UserID,
&i.UserEmail,
&i.UserUsername,
&i.UserHashedPassword,
&i.UserCreatedAt,
&i.UserUpdatedAt,
&i.UserStatus,
pq.Array(&i.UserRbacRoles),
&i.UserLoginType,
&i.UserAvatarUrl,
&i.UserDeleted,
&i.UserLastSeenAt,
&i.UserQuietHoursSchedule,
&i.UserName,
&i.UserGithubComUserID,
&i.UserIsSystem,
&i.UserIsServiceAccount,
&i.OrganizationID,
&i.GroupName,
&i.GroupID,
&i.Count,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getGroupMembersCountByGroupID = `-- name: GetGroupMembersCountByGroupID :one
SELECT COUNT(*)
FROM group_members_expanded
WHERE group_id = $1
-- Filter by system type
AND CASE
WHEN $2::bool THEN TRUE
ELSE
user_is_system = false
END
`
type GetGroupMembersCountByGroupIDParams struct {
GroupID uuid.UUID `db:"group_id" json:"group_id"`
IncludeSystem bool `db:"include_system" json:"include_system"`
}
// Returns the total count of members in a group. Shows the total
// count even if the caller does not have read access to ResourceGroupMember.
// They only need ResourceGroup read access.
func (q *sqlQuerier) GetGroupMembersCountByGroupID(ctx context.Context, arg GetGroupMembersCountByGroupIDParams) (int64, error) {
row := q.db.QueryRowContext(ctx, getGroupMembersCountByGroupID, arg.GroupID, arg.IncludeSystem)
var count int64
err := row.Scan(&count)
return count, err
}
const getGroupMembersCountByGroupIDs = `-- name: GetGroupMembersCountByGroupIDs :many
SELECT
group_id,
COUNT(*) AS member_count
FROM group_members_expanded
WHERE group_id = ANY($1 :: uuid[])
AND CASE
WHEN $2::bool THEN TRUE
ELSE user_is_system = false
END
GROUP BY group_id
`
type GetGroupMembersCountByGroupIDsParams struct {
GroupIds []uuid.UUID `db:"group_ids" json:"group_ids"`
IncludeSystem bool `db:"include_system" json:"include_system"`
}
type GetGroupMembersCountByGroupIDsRow struct {
GroupID uuid.UUID `db:"group_id" json:"group_id"`
MemberCount int64 `db:"member_count" json:"member_count"`
}
// Returns the total member count for each of the given group IDs in a
// single query. Used to avoid N+1 lookups when listing many groups. Like
// GetGroupMembersCountByGroupID, the count is returned even when the
// caller does not have read access to individual group members.
func (q *sqlQuerier) GetGroupMembersCountByGroupIDs(ctx context.Context, arg GetGroupMembersCountByGroupIDsParams) ([]GetGroupMembersCountByGroupIDsRow, error) {
rows, err := q.db.QueryContext(ctx, getGroupMembersCountByGroupIDs, pq.Array(arg.GroupIds), arg.IncludeSystem)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetGroupMembersCountByGroupIDsRow
for rows.Next() {
var i GetGroupMembersCountByGroupIDsRow
if err := rows.Scan(&i.GroupID, &i.MemberCount); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertGroupMember = `-- name: InsertGroupMember :exec
INSERT INTO
group_members (user_id, group_id)
VALUES
($1, $2)
`
type InsertGroupMemberParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
GroupID uuid.UUID `db:"group_id" json:"group_id"`
}
func (q *sqlQuerier) InsertGroupMember(ctx context.Context, arg InsertGroupMemberParams) error {
_, err := q.db.ExecContext(ctx, insertGroupMember, arg.UserID, arg.GroupID)
return err
}
const insertUserGroupsByID = `-- name: InsertUserGroupsByID :many
WITH groups AS (
SELECT
id
FROM
groups
WHERE
groups.id = ANY($2 :: uuid [])
)
INSERT INTO
group_members (user_id, group_id)
SELECT
$1,
groups.id
FROM
groups
ON CONFLICT DO NOTHING
RETURNING group_id
`
type InsertUserGroupsByIDParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
GroupIds []uuid.UUID `db:"group_ids" json:"group_ids"`
}
// InsertUserGroupsByID adds a user to all provided groups, if they exist.
// If there is a conflict, the user is already a member
func (q *sqlQuerier) InsertUserGroupsByID(ctx context.Context, arg InsertUserGroupsByIDParams) ([]uuid.UUID, error) {
rows, err := q.db.QueryContext(ctx, insertUserGroupsByID, arg.UserID, pq.Array(arg.GroupIds))
if err != nil {
return nil, err
}
defer rows.Close()
var items []uuid.UUID
for rows.Next() {
var group_id uuid.UUID
if err := rows.Scan(&group_id); err != nil {
return nil, err
}
items = append(items, group_id)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const removeUserFromGroups = `-- name: RemoveUserFromGroups :many
DELETE FROM
group_members
WHERE
user_id = $1 AND
group_id = ANY($2 :: uuid [])
RETURNING group_id
`
type RemoveUserFromGroupsParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
GroupIds []uuid.UUID `db:"group_ids" json:"group_ids"`
}
func (q *sqlQuerier) RemoveUserFromGroups(ctx context.Context, arg RemoveUserFromGroupsParams) ([]uuid.UUID, error) {
rows, err := q.db.QueryContext(ctx, removeUserFromGroups, arg.UserID, pq.Array(arg.GroupIds))
if err != nil {
return nil, err
}
defer rows.Close()
var items []uuid.UUID
for rows.Next() {
var group_id uuid.UUID
if err := rows.Scan(&group_id); err != nil {
return nil, err
}
items = append(items, group_id)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const deleteGroupByID = `-- name: DeleteGroupByID :exec
DELETE FROM
groups
WHERE
id = $1
`
func (q *sqlQuerier) DeleteGroupByID(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteGroupByID, id)
return err
}
const getGroupByID = `-- name: GetGroupByID :one
SELECT
id, name, organization_id, avatar_url, quota_allowance, display_name, source, chat_spend_limit_micros
FROM
groups
WHERE
id = $1
LIMIT
1
`
func (q *sqlQuerier) GetGroupByID(ctx context.Context, id uuid.UUID) (Group, error) {
row := q.db.QueryRowContext(ctx, getGroupByID, id)
var i Group
err := row.Scan(
&i.ID,
&i.Name,
&i.OrganizationID,
&i.AvatarURL,
&i.QuotaAllowance,
&i.DisplayName,
&i.Source,
&i.ChatSpendLimitMicros,
)
return i, err
}
const getGroupByOrgAndName = `-- name: GetGroupByOrgAndName :one
SELECT
id, name, organization_id, avatar_url, quota_allowance, display_name, source, chat_spend_limit_micros
FROM
groups
WHERE
organization_id = $1
AND
-- Match group name case-insensitively, consistent with organization and
-- workspace name lookups.
LOWER("name") = LOWER($2)
LIMIT
1
`
type GetGroupByOrgAndNameParams struct {
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
Name string `db:"name" json:"name"`
}
func (q *sqlQuerier) GetGroupByOrgAndName(ctx context.Context, arg GetGroupByOrgAndNameParams) (Group, error) {
row := q.db.QueryRowContext(ctx, getGroupByOrgAndName, arg.OrganizationID, arg.Name)
var i Group
err := row.Scan(
&i.ID,
&i.Name,
&i.OrganizationID,
&i.AvatarURL,
&i.QuotaAllowance,
&i.DisplayName,
&i.Source,
&i.ChatSpendLimitMicros,
)
return i, err
}
const getGroups = `-- name: GetGroups :many
SELECT
groups.id, groups.name, groups.organization_id, groups.avatar_url, groups.quota_allowance, groups.display_name, groups.source, groups.chat_spend_limit_micros,
organizations.name AS organization_name,
organizations.display_name AS organization_display_name
FROM
groups
INNER JOIN
organizations ON groups.organization_id = organizations.id
WHERE
true
AND CASE
WHEN $1:: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
groups.organization_id = $1
ELSE true
END
AND CASE
-- Filter to only include groups a user is a member of
WHEN $2::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
EXISTS (
SELECT
1
FROM
-- this view handles the 'everyone' group in orgs.
group_members_expanded
WHERE
group_members_expanded.group_id = groups.id
AND
group_members_expanded.user_id = $2
)
ELSE true
END
AND CASE WHEN array_length($3 :: text[], 1) > 0 THEN
groups.name = ANY($3)
ELSE true
END
AND CASE WHEN array_length($4 :: uuid[], 1) > 0 THEN
groups.id = ANY($4)
ELSE true
END
-- Filter by group name or display name (substring, case-insensitive).
AND CASE WHEN $5 :: text != '' THEN (
groups.name ILIKE concat('%', $5, '%')
OR groups.display_name ILIKE concat('%', $5, '%')
)
ELSE true
END
LIMIT NULLIF($6 :: int, 0)
`
type GetGroupsParams struct {
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
HasMemberID uuid.UUID `db:"has_member_id" json:"has_member_id"`
GroupNames []string `db:"group_names" json:"group_names"`
GroupIds []uuid.UUID `db:"group_ids" json:"group_ids"`
Search string `db:"search" json:"search"`
LimitOpt int32 `db:"limit_opt" json:"limit_opt"`
}
type GetGroupsRow struct {
Group Group `db:"group" json:"group"`
OrganizationName string `db:"organization_name" json:"organization_name"`
OrganizationDisplayName string `db:"organization_display_name" json:"organization_display_name"`
}
// A limit of 0 means "no limit".
func (q *sqlQuerier) GetGroups(ctx context.Context, arg GetGroupsParams) ([]GetGroupsRow, error) {
rows, err := q.db.QueryContext(ctx, getGroups,
arg.OrganizationID,
arg.HasMemberID,
pq.Array(arg.GroupNames),
pq.Array(arg.GroupIds),
arg.Search,
arg.LimitOpt,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetGroupsRow
for rows.Next() {
var i GetGroupsRow
if err := rows.Scan(
&i.Group.ID,
&i.Group.Name,
&i.Group.OrganizationID,
&i.Group.AvatarURL,
&i.Group.QuotaAllowance,
&i.Group.DisplayName,
&i.Group.Source,
&i.Group.ChatSpendLimitMicros,
&i.OrganizationName,
&i.OrganizationDisplayName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getGroupsByOrganizationIDPaginated = `-- name: GetGroupsByOrganizationIDPaginated :many
SELECT
groups.id, groups.name, groups.organization_id, groups.avatar_url, groups.quota_allowance, groups.display_name, groups.source, groups.chat_spend_limit_micros,
organizations.name AS organization_name,
organizations.display_name AS organization_display_name,
COUNT(*) OVER() AS count
FROM
groups
INNER JOIN
organizations ON groups.organization_id = organizations.id
WHERE
true
AND groups.organization_id = $1
-- Keyset pagination cursor. When @after_id is set, return only groups
-- ordered after it, matching the ORDER BY (LOWER(name), id) below. This
-- lets callers page without duplicated or skipped rows even if groups are
-- inserted or deleted between page requests.
AND CASE
WHEN $2 :: uuid != '00000000-0000-0000-0000-000000000000' :: uuid THEN
(LOWER(groups.name), groups.id) > (
SELECT LOWER(name), id FROM groups WHERE id = $2
)
ELSE true
END
-- Filter by group name or display name (substring, case-insensitive).
AND CASE WHEN $3 :: text != '' THEN (
groups.name ILIKE concat('%', $3, '%')
OR groups.display_name ILIKE concat('%', $3, '%')
)
ELSE true
END
ORDER BY
-- Deterministic and consistent ordering of all groups. This is to ensure consistent pagination.
LOWER(groups.name) ASC, groups.id ASC OFFSET $4
LIMIT
-- A null limit means "no limit", so 0 means return all
NULLIF($5 :: int, 0)
`
type GetGroupsByOrganizationIDPaginatedParams struct {
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
AfterID uuid.UUID `db:"after_id" json:"after_id"`
Search string `db:"search" json:"search"`
OffsetOpt int32 `db:"offset_opt" json:"offset_opt"`
LimitOpt int32 `db:"limit_opt" json:"limit_opt"`
}
type GetGroupsByOrganizationIDPaginatedRow struct {
Group Group `db:"group" json:"group"`
OrganizationName string `db:"organization_name" json:"organization_name"`
OrganizationDisplayName string `db:"organization_display_name" json:"organization_display_name"`
Count int64 `db:"count" json:"count"`
}
func (q *sqlQuerier) GetGroupsByOrganizationIDPaginated(ctx context.Context, arg GetGroupsByOrganizationIDPaginatedParams) ([]GetGroupsByOrganizationIDPaginatedRow, error) {
rows, err := q.db.QueryContext(ctx, getGroupsByOrganizationIDPaginated,
arg.OrganizationID,
arg.AfterID,
arg.Search,
arg.OffsetOpt,
arg.LimitOpt,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetGroupsByOrganizationIDPaginatedRow
for rows.Next() {
var i GetGroupsByOrganizationIDPaginatedRow
if err := rows.Scan(
&i.Group.ID,
&i.Group.Name,
&i.Group.OrganizationID,
&i.Group.AvatarURL,
&i.Group.QuotaAllowance,
&i.Group.DisplayName,
&i.Group.Source,
&i.Group.ChatSpendLimitMicros,
&i.OrganizationName,
&i.OrganizationDisplayName,
&i.Count,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertAllUsersGroup = `-- name: InsertAllUsersGroup :one
INSERT INTO groups (
id,
name,
organization_id
)
VALUES
($1, 'Everyone', $1) RETURNING id, name, organization_id, avatar_url, quota_allowance, display_name, source, chat_spend_limit_micros
`
// We use the organization_id as the id
// for simplicity since all users is
// every member of the org.
func (q *sqlQuerier) InsertAllUsersGroup(ctx context.Context, organizationID uuid.UUID) (Group, error) {
row := q.db.QueryRowContext(ctx, insertAllUsersGroup, organizationID)
var i Group
err := row.Scan(
&i.ID,
&i.Name,
&i.OrganizationID,
&i.AvatarURL,
&i.QuotaAllowance,
&i.DisplayName,
&i.Source,
&i.ChatSpendLimitMicros,
)
return i, err
}
const insertGroup = `-- name: InsertGroup :one
INSERT INTO groups (
id,
name,
display_name,
organization_id,
avatar_url,
quota_allowance
)
VALUES
($1, $2, $3, $4, $5, $6) RETURNING id, name, organization_id, avatar_url, quota_allowance, display_name, source, chat_spend_limit_micros
`
type InsertGroupParams struct {
ID uuid.UUID `db:"id" json:"id"`
Name string `db:"name" json:"name"`
DisplayName string `db:"display_name" json:"display_name"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
AvatarURL string `db:"avatar_url" json:"avatar_url"`
QuotaAllowance int32 `db:"quota_allowance" json:"quota_allowance"`
}
func (q *sqlQuerier) InsertGroup(ctx context.Context, arg InsertGroupParams) (Group, error) {
row := q.db.QueryRowContext(ctx, insertGroup,
arg.ID,
arg.Name,
arg.DisplayName,
arg.OrganizationID,
arg.AvatarURL,
arg.QuotaAllowance,
)
var i Group
err := row.Scan(
&i.ID,
&i.Name,
&i.OrganizationID,
&i.AvatarURL,
&i.QuotaAllowance,
&i.DisplayName,
&i.Source,
&i.ChatSpendLimitMicros,
)
return i, err
}
const insertMissingGroups = `-- name: InsertMissingGroups :many
INSERT INTO groups (
id,
name,
organization_id,
source
)
SELECT
gen_random_uuid(),
group_name,
$1,
$2
FROM
UNNEST($3 :: text[]) AS group_name
ON CONFLICT DO NOTHING
RETURNING id, name, organization_id, avatar_url, quota_allowance, display_name, source, chat_spend_limit_micros
`
type InsertMissingGroupsParams struct {
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
Source GroupSource `db:"source" json:"source"`
GroupNames []string `db:"group_names" json:"group_names"`
}
// Inserts any group by name that does not exist. All new groups are given
// a random uuid, are inserted into the same organization. They have the default
// values for avatar, display name, and quota allowance (all zero values).
// If the name conflicts, do nothing.
func (q *sqlQuerier) InsertMissingGroups(ctx context.Context, arg InsertMissingGroupsParams) ([]Group, error) {
rows, err := q.db.QueryContext(ctx, insertMissingGroups, arg.OrganizationID, arg.Source, pq.Array(arg.GroupNames))
if err != nil {
return nil, err
}
defer rows.Close()
var items []Group
for rows.Next() {
var i Group
if err := rows.Scan(
&i.ID,
&i.Name,
&i.OrganizationID,
&i.AvatarURL,
&i.QuotaAllowance,
&i.DisplayName,
&i.Source,
&i.ChatSpendLimitMicros,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateGroupByID = `-- name: UpdateGroupByID :one
UPDATE
groups
SET
name = $1,
display_name = $2,
avatar_url = $3,
quota_allowance = $4
WHERE
id = $5
RETURNING id, name, organization_id, avatar_url, quota_allowance, display_name, source, chat_spend_limit_micros
`
type UpdateGroupByIDParams struct {
Name string `db:"name" json:"name"`
DisplayName string `db:"display_name" json:"display_name"`
AvatarURL string `db:"avatar_url" json:"avatar_url"`
QuotaAllowance int32 `db:"quota_allowance" json:"quota_allowance"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateGroupByID(ctx context.Context, arg UpdateGroupByIDParams) (Group, error) {
row := q.db.QueryRowContext(ctx, updateGroupByID,
arg.Name,
arg.DisplayName,
arg.AvatarURL,
arg.QuotaAllowance,
arg.ID,
)
var i Group
err := row.Scan(
&i.ID,
&i.Name,
&i.OrganizationID,
&i.AvatarURL,
&i.QuotaAllowance,
&i.DisplayName,
&i.Source,
&i.ChatSpendLimitMicros,
)
return i, err
}
const validateGroupIDs = `-- name: ValidateGroupIDs :one
WITH input AS (
SELECT
unnest($1::uuid[]) AS id
)
SELECT
array_agg(input.id)::uuid[] as invalid_group_ids,
COUNT(*) = 0 as ok
FROM
-- Preserve rows where there is not a matching left (groups) row for each
-- right (input) row...
groups
RIGHT JOIN input ON groups.id = input.id
WHERE
-- ...so that we can retain exactly those rows where an input ID does not
-- match an existing group.
groups.id IS NULL
`
type ValidateGroupIDsRow struct {
InvalidGroupIds []uuid.UUID `db:"invalid_group_ids" json:"invalid_group_ids"`
Ok bool `db:"ok" json:"ok"`
}
func (q *sqlQuerier) ValidateGroupIDs(ctx context.Context, groupIds []uuid.UUID) (ValidateGroupIDsRow, error) {
row := q.db.QueryRowContext(ctx, validateGroupIDs, pq.Array(groupIds))
var i ValidateGroupIDsRow
err := row.Scan(pq.Array(&i.InvalidGroupIds), &i.Ok)
return i, err
}
const getTemplateAppInsights = `-- name: GetTemplateAppInsights :many
WITH
-- Create a list of all unique apps by template, this is used to
-- filter out irrelevant template usage stats.
apps AS (
SELECT DISTINCT ON (ws.template_id, app.slug)
ws.template_id,
app.slug,
app.display_name,
app.icon
FROM
workspaces ws
JOIN
workspace_builds AS build
ON
build.workspace_id = ws.id
JOIN
workspace_resources AS resource
ON
resource.job_id = build.job_id
JOIN
workspace_agents AS agent
ON
agent.resource_id = resource.id
JOIN
workspace_apps AS app
ON
app.agent_id = agent.id
WHERE
-- Partial query parameter filter.
CASE WHEN COALESCE(array_length($1::uuid[], 1), 0) > 0 THEN ws.template_id = ANY($1::uuid[]) ELSE TRUE END
ORDER BY
ws.template_id, app.slug, app.created_at DESC
),
-- Join apps and template usage stats to filter out irrelevant rows.
-- Note that this way of joining will eliminate all data-points that
-- aren't for "real" apps. That means ports are ignored (even though
-- they're part of the dataset), as well as are "[terminal]" entries
-- which are alternate datapoints for reconnecting pty usage.
template_usage_stats_with_apps AS (
SELECT
tus.start_time,
tus.template_id,
tus.user_id,
apps.slug,
apps.display_name,
apps.icon,
(tus.app_usage_mins -> apps.slug)::smallint AS usage_mins
FROM
apps
JOIN
template_usage_stats AS tus
ON
-- Query parameter filter.
tus.start_time >= $2::timestamptz
AND tus.end_time <= $3::timestamptz
AND CASE WHEN COALESCE(array_length($1::uuid[], 1), 0) > 0 THEN tus.template_id = ANY($1::uuid[]) ELSE TRUE END
-- Primary join condition.
AND tus.template_id = apps.template_id
AND tus.app_usage_mins ? apps.slug -- Key exists in object.
),
-- Group the app insights by interval, user and unique app. This
-- allows us to deduplicate a user using the same app across
-- multiple templates.
app_insights AS (
SELECT
user_id,
slug,
display_name,
icon,
-- See motivation in GetTemplateInsights for LEAST(SUM(n), 30).
LEAST(SUM(usage_mins), 30) AS usage_mins
FROM
template_usage_stats_with_apps
GROUP BY
start_time, user_id, slug, display_name, icon
),
-- Analyze the users unique app usage across all templates. Count
-- usage across consecutive intervals as continuous usage.
times_used AS (
SELECT DISTINCT ON (user_id, slug, display_name, icon, uniq)
slug,
display_name,
icon,
-- Turn start_time into a unique identifier that identifies a users
-- continuous app usage. The value of uniq is otherwise garbage.
--
-- Since we're aggregating per user app usage across templates,
-- there can be duplicate start_times. To handle this, we use the
-- dense_rank() function, otherwise row_number() would suffice.
start_time - (
dense_rank() OVER (
PARTITION BY
user_id, slug, display_name, icon
ORDER BY
start_time
) * '30 minutes'::interval
) AS uniq
FROM
template_usage_stats_with_apps
),
-- Even though we allow identical apps to be aggregated across
-- templates, we still want to be able to report which templates
-- the data comes from.
templates AS (
SELECT
slug,
display_name,
icon,
array_agg(DISTINCT template_id)::uuid[] AS template_ids
FROM
template_usage_stats_with_apps
GROUP BY
slug, display_name, icon
)
SELECT
t.template_ids,
COUNT(DISTINCT ai.user_id) AS active_users,
ai.slug,
ai.display_name,
ai.icon,
(SUM(ai.usage_mins) * 60)::bigint AS usage_seconds,
COALESCE((
SELECT
COUNT(*)
FROM
times_used
WHERE
times_used.slug = ai.slug
AND times_used.display_name = ai.display_name
AND times_used.icon = ai.icon
), 0)::bigint AS times_used
FROM
app_insights AS ai
JOIN
templates AS t
ON
t.slug = ai.slug
AND t.display_name = ai.display_name
AND t.icon = ai.icon
GROUP BY
t.template_ids, ai.slug, ai.display_name, ai.icon
`
type GetTemplateAppInsightsParams struct {
TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
StartTime time.Time `db:"start_time" json:"start_time"`
EndTime time.Time `db:"end_time" json:"end_time"`
}
type GetTemplateAppInsightsRow struct {
TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
ActiveUsers int64 `db:"active_users" json:"active_users"`
Slug string `db:"slug" json:"slug"`
DisplayName string `db:"display_name" json:"display_name"`
Icon string `db:"icon" json:"icon"`
UsageSeconds int64 `db:"usage_seconds" json:"usage_seconds"`
TimesUsed int64 `db:"times_used" json:"times_used"`
}
// GetTemplateAppInsights returns the aggregate usage of each app in a given
// timeframe. The result can be filtered on template_ids, meaning only user data
// from workspaces based on those templates will be included.
func (q *sqlQuerier) GetTemplateAppInsights(ctx context.Context, arg GetTemplateAppInsightsParams) ([]GetTemplateAppInsightsRow, error) {
rows, err := q.db.QueryContext(ctx, getTemplateAppInsights, pq.Array(arg.TemplateIDs), arg.StartTime, arg.EndTime)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetTemplateAppInsightsRow
for rows.Next() {
var i GetTemplateAppInsightsRow
if err := rows.Scan(
pq.Array(&i.TemplateIDs),
&i.ActiveUsers,
&i.Slug,
&i.DisplayName,
&i.Icon,
&i.UsageSeconds,
&i.TimesUsed,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getTemplateAppInsightsByTemplate = `-- name: GetTemplateAppInsightsByTemplate :many
WITH
filtered_stats AS (
SELECT
was.workspace_id,
was.user_id,
was.agent_id,
was.access_method,
was.slug_or_port,
was.session_started_at,
was.session_ended_at
FROM
workspace_app_stats AS was
WHERE
was.session_ended_at >= $1::timestamptz
AND was.session_started_at < $2::timestamptz
),
-- This CTE is used to explode app usage into minute buckets, then
-- flatten the users app usage within the template so that usage in
-- multiple workspaces under one template is only counted once for
-- every minute.
app_insights AS (
SELECT
w.template_id,
fs.user_id,
-- Both app stats and agent stats track web terminal usage, but
-- by different means. The app stats value should be more
-- accurate so we don't want to discard it just yet.
CASE
WHEN fs.access_method = 'terminal'
THEN '[terminal]' -- Unique name, app names can't contain brackets.
ELSE fs.slug_or_port
END::text AS app_name,
COALESCE(wa.display_name, '') AS display_name,
(wa.slug IS NOT NULL)::boolean AS is_app,
COUNT(DISTINCT s.minute_bucket) AS app_minutes
FROM
filtered_stats AS fs
JOIN
workspaces AS w
ON
w.id = fs.workspace_id
-- We do a left join here because we want to include user IDs that have used
-- e.g. ports when counting active users.
LEFT JOIN
workspace_apps wa
ON
wa.agent_id = fs.agent_id
AND wa.slug = fs.slug_or_port
-- Generate a series of minute buckets for each session for computing the
-- mintes/bucket.
CROSS JOIN
generate_series(
date_trunc('minute', fs.session_started_at),
-- Subtract 1 μs to avoid creating an extra series.
date_trunc('minute', fs.session_ended_at - '1 microsecond'::interval),
'1 minute'::interval
) AS s(minute_bucket)
WHERE
s.minute_bucket >= $1::timestamptz
AND s.minute_bucket < $2::timestamptz
GROUP BY
w.template_id, fs.user_id, fs.access_method, fs.slug_or_port, wa.display_name, wa.slug
)
SELECT
template_id,
app_name AS slug_or_port,
display_name AS display_name,
COUNT(DISTINCT user_id)::bigint AS active_users,
(SUM(app_minutes) * 60)::bigint AS usage_seconds
FROM
app_insights
WHERE
is_app IS TRUE
GROUP BY
template_id, slug_or_port, display_name
`
type GetTemplateAppInsightsByTemplateParams struct {
StartTime time.Time `db:"start_time" json:"start_time"`
EndTime time.Time `db:"end_time" json:"end_time"`
}
type GetTemplateAppInsightsByTemplateRow struct {
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
SlugOrPort string `db:"slug_or_port" json:"slug_or_port"`
DisplayName string `db:"display_name" json:"display_name"`
ActiveUsers int64 `db:"active_users" json:"active_users"`
UsageSeconds int64 `db:"usage_seconds" json:"usage_seconds"`
}
// GetTemplateAppInsightsByTemplate is used for Prometheus metrics. Keep
// in sync with GetTemplateAppInsights and UpsertTemplateUsageStats.
func (q *sqlQuerier) GetTemplateAppInsightsByTemplate(ctx context.Context, arg GetTemplateAppInsightsByTemplateParams) ([]GetTemplateAppInsightsByTemplateRow, error) {
rows, err := q.db.QueryContext(ctx, getTemplateAppInsightsByTemplate, arg.StartTime, arg.EndTime)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetTemplateAppInsightsByTemplateRow
for rows.Next() {
var i GetTemplateAppInsightsByTemplateRow
if err := rows.Scan(
&i.TemplateID,
&i.SlugOrPort,
&i.DisplayName,
&i.ActiveUsers,
&i.UsageSeconds,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getTemplateInsights = `-- name: GetTemplateInsights :one
WITH
insights AS (
SELECT
user_id,
-- See motivation in GetTemplateInsights for LEAST(SUM(n), 30).
LEAST(SUM(usage_mins), 30) AS usage_mins,
LEAST(SUM(ssh_mins), 30) AS ssh_mins,
LEAST(SUM(sftp_mins), 30) AS sftp_mins,
LEAST(SUM(reconnecting_pty_mins), 30) AS reconnecting_pty_mins,
LEAST(SUM(vscode_mins), 30) AS vscode_mins,
LEAST(SUM(jetbrains_mins), 30) AS jetbrains_mins
FROM
template_usage_stats
WHERE
start_time >= $1::timestamptz
AND end_time <= $2::timestamptz
AND CASE WHEN COALESCE(array_length($3::uuid[], 1), 0) > 0 THEN template_id = ANY($3::uuid[]) ELSE TRUE END
GROUP BY
start_time, user_id
),
templates AS (
SELECT
array_agg(DISTINCT template_id) AS template_ids,
array_agg(DISTINCT template_id) FILTER (WHERE ssh_mins > 0) AS ssh_template_ids,
array_agg(DISTINCT template_id) FILTER (WHERE sftp_mins > 0) AS sftp_template_ids,
array_agg(DISTINCT template_id) FILTER (WHERE reconnecting_pty_mins > 0) AS reconnecting_pty_template_ids,
array_agg(DISTINCT template_id) FILTER (WHERE vscode_mins > 0) AS vscode_template_ids,
array_agg(DISTINCT template_id) FILTER (WHERE jetbrains_mins > 0) AS jetbrains_template_ids
FROM
template_usage_stats
WHERE
start_time >= $1::timestamptz
AND end_time <= $2::timestamptz
AND CASE WHEN COALESCE(array_length($3::uuid[], 1), 0) > 0 THEN template_id = ANY($3::uuid[]) ELSE TRUE END
)
SELECT
COALESCE((SELECT template_ids FROM templates), '{}')::uuid[] AS template_ids, -- Includes app usage.
COALESCE((SELECT ssh_template_ids FROM templates), '{}')::uuid[] AS ssh_template_ids,
COALESCE((SELECT sftp_template_ids FROM templates), '{}')::uuid[] AS sftp_template_ids,
COALESCE((SELECT reconnecting_pty_template_ids FROM templates), '{}')::uuid[] AS reconnecting_pty_template_ids,
COALESCE((SELECT vscode_template_ids FROM templates), '{}')::uuid[] AS vscode_template_ids,
COALESCE((SELECT jetbrains_template_ids FROM templates), '{}')::uuid[] AS jetbrains_template_ids,
COALESCE(COUNT(DISTINCT user_id), 0)::bigint AS active_users, -- Includes app usage.
COALESCE(SUM(usage_mins) * 60, 0)::bigint AS usage_total_seconds, -- Includes app usage.
COALESCE(SUM(ssh_mins) * 60, 0)::bigint AS usage_ssh_seconds,
COALESCE(SUM(sftp_mins) * 60, 0)::bigint AS usage_sftp_seconds,
COALESCE(SUM(reconnecting_pty_mins) * 60, 0)::bigint AS usage_reconnecting_pty_seconds,
COALESCE(SUM(vscode_mins) * 60, 0)::bigint AS usage_vscode_seconds,
COALESCE(SUM(jetbrains_mins) * 60, 0)::bigint AS usage_jetbrains_seconds
FROM
insights
`
type GetTemplateInsightsParams struct {
StartTime time.Time `db:"start_time" json:"start_time"`
EndTime time.Time `db:"end_time" json:"end_time"`
TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
}
type GetTemplateInsightsRow struct {
TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
SshTemplateIds []uuid.UUID `db:"ssh_template_ids" json:"ssh_template_ids"`
SftpTemplateIds []uuid.UUID `db:"sftp_template_ids" json:"sftp_template_ids"`
ReconnectingPtyTemplateIds []uuid.UUID `db:"reconnecting_pty_template_ids" json:"reconnecting_pty_template_ids"`
VscodeTemplateIds []uuid.UUID `db:"vscode_template_ids" json:"vscode_template_ids"`
JetbrainsTemplateIds []uuid.UUID `db:"jetbrains_template_ids" json:"jetbrains_template_ids"`
ActiveUsers int64 `db:"active_users" json:"active_users"`
UsageTotalSeconds int64 `db:"usage_total_seconds" json:"usage_total_seconds"`
UsageSshSeconds int64 `db:"usage_ssh_seconds" json:"usage_ssh_seconds"`
UsageSftpSeconds int64 `db:"usage_sftp_seconds" json:"usage_sftp_seconds"`
UsageReconnectingPtySeconds int64 `db:"usage_reconnecting_pty_seconds" json:"usage_reconnecting_pty_seconds"`
UsageVscodeSeconds int64 `db:"usage_vscode_seconds" json:"usage_vscode_seconds"`
UsageJetbrainsSeconds int64 `db:"usage_jetbrains_seconds" json:"usage_jetbrains_seconds"`
}
// GetTemplateInsights returns the aggregate user-produced usage of all
// workspaces in a given timeframe. The template IDs, active users, and
// usage_seconds all reflect any usage in the template, including apps.
//
// When combining data from multiple templates, we must make a guess at
// how the user behaved for the 30 minute interval. In this case we make
// the assumption that if the user used two workspaces for 15 minutes,
// they did so sequentially, thus we sum the usage up to a maximum of
// 30 minutes with LEAST(SUM(n), 30).
func (q *sqlQuerier) GetTemplateInsights(ctx context.Context, arg GetTemplateInsightsParams) (GetTemplateInsightsRow, error) {
row := q.db.QueryRowContext(ctx, getTemplateInsights, arg.StartTime, arg.EndTime, pq.Array(arg.TemplateIDs))
var i GetTemplateInsightsRow
err := row.Scan(
pq.Array(&i.TemplateIDs),
pq.Array(&i.SshTemplateIds),
pq.Array(&i.SftpTemplateIds),
pq.Array(&i.ReconnectingPtyTemplateIds),
pq.Array(&i.VscodeTemplateIds),
pq.Array(&i.JetbrainsTemplateIds),
&i.ActiveUsers,
&i.UsageTotalSeconds,
&i.UsageSshSeconds,
&i.UsageSftpSeconds,
&i.UsageReconnectingPtySeconds,
&i.UsageVscodeSeconds,
&i.UsageJetbrainsSeconds,
)
return i, err
}
const getTemplateInsightsByInterval = `-- name: GetTemplateInsightsByInterval :many
WITH
ts AS (
SELECT
d::timestamptz AS from_,
LEAST(
(d::timestamptz + ($2::int || ' day')::interval)::timestamptz,
$3::timestamptz
)::timestamptz AS to_
FROM
generate_series(
$4::timestamptz,
-- Subtract 1 μs to avoid creating an extra series.
($3::timestamptz) - '1 microsecond'::interval,
($2::int || ' day')::interval
) AS d
)
SELECT
ts.from_ AS start_time,
ts.to_ AS end_time,
array_remove(array_agg(DISTINCT tus.template_id), NULL)::uuid[] AS template_ids,
COUNT(DISTINCT tus.user_id) AS active_users
FROM
ts
LEFT JOIN
template_usage_stats AS tus
ON
tus.start_time >= ts.from_
AND tus.start_time < ts.to_ -- End time exclusion criteria optimization for index.
AND tus.end_time <= ts.to_
AND CASE WHEN COALESCE(array_length($1::uuid[], 1), 0) > 0 THEN tus.template_id = ANY($1::uuid[]) ELSE TRUE END
GROUP BY
ts.from_, ts.to_
`
type GetTemplateInsightsByIntervalParams struct {
TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
IntervalDays int32 `db:"interval_days" json:"interval_days"`
EndTime time.Time `db:"end_time" json:"end_time"`
StartTime time.Time `db:"start_time" json:"start_time"`
}
type GetTemplateInsightsByIntervalRow struct {
StartTime time.Time `db:"start_time" json:"start_time"`
EndTime time.Time `db:"end_time" json:"end_time"`
TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
ActiveUsers int64 `db:"active_users" json:"active_users"`
}
// GetTemplateInsightsByInterval returns all intervals between start and end
// time, if end time is a partial interval, it will be included in the results and
// that interval will be shorter than a full one. If there is no data for a selected
// interval/template, it will be included in the results with 0 active users.
func (q *sqlQuerier) GetTemplateInsightsByInterval(ctx context.Context, arg GetTemplateInsightsByIntervalParams) ([]GetTemplateInsightsByIntervalRow, error) {
rows, err := q.db.QueryContext(ctx, getTemplateInsightsByInterval,
pq.Array(arg.TemplateIDs),
arg.IntervalDays,
arg.EndTime,
arg.StartTime,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetTemplateInsightsByIntervalRow
for rows.Next() {
var i GetTemplateInsightsByIntervalRow
if err := rows.Scan(
&i.StartTime,
&i.EndTime,
pq.Array(&i.TemplateIDs),
&i.ActiveUsers,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getTemplateInsightsByTemplate = `-- name: GetTemplateInsightsByTemplate :many
WITH
-- This CTE is used to truncate agent usage into minute buckets, then
-- flatten the users agent usage within the template so that usage in
-- multiple workspaces under one template is only counted once for
-- every minute (per user).
insights AS (
SELECT
template_id,
user_id,
COUNT(DISTINCT CASE WHEN session_count_ssh > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS ssh_mins,
-- TODO(mafredri): Enable when we have the column.
-- COUNT(DISTINCT CASE WHEN session_count_sftp > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS sftp_mins,
COUNT(DISTINCT CASE WHEN session_count_reconnecting_pty > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS reconnecting_pty_mins,
COUNT(DISTINCT CASE WHEN session_count_vscode > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS vscode_mins,
COUNT(DISTINCT CASE WHEN session_count_jetbrains > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS jetbrains_mins,
-- NOTE(mafredri): The agent stats are currently very unreliable, and
-- sometimes the connections are missing, even during active sessions.
-- Since we can't fully rely on this, we check for "any connection
-- within this bucket". A better solution here would be preferable.
MAX(connection_count) > 0 AS has_connection
FROM
workspace_agent_stats
WHERE
created_at >= $1::timestamptz
AND created_at < $2::timestamptz
-- Inclusion criteria to filter out empty results.
AND (
session_count_ssh > 0
-- TODO(mafredri): Enable when we have the column.
-- OR session_count_sftp > 0
OR session_count_reconnecting_pty > 0
OR session_count_vscode > 0
OR session_count_jetbrains > 0
)
GROUP BY
template_id, user_id
)
SELECT
template_id,
COUNT(DISTINCT user_id)::bigint AS active_users,
(SUM(vscode_mins) * 60)::bigint AS usage_vscode_seconds,
(SUM(jetbrains_mins) * 60)::bigint AS usage_jetbrains_seconds,
(SUM(reconnecting_pty_mins) * 60)::bigint AS usage_reconnecting_pty_seconds,
(SUM(ssh_mins) * 60)::bigint AS usage_ssh_seconds
FROM
insights
WHERE
has_connection
GROUP BY
template_id
`
type GetTemplateInsightsByTemplateParams struct {
StartTime time.Time `db:"start_time" json:"start_time"`
EndTime time.Time `db:"end_time" json:"end_time"`
}
type GetTemplateInsightsByTemplateRow struct {
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
ActiveUsers int64 `db:"active_users" json:"active_users"`
UsageVscodeSeconds int64 `db:"usage_vscode_seconds" json:"usage_vscode_seconds"`
UsageJetbrainsSeconds int64 `db:"usage_jetbrains_seconds" json:"usage_jetbrains_seconds"`
UsageReconnectingPtySeconds int64 `db:"usage_reconnecting_pty_seconds" json:"usage_reconnecting_pty_seconds"`
UsageSshSeconds int64 `db:"usage_ssh_seconds" json:"usage_ssh_seconds"`
}
// GetTemplateInsightsByTemplate is used for Prometheus metrics. Keep
// in sync with GetTemplateInsights and UpsertTemplateUsageStats.
func (q *sqlQuerier) GetTemplateInsightsByTemplate(ctx context.Context, arg GetTemplateInsightsByTemplateParams) ([]GetTemplateInsightsByTemplateRow, error) {
rows, err := q.db.QueryContext(ctx, getTemplateInsightsByTemplate, arg.StartTime, arg.EndTime)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetTemplateInsightsByTemplateRow
for rows.Next() {
var i GetTemplateInsightsByTemplateRow
if err := rows.Scan(
&i.TemplateID,
&i.ActiveUsers,
&i.UsageVscodeSeconds,
&i.UsageJetbrainsSeconds,
&i.UsageReconnectingPtySeconds,
&i.UsageSshSeconds,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getTemplateParameterInsights = `-- name: GetTemplateParameterInsights :many
WITH latest_workspace_builds AS (
SELECT
wb.id,
wbmax.template_id,
wb.template_version_id
FROM (
SELECT
tv.template_id, wbmax.workspace_id, MAX(wbmax.build_number) as max_build_number
FROM workspace_builds wbmax
JOIN template_versions tv ON (tv.id = wbmax.template_version_id)
WHERE
wbmax.created_at >= $1::timestamptz
AND wbmax.created_at < $2::timestamptz
AND CASE WHEN COALESCE(array_length($3::uuid[], 1), 0) > 0 THEN tv.template_id = ANY($3::uuid[]) ELSE TRUE END
GROUP BY tv.template_id, wbmax.workspace_id
) wbmax
JOIN workspace_builds wb ON (
wb.workspace_id = wbmax.workspace_id
AND wb.build_number = wbmax.max_build_number
)
), unique_template_params AS (
SELECT
ROW_NUMBER() OVER () AS num,
array_agg(DISTINCT wb.template_id)::uuid[] AS template_ids,
array_agg(wb.id)::uuid[] AS workspace_build_ids,
tvp.name,
tvp.type,
tvp.display_name,
tvp.description,
tvp.options
FROM latest_workspace_builds wb
JOIN template_version_parameters tvp ON (tvp.template_version_id = wb.template_version_id)
GROUP BY tvp.name, tvp.type, tvp.display_name, tvp.description, tvp.options
)
SELECT
utp.num,
utp.template_ids,
utp.name,
utp.type,
utp.display_name,
utp.description,
utp.options,
wbp.value,
COUNT(wbp.value) AS count
FROM unique_template_params utp
JOIN workspace_build_parameters wbp ON (utp.workspace_build_ids @> ARRAY[wbp.workspace_build_id] AND utp.name = wbp.name)
GROUP BY utp.num, utp.template_ids, utp.name, utp.type, utp.display_name, utp.description, utp.options, wbp.value
`
type GetTemplateParameterInsightsParams struct {
StartTime time.Time `db:"start_time" json:"start_time"`
EndTime time.Time `db:"end_time" json:"end_time"`
TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
}
type GetTemplateParameterInsightsRow struct {
Num int64 `db:"num" json:"num"`
TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
Name string `db:"name" json:"name"`
Type string `db:"type" json:"type"`
DisplayName string `db:"display_name" json:"display_name"`
Description string `db:"description" json:"description"`
Options json.RawMessage `db:"options" json:"options"`
Value string `db:"value" json:"value"`
Count int64 `db:"count" json:"count"`
}
// GetTemplateParameterInsights does for each template in a given timeframe,
// look for the latest workspace build (for every workspace) that has been
// created in the timeframe and return the aggregate usage counts of parameter
// values.
func (q *sqlQuerier) GetTemplateParameterInsights(ctx context.Context, arg GetTemplateParameterInsightsParams) ([]GetTemplateParameterInsightsRow, error) {
rows, err := q.db.QueryContext(ctx, getTemplateParameterInsights, arg.StartTime, arg.EndTime, pq.Array(arg.TemplateIDs))
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetTemplateParameterInsightsRow
for rows.Next() {
var i GetTemplateParameterInsightsRow
if err := rows.Scan(
&i.Num,
pq.Array(&i.TemplateIDs),
&i.Name,
&i.Type,
&i.DisplayName,
&i.Description,
&i.Options,
&i.Value,
&i.Count,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getTemplateUsageStats = `-- name: GetTemplateUsageStats :many
SELECT
start_time, end_time, template_id, user_id, median_latency_ms, usage_mins, ssh_mins, sftp_mins, reconnecting_pty_mins, vscode_mins, jetbrains_mins, app_usage_mins
FROM
template_usage_stats
WHERE
start_time >= $1::timestamptz
AND end_time <= $2::timestamptz
AND CASE WHEN COALESCE(array_length($3::uuid[], 1), 0) > 0 THEN template_id = ANY($3::uuid[]) ELSE TRUE END
`
type GetTemplateUsageStatsParams struct {
StartTime time.Time `db:"start_time" json:"start_time"`
EndTime time.Time `db:"end_time" json:"end_time"`
TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
}
func (q *sqlQuerier) GetTemplateUsageStats(ctx context.Context, arg GetTemplateUsageStatsParams) ([]TemplateUsageStat, error) {
rows, err := q.db.QueryContext(ctx, getTemplateUsageStats, arg.StartTime, arg.EndTime, pq.Array(arg.TemplateIDs))
if err != nil {
return nil, err
}
defer rows.Close()
var items []TemplateUsageStat
for rows.Next() {
var i TemplateUsageStat
if err := rows.Scan(
&i.StartTime,
&i.EndTime,
&i.TemplateID,
&i.UserID,
&i.MedianLatencyMs,
&i.UsageMins,
&i.SshMins,
&i.SftpMins,
&i.ReconnectingPtyMins,
&i.VscodeMins,
&i.JetbrainsMins,
&i.AppUsageMins,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getUserActivityInsights = `-- name: GetUserActivityInsights :many
WITH
deployment_stats AS (
SELECT
start_time,
user_id,
array_agg(template_id) AS template_ids,
-- See motivation in GetTemplateInsights for LEAST(SUM(n), 30).
LEAST(SUM(usage_mins), 30) AS usage_mins
FROM
template_usage_stats
WHERE
start_time >= $1::timestamptz
AND end_time <= $2::timestamptz
AND CASE WHEN COALESCE(array_length($3::uuid[], 1), 0) > 0 THEN template_id = ANY($3::uuid[]) ELSE TRUE END
GROUP BY
start_time, user_id
),
template_ids AS (
SELECT
user_id,
array_agg(DISTINCT template_id) AS ids
FROM
deployment_stats, unnest(template_ids) template_id
GROUP BY
user_id
)
SELECT
ds.user_id,
u.username,
u.avatar_url,
t.ids::uuid[] AS template_ids,
(SUM(ds.usage_mins) * 60)::bigint AS usage_seconds
FROM
deployment_stats ds
JOIN
users u
ON
u.id = ds.user_id
JOIN
template_ids t
ON
ds.user_id = t.user_id
GROUP BY
ds.user_id, u.username, u.avatar_url, t.ids
ORDER BY
ds.user_id ASC
`
type GetUserActivityInsightsParams struct {
StartTime time.Time `db:"start_time" json:"start_time"`
EndTime time.Time `db:"end_time" json:"end_time"`
TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
}
type GetUserActivityInsightsRow struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
Username string `db:"username" json:"username"`
AvatarURL string `db:"avatar_url" json:"avatar_url"`
TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
UsageSeconds int64 `db:"usage_seconds" json:"usage_seconds"`
}
// GetUserActivityInsights returns the ranking with top active users.
// The result can be filtered on template_ids, meaning only user data
// from workspaces based on those templates will be included.
// Note: The usage_seconds and usage_seconds_cumulative differ only when
// requesting deployment-wide (or multiple template) data. Cumulative
// produces a bloated value if a user has used multiple templates
// simultaneously.
func (q *sqlQuerier) GetUserActivityInsights(ctx context.Context, arg GetUserActivityInsightsParams) ([]GetUserActivityInsightsRow, error) {
rows, err := q.db.QueryContext(ctx, getUserActivityInsights, arg.StartTime, arg.EndTime, pq.Array(arg.TemplateIDs))
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetUserActivityInsightsRow
for rows.Next() {
var i GetUserActivityInsightsRow
if err := rows.Scan(
&i.UserID,
&i.Username,
&i.AvatarURL,
pq.Array(&i.TemplateIDs),
&i.UsageSeconds,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getUserLatencyInsights = `-- name: GetUserLatencyInsights :many
SELECT
tus.user_id,
u.username,
u.avatar_url,
array_agg(DISTINCT tus.template_id)::uuid[] AS template_ids,
COALESCE((PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY tus.median_latency_ms)), -1)::float AS workspace_connection_latency_50,
COALESCE((PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY tus.median_latency_ms)), -1)::float AS workspace_connection_latency_95
FROM
template_usage_stats tus
JOIN
users u
ON
u.id = tus.user_id
WHERE
tus.start_time >= $1::timestamptz
AND tus.end_time <= $2::timestamptz
AND CASE WHEN COALESCE(array_length($3::uuid[], 1), 0) > 0 THEN tus.template_id = ANY($3::uuid[]) ELSE TRUE END
GROUP BY
tus.user_id, u.username, u.avatar_url
ORDER BY
tus.user_id ASC
`
type GetUserLatencyInsightsParams struct {
StartTime time.Time `db:"start_time" json:"start_time"`
EndTime time.Time `db:"end_time" json:"end_time"`
TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
}
type GetUserLatencyInsightsRow struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
Username string `db:"username" json:"username"`
AvatarURL string `db:"avatar_url" json:"avatar_url"`
TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
WorkspaceConnectionLatency50 float64 `db:"workspace_connection_latency_50" json:"workspace_connection_latency_50"`
WorkspaceConnectionLatency95 float64 `db:"workspace_connection_latency_95" json:"workspace_connection_latency_95"`
}
// GetUserLatencyInsights returns the median and 95th percentile connection
// latency that users have experienced. The result can be filtered on
// template_ids, meaning only user data from workspaces based on those templates
// will be included.
func (q *sqlQuerier) GetUserLatencyInsights(ctx context.Context, arg GetUserLatencyInsightsParams) ([]GetUserLatencyInsightsRow, error) {
rows, err := q.db.QueryContext(ctx, getUserLatencyInsights, arg.StartTime, arg.EndTime, pq.Array(arg.TemplateIDs))
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetUserLatencyInsightsRow
for rows.Next() {
var i GetUserLatencyInsightsRow
if err := rows.Scan(
&i.UserID,
&i.Username,
&i.AvatarURL,
pq.Array(&i.TemplateIDs),
&i.WorkspaceConnectionLatency50,
&i.WorkspaceConnectionLatency95,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getUserStatusCounts = `-- name: GetUserStatusCounts :many
WITH
system_users AS (
SELECT id FROM users WHERE is_system = TRUE
),
-- dates_of_interest generates the dates that will represent the horizontal axis of the chart.
dates_of_interest AS (
SELECT timezone($1::text, gs_local) AS date
FROM generate_series(
timezone($1::text, $2::timestamptz),
timezone($1::text, $3::timestamptz),
interval '1 day'
) AS gs_local
),
-- latest_status_before_range selects the last status of each user before the start_time.
-- This represents the status of all users at the start of the time range.
latest_status_before_range AS (
SELECT
DISTINCT usc.user_id,
usc.new_status,
usc.changed_at
FROM user_status_changes usc
LEFT JOIN LATERAL (
SELECT COUNT(*) > 0 AS deleted
FROM user_deleted ud
WHERE ud.user_id = usc.user_id AND (ud.deleted_at < usc.changed_at OR ud.deleted_at < $2)
) AS ud ON true
WHERE usc.user_id NOT IN (SELECT id FROM system_users)
AND NOT ud.deleted
AND usc.changed_at < $2::timestamptz
ORDER BY usc.user_id, usc.changed_at DESC
),
-- status_changes_during_range selects the statuses of each user during the start_time and end_time.
status_changes_during_range AS (
SELECT
usc.user_id,
usc.new_status,
usc.changed_at
FROM user_status_changes usc
LEFT JOIN LATERAL (
SELECT COUNT(*) > 0 AS deleted
FROM user_deleted ud
WHERE ud.user_id = usc.user_id AND ud.deleted_at < usc.changed_at
) AS ud ON true
WHERE usc.user_id NOT IN (SELECT id FROM system_users)
AND NOT ud.deleted
AND usc.changed_at >= $2::timestamptz
AND usc.changed_at <= $3::timestamptz
),
relevant_status_changes AS (
SELECT user_id, new_status, changed_at
FROM latest_status_before_range
UNION ALL
SELECT user_id, new_status, changed_at
FROM status_changes_during_range
),
-- statuses selects all the distinct statuses that were present just before and during the time range.
-- Each status will have a series on the chart.
statuses AS (
SELECT DISTINCT new_status FROM relevant_status_changes
),
-- ranked_status_change_per_user_per_date selects the latest status change for each user on each date.
-- The last status for a user on every given date will be counted.
ranked_status_change_per_user_per_date AS (
SELECT
d.date,
rsc1.user_id,
ROW_NUMBER() OVER (PARTITION BY d.date, rsc1.user_id ORDER BY rsc1.changed_at DESC) AS rn,
rsc1.new_status
FROM dates_of_interest d
LEFT JOIN relevant_status_changes rsc1 ON rsc1.changed_at <= d.date
)
SELECT
rscpupd.date::timestamptz AS date,
statuses.new_status AS status,
COUNT(rscpupd.user_id) FILTER (
WHERE rscpupd.rn = 1
AND (
rscpupd.new_status = statuses.new_status
AND (
-- Include users who haven't been deleted
NOT EXISTS (SELECT 1 FROM user_deleted WHERE user_id = rscpupd.user_id)
OR
-- Or users whose deletion date is after the current date we're looking at
rscpupd.date < (SELECT deleted_at FROM user_deleted WHERE user_id = rscpupd.user_id)
)
)
) AS count
FROM ranked_status_change_per_user_per_date rscpupd
CROSS JOIN statuses
GROUP BY rscpupd.date, statuses.new_status
ORDER BY rscpupd.date
`
type GetUserStatusCountsParams struct {
Tz string `db:"tz" json:"tz"`
StartTime time.Time `db:"start_time" json:"start_time"`
EndTime time.Time `db:"end_time" json:"end_time"`
}
type GetUserStatusCountsRow struct {
Date time.Time `db:"date" json:"date"`
Status UserStatus `db:"status" json:"status"`
Count int64 `db:"count" json:"count"`
}
// GetUserStatusCounts returns the count of users in each status over time.
// The time range is inclusively defined by the start_time and end_time parameters.
func (q *sqlQuerier) GetUserStatusCounts(ctx context.Context, arg GetUserStatusCountsParams) ([]GetUserStatusCountsRow, error) {
rows, err := q.db.QueryContext(ctx, getUserStatusCounts, arg.Tz, arg.StartTime, arg.EndTime)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetUserStatusCountsRow
for rows.Next() {
var i GetUserStatusCountsRow
if err := rows.Scan(&i.Date, &i.Status, &i.Count); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const upsertTemplateUsageStats = `-- name: UpsertTemplateUsageStats :exec
WITH
latest_start AS (
SELECT
-- Truncate to hour so that we always look at even ranges of data.
date_trunc('hour', COALESCE(
MAX(start_time) - '1 hour'::interval,
-- Fallback when there are no template usage stats yet.
-- App stats can exist before this, but not agent stats,
-- limit the lookback to avoid inconsistency.
(SELECT MIN(created_at) FROM workspace_agent_stats)
)) AS t
FROM
template_usage_stats
),
filtered_app_stats AS (
SELECT
was.workspace_id,
was.user_id,
was.agent_id,
was.access_method,
was.slug_or_port,
was.session_started_at,
was.session_ended_at
FROM
workspace_app_stats AS was
WHERE
was.session_ended_at >= (SELECT t FROM latest_start)
AND was.session_started_at < NOW()
),
workspace_app_stat_buckets AS (
SELECT
-- Truncate the minute to the nearest half hour, this is the bucket size
-- for the data.
date_trunc('hour', s.minute_bucket) + trunc(date_part('minute', s.minute_bucket) / 30) * 30 * '1 minute'::interval AS time_bucket,
w.template_id,
fas.user_id,
-- Both app stats and agent stats track web terminal usage, but
-- by different means. The app stats value should be more
-- accurate so we don't want to discard it just yet.
CASE
WHEN fas.access_method = 'terminal'
THEN '[terminal]' -- Unique name, app names can't contain brackets.
ELSE fas.slug_or_port
END AS app_name,
COUNT(DISTINCT s.minute_bucket) AS app_minutes,
-- Store each unique minute bucket for later merge between datasets.
array_agg(DISTINCT s.minute_bucket) AS minute_buckets
FROM
filtered_app_stats AS fas
JOIN
workspaces AS w
ON
w.id = fas.workspace_id
-- Generate a series of minute buckets for each session for computing the
-- mintes/bucket.
CROSS JOIN
generate_series(
date_trunc('minute', fas.session_started_at),
-- Subtract 1 μs to avoid creating an extra series.
date_trunc('minute', fas.session_ended_at - '1 microsecond'::interval),
'1 minute'::interval
) AS s(minute_bucket)
WHERE
-- s.minute_bucket >= @start_time::timestamptz
-- AND s.minute_bucket < @end_time::timestamptz
s.minute_bucket >= (SELECT t FROM latest_start)
AND s.minute_bucket < NOW()
GROUP BY
time_bucket, w.template_id, fas.user_id, fas.access_method, fas.slug_or_port
),
agent_stats_buckets AS (
SELECT
-- Truncate the minute to the nearest half hour, this is the bucket size
-- for the data.
date_trunc('hour', created_at) + trunc(date_part('minute', created_at) / 30) * 30 * '1 minute'::interval AS time_bucket,
template_id,
user_id,
-- Store each unique minute bucket for later merge between datasets.
array_agg(
DISTINCT CASE
WHEN
session_count_ssh > 0
-- TODO(mafredri): Enable when we have the column.
-- OR session_count_sftp > 0
OR session_count_reconnecting_pty > 0
OR session_count_vscode > 0
OR session_count_jetbrains > 0
THEN
date_trunc('minute', created_at)
ELSE
NULL
END
) AS minute_buckets,
COUNT(DISTINCT CASE WHEN session_count_ssh > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS ssh_mins,
-- TODO(mafredri): Enable when we have the column.
-- COUNT(DISTINCT CASE WHEN session_count_sftp > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS sftp_mins,
COUNT(DISTINCT CASE WHEN session_count_reconnecting_pty > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS reconnecting_pty_mins,
COUNT(DISTINCT CASE WHEN session_count_vscode > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS vscode_mins,
COUNT(DISTINCT CASE WHEN session_count_jetbrains > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS jetbrains_mins,
-- NOTE(mafredri): The agent stats are currently very unreliable, and
-- sometimes the connections are missing, even during active sessions.
-- Since we can't fully rely on this, we check for "any connection
-- during this half-hour". A better solution here would be preferable.
MAX(connection_count) > 0 AS has_connection
FROM
workspace_agent_stats
WHERE
-- created_at >= @start_time::timestamptz
-- AND created_at < @end_time::timestamptz
created_at >= (SELECT t FROM latest_start)
AND created_at < NOW()
-- Inclusion criteria to filter out empty results.
AND (
session_count_ssh > 0
-- TODO(mafredri): Enable when we have the column.
-- OR session_count_sftp > 0
OR session_count_reconnecting_pty > 0
OR session_count_vscode > 0
OR session_count_jetbrains > 0
)
GROUP BY
time_bucket, template_id, user_id
),
stats AS (
SELECT
stats.time_bucket AS start_time,
stats.time_bucket + '30 minutes'::interval AS end_time,
stats.template_id,
stats.user_id,
-- Sum/distinct to handle zero/duplicate values due union and to unnest.
COUNT(DISTINCT minute_bucket) AS usage_mins,
array_agg(DISTINCT minute_bucket) AS minute_buckets,
SUM(DISTINCT stats.ssh_mins) AS ssh_mins,
SUM(DISTINCT stats.sftp_mins) AS sftp_mins,
SUM(DISTINCT stats.reconnecting_pty_mins) AS reconnecting_pty_mins,
SUM(DISTINCT stats.vscode_mins) AS vscode_mins,
SUM(DISTINCT stats.jetbrains_mins) AS jetbrains_mins,
-- This is what we unnested, re-nest as json.
jsonb_object_agg(stats.app_name, stats.app_minutes) FILTER (WHERE stats.app_name IS NOT NULL) AS app_usage_mins
FROM (
SELECT
time_bucket,
template_id,
user_id,
0 AS ssh_mins,
0 AS sftp_mins,
0 AS reconnecting_pty_mins,
0 AS vscode_mins,
0 AS jetbrains_mins,
app_name,
app_minutes,
minute_buckets
FROM
workspace_app_stat_buckets
UNION ALL
SELECT
time_bucket,
template_id,
user_id,
ssh_mins,
-- TODO(mafredri): Enable when we have the column.
0 AS sftp_mins,
reconnecting_pty_mins,
vscode_mins,
jetbrains_mins,
NULL AS app_name,
NULL AS app_minutes,
minute_buckets
FROM
agent_stats_buckets
WHERE
-- See note in the agent_stats_buckets CTE.
has_connection
) AS stats, unnest(minute_buckets) AS minute_bucket
GROUP BY
stats.time_bucket, stats.template_id, stats.user_id
),
minute_buckets AS (
-- Create distinct minute buckets for user-activity, so we can filter out
-- irrelevant latencies.
SELECT DISTINCT ON (stats.start_time, stats.template_id, stats.user_id, minute_bucket)
stats.start_time,
stats.template_id,
stats.user_id,
minute_bucket
FROM
stats, unnest(minute_buckets) AS minute_bucket
),
latencies AS (
-- Select all non-zero latencies for all the minutes that a user used the
-- workspace in some way.
SELECT
mb.start_time,
mb.template_id,
mb.user_id,
-- TODO(mafredri): We're doing medians on medians here, we may want to
-- improve upon this at some point.
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY was.connection_median_latency_ms)::real AS median_latency_ms
FROM
minute_buckets AS mb
JOIN
workspace_agent_stats AS was
ON
was.created_at >= (SELECT t FROM latest_start)
AND was.created_at < NOW()
AND date_trunc('minute', was.created_at) = mb.minute_bucket
AND was.template_id = mb.template_id
AND was.user_id = mb.user_id
AND was.connection_median_latency_ms > 0
GROUP BY
mb.start_time, mb.template_id, mb.user_id
)
INSERT INTO template_usage_stats AS tus (
start_time,
end_time,
template_id,
user_id,
usage_mins,
median_latency_ms,
ssh_mins,
sftp_mins,
reconnecting_pty_mins,
vscode_mins,
jetbrains_mins,
app_usage_mins
) (
SELECT
stats.start_time,
stats.end_time,
stats.template_id,
stats.user_id,
stats.usage_mins,
latencies.median_latency_ms,
stats.ssh_mins,
stats.sftp_mins,
stats.reconnecting_pty_mins,
stats.vscode_mins,
stats.jetbrains_mins,
stats.app_usage_mins
FROM
stats
LEFT JOIN
latencies
ON
-- The latencies group-by ensures there at most one row.
latencies.start_time = stats.start_time
AND latencies.template_id = stats.template_id
AND latencies.user_id = stats.user_id
)
ON CONFLICT
(start_time, template_id, user_id)
DO UPDATE
SET
usage_mins = EXCLUDED.usage_mins,
median_latency_ms = EXCLUDED.median_latency_ms,
ssh_mins = EXCLUDED.ssh_mins,
sftp_mins = EXCLUDED.sftp_mins,
reconnecting_pty_mins = EXCLUDED.reconnecting_pty_mins,
vscode_mins = EXCLUDED.vscode_mins,
jetbrains_mins = EXCLUDED.jetbrains_mins,
app_usage_mins = EXCLUDED.app_usage_mins
WHERE
(tus.*) IS DISTINCT FROM (EXCLUDED.*)
`
// This query aggregates the workspace_agent_stats and workspace_app_stats data
// into a single table for efficient storage and querying. Half-hour buckets are
// used to store the data, and the minutes are summed for each user and template
// combination. The result is stored in the template_usage_stats table.
func (q *sqlQuerier) UpsertTemplateUsageStats(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, upsertTemplateUsageStats)
return err
}
const deleteLicense = `-- name: DeleteLicense :one
DELETE
FROM licenses
WHERE id = $1
RETURNING id
`
func (q *sqlQuerier) DeleteLicense(ctx context.Context, id int32) (int32, error) {
row := q.db.QueryRowContext(ctx, deleteLicense, id)
var id_2 int32
err := row.Scan(&id_2)
return id_2, err
}
const getLicenseByID = `-- name: GetLicenseByID :one
SELECT
id, uploaded_at, jwt, exp, uuid
FROM
licenses
WHERE
id = $1
LIMIT
1
`
func (q *sqlQuerier) GetLicenseByID(ctx context.Context, id int32) (License, error) {
row := q.db.QueryRowContext(ctx, getLicenseByID, id)
var i License
err := row.Scan(
&i.ID,
&i.UploadedAt,
&i.JWT,
&i.Exp,
&i.UUID,
)
return i, err
}
const getLicenses = `-- name: GetLicenses :many
SELECT id, uploaded_at, jwt, exp, uuid
FROM licenses
ORDER BY (id)
`
func (q *sqlQuerier) GetLicenses(ctx context.Context) ([]License, error) {
rows, err := q.db.QueryContext(ctx, getLicenses)
if err != nil {
return nil, err
}
defer rows.Close()
var items []License
for rows.Next() {
var i License
if err := rows.Scan(
&i.ID,
&i.UploadedAt,
&i.JWT,
&i.Exp,
&i.UUID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getUnexpiredLicenses = `-- name: GetUnexpiredLicenses :many
SELECT id, uploaded_at, jwt, exp, uuid
FROM licenses
WHERE exp > NOW()
ORDER BY (id)
`
func (q *sqlQuerier) GetUnexpiredLicenses(ctx context.Context) ([]License, error) {
rows, err := q.db.QueryContext(ctx, getUnexpiredLicenses)
if err != nil {
return nil, err
}
defer rows.Close()
var items []License
for rows.Next() {
var i License
if err := rows.Scan(
&i.ID,
&i.UploadedAt,
&i.JWT,
&i.Exp,
&i.UUID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertLicense = `-- name: InsertLicense :one
INSERT INTO
licenses (
uploaded_at,
jwt,
exp,
uuid
)
VALUES
($1, $2, $3, $4) RETURNING id, uploaded_at, jwt, exp, uuid
`
type InsertLicenseParams struct {
UploadedAt time.Time `db:"uploaded_at" json:"uploaded_at"`
JWT string `db:"jwt" json:"jwt"`
Exp time.Time `db:"exp" json:"exp"`
UUID uuid.UUID `db:"uuid" json:"uuid"`
}
func (q *sqlQuerier) InsertLicense(ctx context.Context, arg InsertLicenseParams) (License, error) {
row := q.db.QueryRowContext(ctx, insertLicense,
arg.UploadedAt,
arg.JWT,
arg.Exp,
arg.UUID,
)
var i License
err := row.Scan(
&i.ID,
&i.UploadedAt,
&i.JWT,
&i.Exp,
&i.UUID,
)
return i, err
}
const acquireLock = `-- name: AcquireLock :exec
SELECT pg_advisory_xact_lock($1)
`
// Blocks until the lock is acquired.
//
// This must be called from within a transaction. The lock will be automatically
// released when the transaction ends.
func (q *sqlQuerier) AcquireLock(ctx context.Context, pgAdvisoryXactLock int64) error {
_, err := q.db.ExecContext(ctx, acquireLock, pgAdvisoryXactLock)
return err
}
const tryAcquireLock = `-- name: TryAcquireLock :one
SELECT pg_try_advisory_xact_lock($1)
`
// 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
// released when the transaction ends.
func (q *sqlQuerier) TryAcquireLock(ctx context.Context, pgTryAdvisoryXactLock int64) (bool, error) {
row := q.db.QueryRowContext(ctx, tryAcquireLock, pgTryAdvisoryXactLock)
var pg_try_advisory_xact_lock bool
err := row.Scan(&pg_try_advisory_xact_lock)
return pg_try_advisory_xact_lock, err
}
const cleanupDeletedMCPServerIDsFromChats = `-- name: CleanupDeletedMCPServerIDsFromChats :exec
UPDATE chats
SET mcp_server_ids = (
SELECT COALESCE(array_agg(sid), '{}')
FROM unnest(chats.mcp_server_ids) AS sid
WHERE sid IN (SELECT id FROM mcp_server_configs)
)
WHERE mcp_server_ids != '{}'
AND NOT (mcp_server_ids <@ COALESCE((SELECT array_agg(id) FROM mcp_server_configs), '{}'))
`
func (q *sqlQuerier) CleanupDeletedMCPServerIDsFromChats(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, cleanupDeletedMCPServerIDsFromChats)
return err
}
const deleteMCPServerConfigByID = `-- name: DeleteMCPServerConfigByID :exec
DELETE FROM
mcp_server_configs
WHERE
id = $1::uuid
`
func (q *sqlQuerier) DeleteMCPServerConfigByID(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteMCPServerConfigByID, id)
return err
}
const deleteMCPServerUserToken = `-- name: DeleteMCPServerUserToken :exec
DELETE FROM
mcp_server_user_tokens
WHERE
mcp_server_config_id = $1::uuid
AND user_id = $2::uuid
`
type DeleteMCPServerUserTokenParams struct {
MCPServerConfigID uuid.UUID `db:"mcp_server_config_id" json:"mcp_server_config_id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
}
func (q *sqlQuerier) DeleteMCPServerUserToken(ctx context.Context, arg DeleteMCPServerUserTokenParams) error {
_, err := q.db.ExecContext(ctx, deleteMCPServerUserToken, arg.MCPServerConfigID, arg.UserID)
return err
}
const getEnabledMCPServerConfigs = `-- name: GetEnabledMCPServerConfigs :many
SELECT
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers, oauth2_revocation_url
FROM
mcp_server_configs
WHERE
enabled = TRUE
ORDER BY
display_name ASC
`
func (q *sqlQuerier) GetEnabledMCPServerConfigs(ctx context.Context) ([]MCPServerConfig, error) {
rows, err := q.db.QueryContext(ctx, getEnabledMCPServerConfigs)
if err != nil {
return nil, err
}
defer rows.Close()
var items []MCPServerConfig
for rows.Next() {
var i MCPServerConfig
if err := rows.Scan(
&i.ID,
&i.DisplayName,
&i.Slug,
&i.Description,
&i.IconURL,
&i.Transport,
&i.Url,
&i.AuthType,
&i.OAuth2ClientID,
&i.OAuth2ClientSecret,
&i.OAuth2ClientSecretKeyID,
&i.OAuth2AuthURL,
&i.OAuth2TokenURL,
&i.OAuth2Scopes,
&i.APIKeyHeader,
&i.APIKeyValue,
&i.APIKeyValueKeyID,
&i.CustomHeaders,
&i.CustomHeadersKeyID,
pq.Array(&i.ToolAllowList),
pq.Array(&i.ToolDenyList),
&i.Availability,
&i.Enabled,
&i.CreatedBy,
&i.UpdatedBy,
&i.CreatedAt,
&i.UpdatedAt,
&i.ModelIntent,
&i.AllowInPlanMode,
&i.ForwardCoderHeaders,
&i.OAuth2RevocationURL,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getForcedMCPServerConfigs = `-- name: GetForcedMCPServerConfigs :many
SELECT
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers, oauth2_revocation_url
FROM
mcp_server_configs
WHERE
enabled = TRUE
AND availability = 'force_on'
ORDER BY
display_name ASC
`
func (q *sqlQuerier) GetForcedMCPServerConfigs(ctx context.Context) ([]MCPServerConfig, error) {
rows, err := q.db.QueryContext(ctx, getForcedMCPServerConfigs)
if err != nil {
return nil, err
}
defer rows.Close()
var items []MCPServerConfig
for rows.Next() {
var i MCPServerConfig
if err := rows.Scan(
&i.ID,
&i.DisplayName,
&i.Slug,
&i.Description,
&i.IconURL,
&i.Transport,
&i.Url,
&i.AuthType,
&i.OAuth2ClientID,
&i.OAuth2ClientSecret,
&i.OAuth2ClientSecretKeyID,
&i.OAuth2AuthURL,
&i.OAuth2TokenURL,
&i.OAuth2Scopes,
&i.APIKeyHeader,
&i.APIKeyValue,
&i.APIKeyValueKeyID,
&i.CustomHeaders,
&i.CustomHeadersKeyID,
pq.Array(&i.ToolAllowList),
pq.Array(&i.ToolDenyList),
&i.Availability,
&i.Enabled,
&i.CreatedBy,
&i.UpdatedBy,
&i.CreatedAt,
&i.UpdatedAt,
&i.ModelIntent,
&i.AllowInPlanMode,
&i.ForwardCoderHeaders,
&i.OAuth2RevocationURL,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getMCPServerConfigByID = `-- name: GetMCPServerConfigByID :one
SELECT
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers, oauth2_revocation_url
FROM
mcp_server_configs
WHERE
id = $1::uuid
`
func (q *sqlQuerier) GetMCPServerConfigByID(ctx context.Context, id uuid.UUID) (MCPServerConfig, error) {
row := q.db.QueryRowContext(ctx, getMCPServerConfigByID, id)
var i MCPServerConfig
err := row.Scan(
&i.ID,
&i.DisplayName,
&i.Slug,
&i.Description,
&i.IconURL,
&i.Transport,
&i.Url,
&i.AuthType,
&i.OAuth2ClientID,
&i.OAuth2ClientSecret,
&i.OAuth2ClientSecretKeyID,
&i.OAuth2AuthURL,
&i.OAuth2TokenURL,
&i.OAuth2Scopes,
&i.APIKeyHeader,
&i.APIKeyValue,
&i.APIKeyValueKeyID,
&i.CustomHeaders,
&i.CustomHeadersKeyID,
pq.Array(&i.ToolAllowList),
pq.Array(&i.ToolDenyList),
&i.Availability,
&i.Enabled,
&i.CreatedBy,
&i.UpdatedBy,
&i.CreatedAt,
&i.UpdatedAt,
&i.ModelIntent,
&i.AllowInPlanMode,
&i.ForwardCoderHeaders,
&i.OAuth2RevocationURL,
)
return i, err
}
const getMCPServerConfigBySlug = `-- name: GetMCPServerConfigBySlug :one
SELECT
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers, oauth2_revocation_url
FROM
mcp_server_configs
WHERE
slug = $1::text
`
func (q *sqlQuerier) GetMCPServerConfigBySlug(ctx context.Context, slug string) (MCPServerConfig, error) {
row := q.db.QueryRowContext(ctx, getMCPServerConfigBySlug, slug)
var i MCPServerConfig
err := row.Scan(
&i.ID,
&i.DisplayName,
&i.Slug,
&i.Description,
&i.IconURL,
&i.Transport,
&i.Url,
&i.AuthType,
&i.OAuth2ClientID,
&i.OAuth2ClientSecret,
&i.OAuth2ClientSecretKeyID,
&i.OAuth2AuthURL,
&i.OAuth2TokenURL,
&i.OAuth2Scopes,
&i.APIKeyHeader,
&i.APIKeyValue,
&i.APIKeyValueKeyID,
&i.CustomHeaders,
&i.CustomHeadersKeyID,
pq.Array(&i.ToolAllowList),
pq.Array(&i.ToolDenyList),
&i.Availability,
&i.Enabled,
&i.CreatedBy,
&i.UpdatedBy,
&i.CreatedAt,
&i.UpdatedAt,
&i.ModelIntent,
&i.AllowInPlanMode,
&i.ForwardCoderHeaders,
&i.OAuth2RevocationURL,
)
return i, err
}
const getMCPServerConfigs = `-- name: GetMCPServerConfigs :many
SELECT
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers, oauth2_revocation_url
FROM
mcp_server_configs
ORDER BY
display_name ASC
`
func (q *sqlQuerier) GetMCPServerConfigs(ctx context.Context) ([]MCPServerConfig, error) {
rows, err := q.db.QueryContext(ctx, getMCPServerConfigs)
if err != nil {
return nil, err
}
defer rows.Close()
var items []MCPServerConfig
for rows.Next() {
var i MCPServerConfig
if err := rows.Scan(
&i.ID,
&i.DisplayName,
&i.Slug,
&i.Description,
&i.IconURL,
&i.Transport,
&i.Url,
&i.AuthType,
&i.OAuth2ClientID,
&i.OAuth2ClientSecret,
&i.OAuth2ClientSecretKeyID,
&i.OAuth2AuthURL,
&i.OAuth2TokenURL,
&i.OAuth2Scopes,
&i.APIKeyHeader,
&i.APIKeyValue,
&i.APIKeyValueKeyID,
&i.CustomHeaders,
&i.CustomHeadersKeyID,
pq.Array(&i.ToolAllowList),
pq.Array(&i.ToolDenyList),
&i.Availability,
&i.Enabled,
&i.CreatedBy,
&i.UpdatedBy,
&i.CreatedAt,
&i.UpdatedAt,
&i.ModelIntent,
&i.AllowInPlanMode,
&i.ForwardCoderHeaders,
&i.OAuth2RevocationURL,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getMCPServerConfigsByIDs = `-- name: GetMCPServerConfigsByIDs :many
SELECT
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers, oauth2_revocation_url
FROM
mcp_server_configs
WHERE
id = ANY($1::uuid[])
ORDER BY
display_name ASC
`
func (q *sqlQuerier) GetMCPServerConfigsByIDs(ctx context.Context, ids []uuid.UUID) ([]MCPServerConfig, error) {
rows, err := q.db.QueryContext(ctx, getMCPServerConfigsByIDs, pq.Array(ids))
if err != nil {
return nil, err
}
defer rows.Close()
var items []MCPServerConfig
for rows.Next() {
var i MCPServerConfig
if err := rows.Scan(
&i.ID,
&i.DisplayName,
&i.Slug,
&i.Description,
&i.IconURL,
&i.Transport,
&i.Url,
&i.AuthType,
&i.OAuth2ClientID,
&i.OAuth2ClientSecret,
&i.OAuth2ClientSecretKeyID,
&i.OAuth2AuthURL,
&i.OAuth2TokenURL,
&i.OAuth2Scopes,
&i.APIKeyHeader,
&i.APIKeyValue,
&i.APIKeyValueKeyID,
&i.CustomHeaders,
&i.CustomHeadersKeyID,
pq.Array(&i.ToolAllowList),
pq.Array(&i.ToolDenyList),
&i.Availability,
&i.Enabled,
&i.CreatedBy,
&i.UpdatedBy,
&i.CreatedAt,
&i.UpdatedAt,
&i.ModelIntent,
&i.AllowInPlanMode,
&i.ForwardCoderHeaders,
&i.OAuth2RevocationURL,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getMCPServerUserToken = `-- name: GetMCPServerUserToken :one
SELECT
id, mcp_server_config_id, user_id, access_token, access_token_key_id, refresh_token, refresh_token_key_id, token_type, expiry, created_at, updated_at, oauth_refresh_failure_reason
FROM
mcp_server_user_tokens
WHERE
mcp_server_config_id = $1::uuid
AND user_id = $2::uuid
`
type GetMCPServerUserTokenParams struct {
MCPServerConfigID uuid.UUID `db:"mcp_server_config_id" json:"mcp_server_config_id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
}
func (q *sqlQuerier) GetMCPServerUserToken(ctx context.Context, arg GetMCPServerUserTokenParams) (MCPServerUserToken, error) {
row := q.db.QueryRowContext(ctx, getMCPServerUserToken, arg.MCPServerConfigID, arg.UserID)
var i MCPServerUserToken
err := row.Scan(
&i.ID,
&i.MCPServerConfigID,
&i.UserID,
&i.AccessToken,
&i.AccessTokenKeyID,
&i.RefreshToken,
&i.RefreshTokenKeyID,
&i.TokenType,
&i.Expiry,
&i.CreatedAt,
&i.UpdatedAt,
&i.OauthRefreshFailureReason,
)
return i, err
}
const getMCPServerUserTokensByUserID = `-- name: GetMCPServerUserTokensByUserID :many
SELECT
id, mcp_server_config_id, user_id, access_token, access_token_key_id, refresh_token, refresh_token_key_id, token_type, expiry, created_at, updated_at, oauth_refresh_failure_reason
FROM
mcp_server_user_tokens
WHERE
user_id = $1::uuid
`
func (q *sqlQuerier) GetMCPServerUserTokensByUserID(ctx context.Context, userID uuid.UUID) ([]MCPServerUserToken, error) {
rows, err := q.db.QueryContext(ctx, getMCPServerUserTokensByUserID, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []MCPServerUserToken
for rows.Next() {
var i MCPServerUserToken
if err := rows.Scan(
&i.ID,
&i.MCPServerConfigID,
&i.UserID,
&i.AccessToken,
&i.AccessTokenKeyID,
&i.RefreshToken,
&i.RefreshTokenKeyID,
&i.TokenType,
&i.Expiry,
&i.CreatedAt,
&i.UpdatedAt,
&i.OauthRefreshFailureReason,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertMCPServerConfig = `-- name: InsertMCPServerConfig :one
INSERT INTO mcp_server_configs (
display_name,
slug,
description,
icon_url,
transport,
url,
auth_type,
oauth2_client_id,
oauth2_client_secret,
oauth2_client_secret_key_id,
oauth2_auth_url,
oauth2_token_url,
oauth2_revocation_url,
oauth2_scopes,
api_key_header,
api_key_value,
api_key_value_key_id,
custom_headers,
custom_headers_key_id,
tool_allow_list,
tool_deny_list,
availability,
enabled,
model_intent,
allow_in_plan_mode,
forward_coder_headers,
created_by,
updated_by
) VALUES (
$1::text,
$2::text,
$3::text,
$4::text,
$5::text,
$6::text,
$7::text,
$8::text,
$9::text,
$10::text,
$11::text,
$12::text,
$13::text,
$14::text,
$15::text,
$16::text,
$17::text,
$18::text,
$19::text,
$20::text[],
$21::text[],
$22::text,
$23::boolean,
$24::boolean,
$25::boolean,
$26::boolean,
$27::uuid,
$28::uuid
)
RETURNING
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers, oauth2_revocation_url
`
type InsertMCPServerConfigParams struct {
DisplayName string `db:"display_name" json:"display_name"`
Slug string `db:"slug" json:"slug"`
Description string `db:"description" json:"description"`
IconURL string `db:"icon_url" json:"icon_url"`
Transport string `db:"transport" json:"transport"`
Url string `db:"url" json:"url"`
AuthType string `db:"auth_type" json:"auth_type"`
OAuth2ClientID string `db:"oauth2_client_id" json:"oauth2_client_id"`
OAuth2ClientSecret string `db:"oauth2_client_secret" json:"oauth2_client_secret"`
OAuth2ClientSecretKeyID sql.NullString `db:"oauth2_client_secret_key_id" json:"oauth2_client_secret_key_id"`
OAuth2AuthURL string `db:"oauth2_auth_url" json:"oauth2_auth_url"`
OAuth2TokenURL string `db:"oauth2_token_url" json:"oauth2_token_url"`
OAuth2RevocationURL string `db:"oauth2_revocation_url" json:"oauth2_revocation_url"`
OAuth2Scopes string `db:"oauth2_scopes" json:"oauth2_scopes"`
APIKeyHeader string `db:"api_key_header" json:"api_key_header"`
APIKeyValue string `db:"api_key_value" json:"api_key_value"`
APIKeyValueKeyID sql.NullString `db:"api_key_value_key_id" json:"api_key_value_key_id"`
CustomHeaders string `db:"custom_headers" json:"custom_headers"`
CustomHeadersKeyID sql.NullString `db:"custom_headers_key_id" json:"custom_headers_key_id"`
ToolAllowList []string `db:"tool_allow_list" json:"tool_allow_list"`
ToolDenyList []string `db:"tool_deny_list" json:"tool_deny_list"`
Availability string `db:"availability" json:"availability"`
Enabled bool `db:"enabled" json:"enabled"`
ModelIntent bool `db:"model_intent" json:"model_intent"`
AllowInPlanMode bool `db:"allow_in_plan_mode" json:"allow_in_plan_mode"`
ForwardCoderHeaders bool `db:"forward_coder_headers" json:"forward_coder_headers"`
CreatedBy uuid.UUID `db:"created_by" json:"created_by"`
UpdatedBy uuid.UUID `db:"updated_by" json:"updated_by"`
}
func (q *sqlQuerier) InsertMCPServerConfig(ctx context.Context, arg InsertMCPServerConfigParams) (MCPServerConfig, error) {
row := q.db.QueryRowContext(ctx, insertMCPServerConfig,
arg.DisplayName,
arg.Slug,
arg.Description,
arg.IconURL,
arg.Transport,
arg.Url,
arg.AuthType,
arg.OAuth2ClientID,
arg.OAuth2ClientSecret,
arg.OAuth2ClientSecretKeyID,
arg.OAuth2AuthURL,
arg.OAuth2TokenURL,
arg.OAuth2RevocationURL,
arg.OAuth2Scopes,
arg.APIKeyHeader,
arg.APIKeyValue,
arg.APIKeyValueKeyID,
arg.CustomHeaders,
arg.CustomHeadersKeyID,
pq.Array(arg.ToolAllowList),
pq.Array(arg.ToolDenyList),
arg.Availability,
arg.Enabled,
arg.ModelIntent,
arg.AllowInPlanMode,
arg.ForwardCoderHeaders,
arg.CreatedBy,
arg.UpdatedBy,
)
var i MCPServerConfig
err := row.Scan(
&i.ID,
&i.DisplayName,
&i.Slug,
&i.Description,
&i.IconURL,
&i.Transport,
&i.Url,
&i.AuthType,
&i.OAuth2ClientID,
&i.OAuth2ClientSecret,
&i.OAuth2ClientSecretKeyID,
&i.OAuth2AuthURL,
&i.OAuth2TokenURL,
&i.OAuth2Scopes,
&i.APIKeyHeader,
&i.APIKeyValue,
&i.APIKeyValueKeyID,
&i.CustomHeaders,
&i.CustomHeadersKeyID,
pq.Array(&i.ToolAllowList),
pq.Array(&i.ToolDenyList),
&i.Availability,
&i.Enabled,
&i.CreatedBy,
&i.UpdatedBy,
&i.CreatedAt,
&i.UpdatedAt,
&i.ModelIntent,
&i.AllowInPlanMode,
&i.ForwardCoderHeaders,
&i.OAuth2RevocationURL,
)
return i, err
}
const markMCPServerUserTokenRefreshFailure = `-- name: MarkMCPServerUserTokenRefreshFailure :one
UPDATE mcp_server_user_tokens
SET
access_token = '',
access_token_key_id = NULL,
refresh_token = '',
refresh_token_key_id = NULL,
expiry = NULL,
oauth_refresh_failure_reason = $1::text,
updated_at = NOW()
WHERE
id = $2::uuid
AND updated_at = $3::timestamptz
RETURNING
id, mcp_server_config_id, user_id, access_token, access_token_key_id, refresh_token, refresh_token_key_id, token_type, expiry, created_at, updated_at, oauth_refresh_failure_reason
`
type MarkMCPServerUserTokenRefreshFailureParams struct {
OauthRefreshFailureReason string `db:"oauth_refresh_failure_reason" json:"oauth_refresh_failure_reason"`
ID uuid.UUID `db:"id" json:"id"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
// Records a permanent refresh failure (e.g. revoked grant) and clears
// the dead token material so it is never attached to a request again.
// The updated_at predicate provides optimistic concurrency: if another
// request refreshed or replaced the token since it was read, this
// update matches zero rows and returns sql.ErrNoRows.
func (q *sqlQuerier) MarkMCPServerUserTokenRefreshFailure(ctx context.Context, arg MarkMCPServerUserTokenRefreshFailureParams) (MCPServerUserToken, error) {
row := q.db.QueryRowContext(ctx, markMCPServerUserTokenRefreshFailure, arg.OauthRefreshFailureReason, arg.ID, arg.UpdatedAt)
var i MCPServerUserToken
err := row.Scan(
&i.ID,
&i.MCPServerConfigID,
&i.UserID,
&i.AccessToken,
&i.AccessTokenKeyID,
&i.RefreshToken,
&i.RefreshTokenKeyID,
&i.TokenType,
&i.Expiry,
&i.CreatedAt,
&i.UpdatedAt,
&i.OauthRefreshFailureReason,
)
return i, err
}
const updateMCPServerConfig = `-- name: UpdateMCPServerConfig :one
UPDATE
mcp_server_configs
SET
display_name = $1::text,
slug = $2::text,
description = $3::text,
icon_url = $4::text,
transport = $5::text,
url = $6::text,
auth_type = $7::text,
oauth2_client_id = $8::text,
oauth2_client_secret = $9::text,
oauth2_client_secret_key_id = $10::text,
oauth2_auth_url = $11::text,
oauth2_token_url = $12::text,
oauth2_revocation_url = $13::text,
oauth2_scopes = $14::text,
api_key_header = $15::text,
api_key_value = $16::text,
api_key_value_key_id = $17::text,
custom_headers = $18::text,
custom_headers_key_id = $19::text,
tool_allow_list = $20::text[],
tool_deny_list = $21::text[],
availability = $22::text,
enabled = $23::boolean,
model_intent = $24::boolean,
allow_in_plan_mode = $25::boolean,
forward_coder_headers = $26::boolean,
updated_by = $27::uuid,
updated_at = NOW()
WHERE
id = $28::uuid
RETURNING
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers, oauth2_revocation_url
`
type UpdateMCPServerConfigParams struct {
DisplayName string `db:"display_name" json:"display_name"`
Slug string `db:"slug" json:"slug"`
Description string `db:"description" json:"description"`
IconURL string `db:"icon_url" json:"icon_url"`
Transport string `db:"transport" json:"transport"`
Url string `db:"url" json:"url"`
AuthType string `db:"auth_type" json:"auth_type"`
OAuth2ClientID string `db:"oauth2_client_id" json:"oauth2_client_id"`
OAuth2ClientSecret string `db:"oauth2_client_secret" json:"oauth2_client_secret"`
OAuth2ClientSecretKeyID sql.NullString `db:"oauth2_client_secret_key_id" json:"oauth2_client_secret_key_id"`
OAuth2AuthURL string `db:"oauth2_auth_url" json:"oauth2_auth_url"`
OAuth2TokenURL string `db:"oauth2_token_url" json:"oauth2_token_url"`
OAuth2RevocationURL string `db:"oauth2_revocation_url" json:"oauth2_revocation_url"`
OAuth2Scopes string `db:"oauth2_scopes" json:"oauth2_scopes"`
APIKeyHeader string `db:"api_key_header" json:"api_key_header"`
APIKeyValue string `db:"api_key_value" json:"api_key_value"`
APIKeyValueKeyID sql.NullString `db:"api_key_value_key_id" json:"api_key_value_key_id"`
CustomHeaders string `db:"custom_headers" json:"custom_headers"`
CustomHeadersKeyID sql.NullString `db:"custom_headers_key_id" json:"custom_headers_key_id"`
ToolAllowList []string `db:"tool_allow_list" json:"tool_allow_list"`
ToolDenyList []string `db:"tool_deny_list" json:"tool_deny_list"`
Availability string `db:"availability" json:"availability"`
Enabled bool `db:"enabled" json:"enabled"`
ModelIntent bool `db:"model_intent" json:"model_intent"`
AllowInPlanMode bool `db:"allow_in_plan_mode" json:"allow_in_plan_mode"`
ForwardCoderHeaders bool `db:"forward_coder_headers" json:"forward_coder_headers"`
UpdatedBy uuid.UUID `db:"updated_by" json:"updated_by"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateMCPServerConfig(ctx context.Context, arg UpdateMCPServerConfigParams) (MCPServerConfig, error) {
row := q.db.QueryRowContext(ctx, updateMCPServerConfig,
arg.DisplayName,
arg.Slug,
arg.Description,
arg.IconURL,
arg.Transport,
arg.Url,
arg.AuthType,
arg.OAuth2ClientID,
arg.OAuth2ClientSecret,
arg.OAuth2ClientSecretKeyID,
arg.OAuth2AuthURL,
arg.OAuth2TokenURL,
arg.OAuth2RevocationURL,
arg.OAuth2Scopes,
arg.APIKeyHeader,
arg.APIKeyValue,
arg.APIKeyValueKeyID,
arg.CustomHeaders,
arg.CustomHeadersKeyID,
pq.Array(arg.ToolAllowList),
pq.Array(arg.ToolDenyList),
arg.Availability,
arg.Enabled,
arg.ModelIntent,
arg.AllowInPlanMode,
arg.ForwardCoderHeaders,
arg.UpdatedBy,
arg.ID,
)
var i MCPServerConfig
err := row.Scan(
&i.ID,
&i.DisplayName,
&i.Slug,
&i.Description,
&i.IconURL,
&i.Transport,
&i.Url,
&i.AuthType,
&i.OAuth2ClientID,
&i.OAuth2ClientSecret,
&i.OAuth2ClientSecretKeyID,
&i.OAuth2AuthURL,
&i.OAuth2TokenURL,
&i.OAuth2Scopes,
&i.APIKeyHeader,
&i.APIKeyValue,
&i.APIKeyValueKeyID,
&i.CustomHeaders,
&i.CustomHeadersKeyID,
pq.Array(&i.ToolAllowList),
pq.Array(&i.ToolDenyList),
&i.Availability,
&i.Enabled,
&i.CreatedBy,
&i.UpdatedBy,
&i.CreatedAt,
&i.UpdatedAt,
&i.ModelIntent,
&i.AllowInPlanMode,
&i.ForwardCoderHeaders,
&i.OAuth2RevocationURL,
)
return i, err
}
const updateMCPServerUserTokenFromRefresh = `-- name: UpdateMCPServerUserTokenFromRefresh :one
UPDATE mcp_server_user_tokens
SET
access_token = $1::text,
access_token_key_id = $2::text,
refresh_token = $3::text,
refresh_token_key_id = $4::text,
token_type = $5::text,
expiry = $6::timestamptz,
oauth_refresh_failure_reason = '',
updated_at = NOW()
WHERE
id = $7::uuid
AND updated_at = $8::timestamptz
RETURNING
id, mcp_server_config_id, user_id, access_token, access_token_key_id, refresh_token, refresh_token_key_id, token_type, expiry, created_at, updated_at, oauth_refresh_failure_reason
`
type UpdateMCPServerUserTokenFromRefreshParams struct {
AccessToken string `db:"access_token" json:"access_token"`
AccessTokenKeyID sql.NullString `db:"access_token_key_id" json:"access_token_key_id"`
RefreshToken string `db:"refresh_token" json:"refresh_token"`
RefreshTokenKeyID sql.NullString `db:"refresh_token_key_id" json:"refresh_token_key_id"`
TokenType string `db:"token_type" json:"token_type"`
Expiry sql.NullTime `db:"expiry" json:"expiry"`
ID uuid.UUID `db:"id" json:"id"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
// Refresh persistence must not recreate a token deleted by disconnect.
// The optimistic lock also prevents stale refreshes from replacing newer tokens.
func (q *sqlQuerier) UpdateMCPServerUserTokenFromRefresh(ctx context.Context, arg UpdateMCPServerUserTokenFromRefreshParams) (MCPServerUserToken, error) {
row := q.db.QueryRowContext(ctx, updateMCPServerUserTokenFromRefresh,
arg.AccessToken,
arg.AccessTokenKeyID,
arg.RefreshToken,
arg.RefreshTokenKeyID,
arg.TokenType,
arg.Expiry,
arg.ID,
arg.UpdatedAt,
)
var i MCPServerUserToken
err := row.Scan(
&i.ID,
&i.MCPServerConfigID,
&i.UserID,
&i.AccessToken,
&i.AccessTokenKeyID,
&i.RefreshToken,
&i.RefreshTokenKeyID,
&i.TokenType,
&i.Expiry,
&i.CreatedAt,
&i.UpdatedAt,
&i.OauthRefreshFailureReason,
)
return i, err
}
const upsertMCPServerUserToken = `-- name: UpsertMCPServerUserToken :one
INSERT INTO mcp_server_user_tokens (
mcp_server_config_id,
user_id,
access_token,
access_token_key_id,
refresh_token,
refresh_token_key_id,
token_type,
expiry
) VALUES (
$1::uuid,
$2::uuid,
$3::text,
$4::text,
$5::text,
$6::text,
$7::text,
$8::timestamptz
)
ON CONFLICT (mcp_server_config_id, user_id) DO UPDATE SET
access_token = $3::text,
access_token_key_id = $4::text,
refresh_token = $5::text,
refresh_token_key_id = $6::text,
token_type = $7::text,
expiry = $8::timestamptz,
-- New token material means the user re-authenticated, so any
-- cached permanent refresh failure no longer applies.
oauth_refresh_failure_reason = '',
updated_at = NOW()
RETURNING
id, mcp_server_config_id, user_id, access_token, access_token_key_id, refresh_token, refresh_token_key_id, token_type, expiry, created_at, updated_at, oauth_refresh_failure_reason
`
type UpsertMCPServerUserTokenParams struct {
MCPServerConfigID uuid.UUID `db:"mcp_server_config_id" json:"mcp_server_config_id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
AccessToken string `db:"access_token" json:"access_token"`
AccessTokenKeyID sql.NullString `db:"access_token_key_id" json:"access_token_key_id"`
RefreshToken string `db:"refresh_token" json:"refresh_token"`
RefreshTokenKeyID sql.NullString `db:"refresh_token_key_id" json:"refresh_token_key_id"`
TokenType string `db:"token_type" json:"token_type"`
Expiry sql.NullTime `db:"expiry" json:"expiry"`
}
func (q *sqlQuerier) UpsertMCPServerUserToken(ctx context.Context, arg UpsertMCPServerUserTokenParams) (MCPServerUserToken, error) {
row := q.db.QueryRowContext(ctx, upsertMCPServerUserToken,
arg.MCPServerConfigID,
arg.UserID,
arg.AccessToken,
arg.AccessTokenKeyID,
arg.RefreshToken,
arg.RefreshTokenKeyID,
arg.TokenType,
arg.Expiry,
)
var i MCPServerUserToken
err := row.Scan(
&i.ID,
&i.MCPServerConfigID,
&i.UserID,
&i.AccessToken,
&i.AccessTokenKeyID,
&i.RefreshToken,
&i.RefreshTokenKeyID,
&i.TokenType,
&i.Expiry,
&i.CreatedAt,
&i.UpdatedAt,
&i.OauthRefreshFailureReason,
)
return i, err
}
const acquireNotificationMessages = `-- name: AcquireNotificationMessages :many
WITH acquired AS (
UPDATE
notification_messages
SET queued_seconds = GREATEST(0, EXTRACT(EPOCH FROM (NOW() - updated_at)))::FLOAT,
updated_at = NOW(),
status = 'leased'::notification_message_status,
status_reason = 'Leased by notifier ' || $1::uuid,
leased_until = NOW() + CONCAT($2::int, ' seconds')::interval
WHERE id IN (SELECT nm.id
FROM notification_messages AS nm
WHERE (
(
-- message is in acquirable states
nm.status IN (
'pending'::notification_message_status,
'temporary_failure'::notification_message_status
)
)
-- or somehow the message was left in leased for longer than its lease period
OR (
nm.status = 'leased'::notification_message_status
AND nm.leased_until < NOW()
)
)
AND (
-- exclude all messages which have exceeded the max attempts; these will be purged later
nm.attempt_count IS NULL OR nm.attempt_count < $3::int
)
-- if set, do not retry until we've exceeded the wait time
AND (
CASE
WHEN nm.next_retry_after IS NOT NULL THEN nm.next_retry_after < NOW()
ELSE true
END
)
ORDER BY nm.created_at ASC
-- Ensure that multiple concurrent readers cannot retrieve the same rows
FOR UPDATE OF nm
SKIP LOCKED
LIMIT $4)
RETURNING id, notification_template_id, user_id, method, status, status_reason, created_by, payload, attempt_count, targets, created_at, updated_at, leased_until, next_retry_after, queued_seconds, dedupe_hash)
SELECT
-- message
nm.id,
nm.payload,
nm.method,
nm.attempt_count::int AS attempt_count,
nm.queued_seconds::float AS queued_seconds,
-- template
nt.id AS template_id,
nt.title_template,
nt.body_template,
-- preferences
(CASE WHEN np.disabled IS NULL THEN false ELSE np.disabled END)::bool AS disabled
FROM acquired nm
JOIN notification_templates nt ON nm.notification_template_id = nt.id
LEFT JOIN notification_preferences AS np
ON (np.user_id = nm.user_id AND np.notification_template_id = nm.notification_template_id)
`
type AcquireNotificationMessagesParams struct {
NotifierID uuid.UUID `db:"notifier_id" json:"notifier_id"`
LeaseSeconds int32 `db:"lease_seconds" json:"lease_seconds"`
MaxAttemptCount int32 `db:"max_attempt_count" json:"max_attempt_count"`
Count int32 `db:"count" json:"count"`
}
type AcquireNotificationMessagesRow struct {
ID uuid.UUID `db:"id" json:"id"`
Payload json.RawMessage `db:"payload" json:"payload"`
Method NotificationMethod `db:"method" json:"method"`
AttemptCount int32 `db:"attempt_count" json:"attempt_count"`
QueuedSeconds float64 `db:"queued_seconds" json:"queued_seconds"`
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
TitleTemplate string `db:"title_template" json:"title_template"`
BodyTemplate string `db:"body_template" json:"body_template"`
Disabled bool `db:"disabled" json:"disabled"`
}
// Acquires the lease for a given count of notification messages, to enable concurrent dequeuing and subsequent sending.
// Only rows that aren't already leased (or ones which are leased but have exceeded their lease period) are returned.
//
// A "lease" here refers to a notifier taking ownership of a notification_messages row. A lease survives for the duration
// of CODER_NOTIFICATIONS_LEASE_PERIOD. Once a message is delivered, its status is updated and the lease expires (set to NULL).
// If a message exceeds its lease, that implies the notifier did not shutdown cleanly, or the table update failed somehow,
// and the row will then be eligible to be dequeued by another notifier.
//
// SKIP LOCKED is used to jump over locked rows. This prevents multiple notifiers from acquiring the same messages.
// See: https://www.postgresql.org/docs/9.5/sql-select.html#SQL-FOR-UPDATE-SHARE
func (q *sqlQuerier) AcquireNotificationMessages(ctx context.Context, arg AcquireNotificationMessagesParams) ([]AcquireNotificationMessagesRow, error) {
rows, err := q.db.QueryContext(ctx, acquireNotificationMessages,
arg.NotifierID,
arg.LeaseSeconds,
arg.MaxAttemptCount,
arg.Count,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []AcquireNotificationMessagesRow
for rows.Next() {
var i AcquireNotificationMessagesRow
if err := rows.Scan(
&i.ID,
&i.Payload,
&i.Method,
&i.AttemptCount,
&i.QueuedSeconds,
&i.TemplateID,
&i.TitleTemplate,
&i.BodyTemplate,
&i.Disabled,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const bulkMarkNotificationMessagesFailed = `-- name: BulkMarkNotificationMessagesFailed :execrows
UPDATE notification_messages
SET queued_seconds = 0,
updated_at = subquery.failed_at,
attempt_count = attempt_count + 1,
status = CASE
WHEN attempt_count + 1 < $1::int THEN subquery.status
ELSE 'permanent_failure'::notification_message_status END,
status_reason = subquery.status_reason,
leased_until = NULL,
next_retry_after = CASE
WHEN (attempt_count + 1 < $1::int)
THEN NOW() + CONCAT($2::int, ' seconds')::interval END
FROM (SELECT UNNEST($3::uuid[]) AS id,
UNNEST($4::timestamptz[]) AS failed_at,
UNNEST($5::notification_message_status[]) AS status,
UNNEST($6::text[]) AS status_reason) AS subquery
WHERE notification_messages.id = subquery.id
`
type BulkMarkNotificationMessagesFailedParams struct {
MaxAttempts int32 `db:"max_attempts" json:"max_attempts"`
RetryInterval int32 `db:"retry_interval" json:"retry_interval"`
IDs []uuid.UUID `db:"ids" json:"ids"`
FailedAts []time.Time `db:"failed_ats" json:"failed_ats"`
Statuses []NotificationMessageStatus `db:"statuses" json:"statuses"`
StatusReasons []string `db:"status_reasons" json:"status_reasons"`
}
func (q *sqlQuerier) BulkMarkNotificationMessagesFailed(ctx context.Context, arg BulkMarkNotificationMessagesFailedParams) (int64, error) {
result, err := q.db.ExecContext(ctx, bulkMarkNotificationMessagesFailed,
arg.MaxAttempts,
arg.RetryInterval,
pq.Array(arg.IDs),
pq.Array(arg.FailedAts),
pq.Array(arg.Statuses),
pq.Array(arg.StatusReasons),
)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const bulkMarkNotificationMessagesSent = `-- name: BulkMarkNotificationMessagesSent :execrows
UPDATE notification_messages
SET queued_seconds = 0,
updated_at = new_values.sent_at,
attempt_count = attempt_count + 1,
status = 'sent'::notification_message_status,
status_reason = NULL,
leased_until = NULL,
next_retry_after = NULL
FROM (SELECT UNNEST($1::uuid[]) AS id,
UNNEST($2::timestamptz[]) AS sent_at)
AS new_values
WHERE notification_messages.id = new_values.id
`
type BulkMarkNotificationMessagesSentParams struct {
IDs []uuid.UUID `db:"ids" json:"ids"`
SentAts []time.Time `db:"sent_ats" json:"sent_ats"`
}
func (q *sqlQuerier) BulkMarkNotificationMessagesSent(ctx context.Context, arg BulkMarkNotificationMessagesSentParams) (int64, error) {
result, err := q.db.ExecContext(ctx, bulkMarkNotificationMessagesSent, pq.Array(arg.IDs), pq.Array(arg.SentAts))
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const deleteAllWebpushSubscriptions = `-- name: DeleteAllWebpushSubscriptions :exec
TRUNCATE TABLE webpush_subscriptions
`
// Deletes all existing webpush subscriptions.
// This should be called when the VAPID keypair is regenerated, as the old
// keypair will no longer be valid and all existing subscriptions will need to
// be recreated.
func (q *sqlQuerier) DeleteAllWebpushSubscriptions(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, deleteAllWebpushSubscriptions)
return err
}
const deleteOldNotificationMessages = `-- name: DeleteOldNotificationMessages :exec
DELETE
FROM notification_messages
WHERE id IN
(SELECT id
FROM notification_messages AS nested
WHERE nested.updated_at < NOW() - INTERVAL '7 days')
`
// Delete all notification messages which have not been updated for over a week.
func (q *sqlQuerier) DeleteOldNotificationMessages(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, deleteOldNotificationMessages)
return err
}
const deleteWebpushSubscriptionByUserIDAndEndpoint = `-- name: DeleteWebpushSubscriptionByUserIDAndEndpoint :exec
DELETE FROM webpush_subscriptions
WHERE user_id = $1 AND endpoint = $2
`
type DeleteWebpushSubscriptionByUserIDAndEndpointParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
Endpoint string `db:"endpoint" json:"endpoint"`
}
func (q *sqlQuerier) DeleteWebpushSubscriptionByUserIDAndEndpoint(ctx context.Context, arg DeleteWebpushSubscriptionByUserIDAndEndpointParams) error {
_, err := q.db.ExecContext(ctx, deleteWebpushSubscriptionByUserIDAndEndpoint, arg.UserID, arg.Endpoint)
return err
}
const deleteWebpushSubscriptions = `-- name: DeleteWebpushSubscriptions :exec
DELETE FROM webpush_subscriptions
WHERE id = ANY($1::uuid[])
`
func (q *sqlQuerier) DeleteWebpushSubscriptions(ctx context.Context, ids []uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteWebpushSubscriptions, pq.Array(ids))
return err
}
const enqueueNotificationMessage = `-- name: EnqueueNotificationMessage :exec
INSERT INTO notification_messages (id, notification_template_id, user_id, method, payload, targets, created_by, created_at)
VALUES ($1,
$2,
$3,
$4::notification_method,
$5::jsonb,
$6,
$7,
$8)
`
type EnqueueNotificationMessageParams struct {
ID uuid.UUID `db:"id" json:"id"`
NotificationTemplateID uuid.UUID `db:"notification_template_id" json:"notification_template_id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
Method NotificationMethod `db:"method" json:"method"`
Payload json.RawMessage `db:"payload" json:"payload"`
Targets []uuid.UUID `db:"targets" json:"targets"`
CreatedBy string `db:"created_by" json:"created_by"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
func (q *sqlQuerier) EnqueueNotificationMessage(ctx context.Context, arg EnqueueNotificationMessageParams) error {
_, err := q.db.ExecContext(ctx, enqueueNotificationMessage,
arg.ID,
arg.NotificationTemplateID,
arg.UserID,
arg.Method,
arg.Payload,
pq.Array(arg.Targets),
arg.CreatedBy,
arg.CreatedAt,
)
return err
}
const fetchNewMessageMetadata = `-- name: FetchNewMessageMetadata :one
SELECT nt.name AS notification_name,
nt.id AS notification_template_id,
nt.actions AS actions,
nt.method AS custom_method,
u.id AS user_id,
u.email AS user_email,
COALESCE(NULLIF(u.name, ''), NULLIF(u.username, ''))::text AS user_name,
u.username AS user_username
FROM notification_templates nt,
users u
WHERE nt.id = $1
AND u.id = $2
`
type FetchNewMessageMetadataParams struct {
NotificationTemplateID uuid.UUID `db:"notification_template_id" json:"notification_template_id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
}
type FetchNewMessageMetadataRow struct {
NotificationName string `db:"notification_name" json:"notification_name"`
NotificationTemplateID uuid.UUID `db:"notification_template_id" json:"notification_template_id"`
Actions []byte `db:"actions" json:"actions"`
CustomMethod NullNotificationMethod `db:"custom_method" json:"custom_method"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
UserEmail string `db:"user_email" json:"user_email"`
UserName string `db:"user_name" json:"user_name"`
UserUsername string `db:"user_username" json:"user_username"`
}
// This is used to build up the notification_message's JSON payload.
func (q *sqlQuerier) FetchNewMessageMetadata(ctx context.Context, arg FetchNewMessageMetadataParams) (FetchNewMessageMetadataRow, error) {
row := q.db.QueryRowContext(ctx, fetchNewMessageMetadata, arg.NotificationTemplateID, arg.UserID)
var i FetchNewMessageMetadataRow
err := row.Scan(
&i.NotificationName,
&i.NotificationTemplateID,
&i.Actions,
&i.CustomMethod,
&i.UserID,
&i.UserEmail,
&i.UserName,
&i.UserUsername,
)
return i, err
}
const getNotificationMessagesByStatus = `-- name: GetNotificationMessagesByStatus :many
SELECT id, notification_template_id, user_id, method, status, status_reason, created_by, payload, attempt_count, targets, created_at, updated_at, leased_until, next_retry_after, queued_seconds, dedupe_hash
FROM notification_messages
WHERE status = $1
LIMIT $2::int
`
type GetNotificationMessagesByStatusParams struct {
Status NotificationMessageStatus `db:"status" json:"status"`
Limit int32 `db:"limit" json:"limit"`
}
func (q *sqlQuerier) GetNotificationMessagesByStatus(ctx context.Context, arg GetNotificationMessagesByStatusParams) ([]NotificationMessage, error) {
rows, err := q.db.QueryContext(ctx, getNotificationMessagesByStatus, arg.Status, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []NotificationMessage
for rows.Next() {
var i NotificationMessage
if err := rows.Scan(
&i.ID,
&i.NotificationTemplateID,
&i.UserID,
&i.Method,
&i.Status,
&i.StatusReason,
&i.CreatedBy,
&i.Payload,
&i.AttemptCount,
pq.Array(&i.Targets),
&i.CreatedAt,
&i.UpdatedAt,
&i.LeasedUntil,
&i.NextRetryAfter,
&i.QueuedSeconds,
&i.DedupeHash,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getNotificationReportGeneratorLogByTemplate = `-- name: GetNotificationReportGeneratorLogByTemplate :one
SELECT
notification_template_id, last_generated_at
FROM
notification_report_generator_logs
WHERE
notification_template_id = $1::uuid
`
// Fetch the notification report generator log indicating recent activity.
func (q *sqlQuerier) GetNotificationReportGeneratorLogByTemplate(ctx context.Context, templateID uuid.UUID) (NotificationReportGeneratorLog, error) {
row := q.db.QueryRowContext(ctx, getNotificationReportGeneratorLogByTemplate, templateID)
var i NotificationReportGeneratorLog
err := row.Scan(&i.NotificationTemplateID, &i.LastGeneratedAt)
return i, err
}
const getNotificationTemplateByID = `-- name: GetNotificationTemplateByID :one
SELECT id, name, title_template, body_template, actions, "group", method, kind, enabled_by_default
FROM notification_templates
WHERE id = $1::uuid
`
func (q *sqlQuerier) GetNotificationTemplateByID(ctx context.Context, id uuid.UUID) (NotificationTemplate, error) {
row := q.db.QueryRowContext(ctx, getNotificationTemplateByID, id)
var i NotificationTemplate
err := row.Scan(
&i.ID,
&i.Name,
&i.TitleTemplate,
&i.BodyTemplate,
&i.Actions,
&i.Group,
&i.Method,
&i.Kind,
&i.EnabledByDefault,
)
return i, err
}
const getNotificationTemplatesByKind = `-- name: GetNotificationTemplatesByKind :many
SELECT id, name, title_template, body_template, actions, "group", method, kind, enabled_by_default
FROM notification_templates
WHERE kind = $1::notification_template_kind
ORDER BY name ASC
`
func (q *sqlQuerier) GetNotificationTemplatesByKind(ctx context.Context, kind NotificationTemplateKind) ([]NotificationTemplate, error) {
rows, err := q.db.QueryContext(ctx, getNotificationTemplatesByKind, kind)
if err != nil {
return nil, err
}
defer rows.Close()
var items []NotificationTemplate
for rows.Next() {
var i NotificationTemplate
if err := rows.Scan(
&i.ID,
&i.Name,
&i.TitleTemplate,
&i.BodyTemplate,
&i.Actions,
&i.Group,
&i.Method,
&i.Kind,
&i.EnabledByDefault,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getUserNotificationPreferences = `-- name: GetUserNotificationPreferences :many
SELECT user_id, notification_template_id, disabled, created_at, updated_at
FROM notification_preferences
WHERE user_id = $1::uuid
`
func (q *sqlQuerier) GetUserNotificationPreferences(ctx context.Context, userID uuid.UUID) ([]NotificationPreference, error) {
rows, err := q.db.QueryContext(ctx, getUserNotificationPreferences, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []NotificationPreference
for rows.Next() {
var i NotificationPreference
if err := rows.Scan(
&i.UserID,
&i.NotificationTemplateID,
&i.Disabled,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWebpushSubscriptionsByUserID = `-- name: GetWebpushSubscriptionsByUserID :many
SELECT id, user_id, created_at, endpoint, endpoint_p256dh_key, endpoint_auth_key
FROM webpush_subscriptions
WHERE user_id = $1::uuid
`
func (q *sqlQuerier) GetWebpushSubscriptionsByUserID(ctx context.Context, userID uuid.UUID) ([]WebpushSubscription, error) {
rows, err := q.db.QueryContext(ctx, getWebpushSubscriptionsByUserID, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WebpushSubscription
for rows.Next() {
var i WebpushSubscription
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.CreatedAt,
&i.Endpoint,
&i.EndpointP256dhKey,
&i.EndpointAuthKey,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertWebpushSubscription = `-- name: InsertWebpushSubscription :one
INSERT INTO webpush_subscriptions (user_id, created_at, endpoint, endpoint_p256dh_key, endpoint_auth_key)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (user_id, endpoint) DO UPDATE
SET endpoint_p256dh_key = EXCLUDED.endpoint_p256dh_key,
endpoint_auth_key = EXCLUDED.endpoint_auth_key,
created_at = EXCLUDED.created_at
RETURNING id, user_id, created_at, endpoint, endpoint_p256dh_key, endpoint_auth_key
`
type InsertWebpushSubscriptionParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
Endpoint string `db:"endpoint" json:"endpoint"`
EndpointP256dhKey string `db:"endpoint_p256dh_key" json:"endpoint_p256dh_key"`
EndpointAuthKey string `db:"endpoint_auth_key" json:"endpoint_auth_key"`
}
// Inserts or updates a webpush subscription. The (user_id, endpoint) pair
// is unique; re-subscribing the same endpoint replaces the keys instead of
// inserting a duplicate row. This is the recovery path after a PWA reinstall
// on iOS, where the browser may keep the same endpoint with rotated keys.
func (q *sqlQuerier) InsertWebpushSubscription(ctx context.Context, arg InsertWebpushSubscriptionParams) (WebpushSubscription, error) {
row := q.db.QueryRowContext(ctx, insertWebpushSubscription,
arg.UserID,
arg.CreatedAt,
arg.Endpoint,
arg.EndpointP256dhKey,
arg.EndpointAuthKey,
)
var i WebpushSubscription
err := row.Scan(
&i.ID,
&i.UserID,
&i.CreatedAt,
&i.Endpoint,
&i.EndpointP256dhKey,
&i.EndpointAuthKey,
)
return i, err
}
const updateNotificationTemplateMethodByID = `-- name: UpdateNotificationTemplateMethodByID :one
UPDATE notification_templates
SET method = $1::notification_method
WHERE id = $2::uuid
RETURNING id, name, title_template, body_template, actions, "group", method, kind, enabled_by_default
`
type UpdateNotificationTemplateMethodByIDParams struct {
Method NullNotificationMethod `db:"method" json:"method"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateNotificationTemplateMethodByID(ctx context.Context, arg UpdateNotificationTemplateMethodByIDParams) (NotificationTemplate, error) {
row := q.db.QueryRowContext(ctx, updateNotificationTemplateMethodByID, arg.Method, arg.ID)
var i NotificationTemplate
err := row.Scan(
&i.ID,
&i.Name,
&i.TitleTemplate,
&i.BodyTemplate,
&i.Actions,
&i.Group,
&i.Method,
&i.Kind,
&i.EnabledByDefault,
)
return i, err
}
const updateUserNotificationPreferences = `-- name: UpdateUserNotificationPreferences :execrows
INSERT
INTO notification_preferences (user_id, notification_template_id, disabled)
SELECT $1::uuid, new_values.notification_template_id, new_values.disabled
FROM (SELECT UNNEST($2::uuid[]) AS notification_template_id,
UNNEST($3::bool[]) AS disabled) AS new_values
ON CONFLICT (user_id, notification_template_id) DO UPDATE
SET disabled = EXCLUDED.disabled,
updated_at = CURRENT_TIMESTAMP
`
type UpdateUserNotificationPreferencesParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
NotificationTemplateIds []uuid.UUID `db:"notification_template_ids" json:"notification_template_ids"`
Disableds []bool `db:"disableds" json:"disableds"`
}
func (q *sqlQuerier) UpdateUserNotificationPreferences(ctx context.Context, arg UpdateUserNotificationPreferencesParams) (int64, error) {
result, err := q.db.ExecContext(ctx, updateUserNotificationPreferences, arg.UserID, pq.Array(arg.NotificationTemplateIds), pq.Array(arg.Disableds))
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const upsertNotificationReportGeneratorLog = `-- name: UpsertNotificationReportGeneratorLog :exec
INSERT INTO notification_report_generator_logs (notification_template_id, last_generated_at) VALUES ($1, $2)
ON CONFLICT (notification_template_id) DO UPDATE set last_generated_at = EXCLUDED.last_generated_at
WHERE notification_report_generator_logs.notification_template_id = EXCLUDED.notification_template_id
`
type UpsertNotificationReportGeneratorLogParams struct {
NotificationTemplateID uuid.UUID `db:"notification_template_id" json:"notification_template_id"`
LastGeneratedAt time.Time `db:"last_generated_at" json:"last_generated_at"`
}
// Insert or update notification report generator logs with recent activity.
func (q *sqlQuerier) UpsertNotificationReportGeneratorLog(ctx context.Context, arg UpsertNotificationReportGeneratorLogParams) error {
_, err := q.db.ExecContext(ctx, upsertNotificationReportGeneratorLog, arg.NotificationTemplateID, arg.LastGeneratedAt)
return err
}
const countUnreadInboxNotificationsByUserID = `-- name: CountUnreadInboxNotificationsByUserID :one
SELECT COUNT(*) FROM inbox_notifications WHERE user_id = $1 AND read_at IS NULL
`
func (q *sqlQuerier) CountUnreadInboxNotificationsByUserID(ctx context.Context, userID uuid.UUID) (int64, error) {
row := q.db.QueryRowContext(ctx, countUnreadInboxNotificationsByUserID, userID)
var count int64
err := row.Scan(&count)
return count, err
}
const getFilteredInboxNotificationsByUserID = `-- name: GetFilteredInboxNotificationsByUserID :many
SELECT id, user_id, template_id, targets, title, content, icon, actions, read_at, created_at FROM inbox_notifications WHERE
user_id = $1 AND
($2::UUID[] IS NULL OR template_id = ANY($2::UUID[])) AND
($3::UUID[] IS NULL OR targets @> $3::UUID[]) AND
($4::inbox_notification_read_status = 'all' OR ($4::inbox_notification_read_status = 'unread' AND read_at IS NULL) OR ($4::inbox_notification_read_status = 'read' AND read_at IS NOT NULL)) AND
($5::TIMESTAMPTZ = '0001-01-01 00:00:00Z' OR created_at < $5::TIMESTAMPTZ)
ORDER BY created_at DESC
LIMIT (COALESCE(NULLIF($6 :: INT, 0), 25))
`
type GetFilteredInboxNotificationsByUserIDParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
Templates []uuid.UUID `db:"templates" json:"templates"`
Targets []uuid.UUID `db:"targets" json:"targets"`
ReadStatus InboxNotificationReadStatus `db:"read_status" json:"read_status"`
CreatedAtOpt time.Time `db:"created_at_opt" json:"created_at_opt"`
LimitOpt int32 `db:"limit_opt" json:"limit_opt"`
}
// Fetches inbox notifications for a user filtered by templates and targets
// param user_id: The user ID
// param templates: The template IDs to filter by - the template_id = ANY(@templates::UUID[]) condition checks if the template_id is in the @templates array
// param targets: The target IDs to filter by - the targets @> COALESCE(@targets, ARRAY[]::UUID[]) condition checks if the targets array (from the DB) contains all the elements in the @targets array
// param read_status: The read status to filter by - can be any of 'ALL', 'UNREAD', 'READ'
// param created_at_opt: The created_at timestamp to filter by. This parameter is usd for pagination - it fetches notifications created before the specified timestamp if it is not the zero value
// param limit_opt: The limit of notifications to fetch. If the limit is not specified, it defaults to 25
func (q *sqlQuerier) GetFilteredInboxNotificationsByUserID(ctx context.Context, arg GetFilteredInboxNotificationsByUserIDParams) ([]InboxNotification, error) {
rows, err := q.db.QueryContext(ctx, getFilteredInboxNotificationsByUserID,
arg.UserID,
pq.Array(arg.Templates),
pq.Array(arg.Targets),
arg.ReadStatus,
arg.CreatedAtOpt,
arg.LimitOpt,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []InboxNotification
for rows.Next() {
var i InboxNotification
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.TemplateID,
pq.Array(&i.Targets),
&i.Title,
&i.Content,
&i.Icon,
&i.Actions,
&i.ReadAt,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getInboxNotificationByID = `-- name: GetInboxNotificationByID :one
SELECT id, user_id, template_id, targets, title, content, icon, actions, read_at, created_at FROM inbox_notifications WHERE id = $1
`
func (q *sqlQuerier) GetInboxNotificationByID(ctx context.Context, id uuid.UUID) (InboxNotification, error) {
row := q.db.QueryRowContext(ctx, getInboxNotificationByID, id)
var i InboxNotification
err := row.Scan(
&i.ID,
&i.UserID,
&i.TemplateID,
pq.Array(&i.Targets),
&i.Title,
&i.Content,
&i.Icon,
&i.Actions,
&i.ReadAt,
&i.CreatedAt,
)
return i, err
}
const getInboxNotificationsByUserID = `-- name: GetInboxNotificationsByUserID :many
SELECT id, user_id, template_id, targets, title, content, icon, actions, read_at, created_at FROM inbox_notifications WHERE
user_id = $1 AND
($2::inbox_notification_read_status = 'all' OR ($2::inbox_notification_read_status = 'unread' AND read_at IS NULL) OR ($2::inbox_notification_read_status = 'read' AND read_at IS NOT NULL)) AND
($3::TIMESTAMPTZ = '0001-01-01 00:00:00Z' OR created_at < $3::TIMESTAMPTZ)
ORDER BY created_at DESC
LIMIT (COALESCE(NULLIF($4 :: INT, 0), 25))
`
type GetInboxNotificationsByUserIDParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
ReadStatus InboxNotificationReadStatus `db:"read_status" json:"read_status"`
CreatedAtOpt time.Time `db:"created_at_opt" json:"created_at_opt"`
LimitOpt int32 `db:"limit_opt" json:"limit_opt"`
}
// Fetches inbox notifications for a user filtered by templates and targets
// param user_id: The user ID
// param read_status: The read status to filter by - can be any of 'ALL', 'UNREAD', 'READ'
// param created_at_opt: The created_at timestamp to filter by. This parameter is usd for pagination - it fetches notifications created before the specified timestamp if it is not the zero value
// param limit_opt: The limit of notifications to fetch. If the limit is not specified, it defaults to 25
func (q *sqlQuerier) GetInboxNotificationsByUserID(ctx context.Context, arg GetInboxNotificationsByUserIDParams) ([]InboxNotification, error) {
rows, err := q.db.QueryContext(ctx, getInboxNotificationsByUserID,
arg.UserID,
arg.ReadStatus,
arg.CreatedAtOpt,
arg.LimitOpt,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []InboxNotification
for rows.Next() {
var i InboxNotification
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.TemplateID,
pq.Array(&i.Targets),
&i.Title,
&i.Content,
&i.Icon,
&i.Actions,
&i.ReadAt,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertInboxNotification = `-- name: InsertInboxNotification :one
INSERT INTO
inbox_notifications (
id,
user_id,
template_id,
targets,
title,
content,
icon,
actions,
created_at
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, user_id, template_id, targets, title, content, icon, actions, read_at, created_at
`
type InsertInboxNotificationParams struct {
ID uuid.UUID `db:"id" json:"id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
Targets []uuid.UUID `db:"targets" json:"targets"`
Title string `db:"title" json:"title"`
Content string `db:"content" json:"content"`
Icon string `db:"icon" json:"icon"`
Actions json.RawMessage `db:"actions" json:"actions"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
func (q *sqlQuerier) InsertInboxNotification(ctx context.Context, arg InsertInboxNotificationParams) (InboxNotification, error) {
row := q.db.QueryRowContext(ctx, insertInboxNotification,
arg.ID,
arg.UserID,
arg.TemplateID,
pq.Array(arg.Targets),
arg.Title,
arg.Content,
arg.Icon,
arg.Actions,
arg.CreatedAt,
)
var i InboxNotification
err := row.Scan(
&i.ID,
&i.UserID,
&i.TemplateID,
pq.Array(&i.Targets),
&i.Title,
&i.Content,
&i.Icon,
&i.Actions,
&i.ReadAt,
&i.CreatedAt,
)
return i, err
}
const markAllInboxNotificationsAsRead = `-- name: MarkAllInboxNotificationsAsRead :exec
UPDATE
inbox_notifications
SET
read_at = $1
WHERE
user_id = $2 and read_at IS NULL
`
type MarkAllInboxNotificationsAsReadParams struct {
ReadAt sql.NullTime `db:"read_at" json:"read_at"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
}
func (q *sqlQuerier) MarkAllInboxNotificationsAsRead(ctx context.Context, arg MarkAllInboxNotificationsAsReadParams) error {
_, err := q.db.ExecContext(ctx, markAllInboxNotificationsAsRead, arg.ReadAt, arg.UserID)
return err
}
const updateInboxNotificationReadStatus = `-- name: UpdateInboxNotificationReadStatus :exec
UPDATE
inbox_notifications
SET
read_at = $1
WHERE
id = $2
`
type UpdateInboxNotificationReadStatusParams struct {
ReadAt sql.NullTime `db:"read_at" json:"read_at"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateInboxNotificationReadStatus(ctx context.Context, arg UpdateInboxNotificationReadStatusParams) error {
_, err := q.db.ExecContext(ctx, updateInboxNotificationReadStatus, arg.ReadAt, arg.ID)
return err
}
const deleteOAuth2ProviderAppByClientID = `-- name: DeleteOAuth2ProviderAppByClientID :exec
DELETE FROM oauth2_provider_apps WHERE id = $1
`
func (q *sqlQuerier) DeleteOAuth2ProviderAppByClientID(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteOAuth2ProviderAppByClientID, id)
return err
}
const deleteOAuth2ProviderAppByID = `-- name: DeleteOAuth2ProviderAppByID :exec
DELETE FROM oauth2_provider_apps WHERE id = $1
`
func (q *sqlQuerier) DeleteOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteOAuth2ProviderAppByID, id)
return err
}
const deleteOAuth2ProviderAppCodeByID = `-- name: DeleteOAuth2ProviderAppCodeByID :exec
DELETE FROM oauth2_provider_app_codes WHERE id = $1
`
func (q *sqlQuerier) DeleteOAuth2ProviderAppCodeByID(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteOAuth2ProviderAppCodeByID, id)
return err
}
const deleteOAuth2ProviderAppCodesByAppAndUserID = `-- name: DeleteOAuth2ProviderAppCodesByAppAndUserID :exec
DELETE FROM oauth2_provider_app_codes WHERE app_id = $1 AND user_id = $2
`
type DeleteOAuth2ProviderAppCodesByAppAndUserIDParams struct {
AppID uuid.UUID `db:"app_id" json:"app_id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
}
func (q *sqlQuerier) DeleteOAuth2ProviderAppCodesByAppAndUserID(ctx context.Context, arg DeleteOAuth2ProviderAppCodesByAppAndUserIDParams) error {
_, err := q.db.ExecContext(ctx, deleteOAuth2ProviderAppCodesByAppAndUserID, arg.AppID, arg.UserID)
return err
}
const deleteOAuth2ProviderAppSecretByID = `-- name: DeleteOAuth2ProviderAppSecretByID :exec
DELETE FROM oauth2_provider_app_secrets WHERE id = $1
`
func (q *sqlQuerier) DeleteOAuth2ProviderAppSecretByID(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteOAuth2ProviderAppSecretByID, id)
return err
}
const deleteOAuth2ProviderAppTokensByAppAndUserID = `-- name: DeleteOAuth2ProviderAppTokensByAppAndUserID :exec
DELETE FROM
oauth2_provider_app_tokens
WHERE
oauth2_provider_app_tokens.app_id = $1
AND oauth2_provider_app_tokens.user_id = $2
`
type DeleteOAuth2ProviderAppTokensByAppAndUserIDParams struct {
AppID uuid.UUID `db:"app_id" json:"app_id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
}
// Filters directly on app_id rather than joining through app_secret_id,
// since app_secret_id is NULL for public (secretless) clients and would
// silently exclude their tokens from this delete.
func (q *sqlQuerier) DeleteOAuth2ProviderAppTokensByAppAndUserID(ctx context.Context, arg DeleteOAuth2ProviderAppTokensByAppAndUserIDParams) error {
_, err := q.db.ExecContext(ctx, deleteOAuth2ProviderAppTokensByAppAndUserID, arg.AppID, arg.UserID)
return err
}
const getOAuth2ProviderAppByClientID = `-- name: GetOAuth2ProviderAppByClientID :one
SELECT id, created_at, updated_at, name, icon, callback_url, redirect_uris, client_type, dynamically_registered, client_id_issued_at, client_secret_expires_at, grant_types, response_types, token_endpoint_auth_method, scope, contacts, client_uri, logo_uri, tos_uri, policy_uri, jwks_uri, jwks, software_id, software_version, registration_access_token, registration_client_uri FROM oauth2_provider_apps WHERE id = $1
`
// RFC 7591/7592 Dynamic Client Registration queries
func (q *sqlQuerier) GetOAuth2ProviderAppByClientID(ctx context.Context, id uuid.UUID) (OAuth2ProviderApp, error) {
row := q.db.QueryRowContext(ctx, getOAuth2ProviderAppByClientID, id)
var i OAuth2ProviderApp
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Name,
&i.Icon,
&i.CallbackURL,
pq.Array(&i.RedirectUris),
&i.ClientType,
&i.DynamicallyRegistered,
&i.ClientIDIssuedAt,
&i.ClientSecretExpiresAt,
pq.Array(&i.GrantTypes),
pq.Array(&i.ResponseTypes),
&i.TokenEndpointAuthMethod,
&i.Scope,
pq.Array(&i.Contacts),
&i.ClientUri,
&i.LogoUri,
&i.TosUri,
&i.PolicyUri,
&i.JwksUri,
&i.Jwks,
&i.SoftwareID,
&i.SoftwareVersion,
&i.RegistrationAccessToken,
&i.RegistrationClientUri,
)
return i, err
}
const getOAuth2ProviderAppByID = `-- name: GetOAuth2ProviderAppByID :one
SELECT id, created_at, updated_at, name, icon, callback_url, redirect_uris, client_type, dynamically_registered, client_id_issued_at, client_secret_expires_at, grant_types, response_types, token_endpoint_auth_method, scope, contacts, client_uri, logo_uri, tos_uri, policy_uri, jwks_uri, jwks, software_id, software_version, registration_access_token, registration_client_uri FROM oauth2_provider_apps WHERE id = $1
`
func (q *sqlQuerier) GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderApp, error) {
row := q.db.QueryRowContext(ctx, getOAuth2ProviderAppByID, id)
var i OAuth2ProviderApp
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Name,
&i.Icon,
&i.CallbackURL,
pq.Array(&i.RedirectUris),
&i.ClientType,
&i.DynamicallyRegistered,
&i.ClientIDIssuedAt,
&i.ClientSecretExpiresAt,
pq.Array(&i.GrantTypes),
pq.Array(&i.ResponseTypes),
&i.TokenEndpointAuthMethod,
&i.Scope,
pq.Array(&i.Contacts),
&i.ClientUri,
&i.LogoUri,
&i.TosUri,
&i.PolicyUri,
&i.JwksUri,
&i.Jwks,
&i.SoftwareID,
&i.SoftwareVersion,
&i.RegistrationAccessToken,
&i.RegistrationClientUri,
)
return i, err
}
const getOAuth2ProviderAppCodeByID = `-- name: GetOAuth2ProviderAppCodeByID :one
SELECT id, created_at, expires_at, secret_prefix, hashed_secret, user_id, app_id, resource_uri, code_challenge, code_challenge_method, state_hash, redirect_uri, scope FROM oauth2_provider_app_codes WHERE id = $1
`
func (q *sqlQuerier) GetOAuth2ProviderAppCodeByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderAppCode, error) {
row := q.db.QueryRowContext(ctx, getOAuth2ProviderAppCodeByID, id)
var i OAuth2ProviderAppCode
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.ExpiresAt,
&i.SecretPrefix,
&i.HashedSecret,
&i.UserID,
&i.AppID,
&i.ResourceUri,
&i.CodeChallenge,
&i.CodeChallengeMethod,
&i.StateHash,
&i.RedirectUri,
&i.Scope,
)
return i, err
}
const getOAuth2ProviderAppCodeByPrefix = `-- name: GetOAuth2ProviderAppCodeByPrefix :one
SELECT id, created_at, expires_at, secret_prefix, hashed_secret, user_id, app_id, resource_uri, code_challenge, code_challenge_method, state_hash, redirect_uri, scope FROM oauth2_provider_app_codes WHERE secret_prefix = $1
`
func (q *sqlQuerier) GetOAuth2ProviderAppCodeByPrefix(ctx context.Context, secretPrefix []byte) (OAuth2ProviderAppCode, error) {
row := q.db.QueryRowContext(ctx, getOAuth2ProviderAppCodeByPrefix, secretPrefix)
var i OAuth2ProviderAppCode
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.ExpiresAt,
&i.SecretPrefix,
&i.HashedSecret,
&i.UserID,
&i.AppID,
&i.ResourceUri,
&i.CodeChallenge,
&i.CodeChallengeMethod,
&i.StateHash,
&i.RedirectUri,
&i.Scope,
)
return i, err
}
const getOAuth2ProviderAppSecretByID = `-- name: GetOAuth2ProviderAppSecretByID :one
SELECT id, created_at, last_used_at, hashed_secret, display_secret, app_id, secret_prefix FROM oauth2_provider_app_secrets WHERE id = $1
`
func (q *sqlQuerier) GetOAuth2ProviderAppSecretByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderAppSecret, error) {
row := q.db.QueryRowContext(ctx, getOAuth2ProviderAppSecretByID, id)
var i OAuth2ProviderAppSecret
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.LastUsedAt,
&i.HashedSecret,
&i.DisplaySecret,
&i.AppID,
&i.SecretPrefix,
)
return i, err
}
const getOAuth2ProviderAppSecretByPrefix = `-- name: GetOAuth2ProviderAppSecretByPrefix :one
SELECT id, created_at, last_used_at, hashed_secret, display_secret, app_id, secret_prefix FROM oauth2_provider_app_secrets WHERE secret_prefix = $1
`
func (q *sqlQuerier) GetOAuth2ProviderAppSecretByPrefix(ctx context.Context, secretPrefix []byte) (OAuth2ProviderAppSecret, error) {
row := q.db.QueryRowContext(ctx, getOAuth2ProviderAppSecretByPrefix, secretPrefix)
var i OAuth2ProviderAppSecret
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.LastUsedAt,
&i.HashedSecret,
&i.DisplaySecret,
&i.AppID,
&i.SecretPrefix,
)
return i, err
}
const getOAuth2ProviderAppSecretsByAppID = `-- name: GetOAuth2ProviderAppSecretsByAppID :many
SELECT id, created_at, last_used_at, hashed_secret, display_secret, app_id, secret_prefix FROM oauth2_provider_app_secrets WHERE app_id = $1 ORDER BY (created_at, id) ASC
`
func (q *sqlQuerier) GetOAuth2ProviderAppSecretsByAppID(ctx context.Context, appID uuid.UUID) ([]OAuth2ProviderAppSecret, error) {
rows, err := q.db.QueryContext(ctx, getOAuth2ProviderAppSecretsByAppID, appID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []OAuth2ProviderAppSecret
for rows.Next() {
var i OAuth2ProviderAppSecret
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.LastUsedAt,
&i.HashedSecret,
&i.DisplaySecret,
&i.AppID,
&i.SecretPrefix,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getOAuth2ProviderAppTokenByAPIKeyID = `-- name: GetOAuth2ProviderAppTokenByAPIKeyID :one
SELECT id, created_at, expires_at, hash_prefix, refresh_hash, app_secret_id, api_key_id, audience, user_id, app_id, scope FROM oauth2_provider_app_tokens WHERE api_key_id = $1
`
func (q *sqlQuerier) GetOAuth2ProviderAppTokenByAPIKeyID(ctx context.Context, apiKeyID string) (OAuth2ProviderAppToken, error) {
row := q.db.QueryRowContext(ctx, getOAuth2ProviderAppTokenByAPIKeyID, apiKeyID)
var i OAuth2ProviderAppToken
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.ExpiresAt,
&i.HashPrefix,
&i.RefreshHash,
&i.AppSecretID,
&i.APIKeyID,
&i.Audience,
&i.UserID,
&i.AppID,
&i.Scope,
)
return i, err
}
const getOAuth2ProviderAppTokenByPrefix = `-- name: GetOAuth2ProviderAppTokenByPrefix :one
SELECT id, created_at, expires_at, hash_prefix, refresh_hash, app_secret_id, api_key_id, audience, user_id, app_id, scope FROM oauth2_provider_app_tokens WHERE hash_prefix = $1
`
func (q *sqlQuerier) GetOAuth2ProviderAppTokenByPrefix(ctx context.Context, hashPrefix []byte) (OAuth2ProviderAppToken, error) {
row := q.db.QueryRowContext(ctx, getOAuth2ProviderAppTokenByPrefix, hashPrefix)
var i OAuth2ProviderAppToken
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.ExpiresAt,
&i.HashPrefix,
&i.RefreshHash,
&i.AppSecretID,
&i.APIKeyID,
&i.Audience,
&i.UserID,
&i.AppID,
&i.Scope,
)
return i, err
}
const getOAuth2ProviderApps = `-- name: GetOAuth2ProviderApps :many
SELECT id, created_at, updated_at, name, icon, callback_url, redirect_uris, client_type, dynamically_registered, client_id_issued_at, client_secret_expires_at, grant_types, response_types, token_endpoint_auth_method, scope, contacts, client_uri, logo_uri, tos_uri, policy_uri, jwks_uri, jwks, software_id, software_version, registration_access_token, registration_client_uri FROM oauth2_provider_apps ORDER BY (name, id) ASC
`
func (q *sqlQuerier) GetOAuth2ProviderApps(ctx context.Context) ([]OAuth2ProviderApp, error) {
rows, err := q.db.QueryContext(ctx, getOAuth2ProviderApps)
if err != nil {
return nil, err
}
defer rows.Close()
var items []OAuth2ProviderApp
for rows.Next() {
var i OAuth2ProviderApp
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Name,
&i.Icon,
&i.CallbackURL,
pq.Array(&i.RedirectUris),
&i.ClientType,
&i.DynamicallyRegistered,
&i.ClientIDIssuedAt,
&i.ClientSecretExpiresAt,
pq.Array(&i.GrantTypes),
pq.Array(&i.ResponseTypes),
&i.TokenEndpointAuthMethod,
&i.Scope,
pq.Array(&i.Contacts),
&i.ClientUri,
&i.LogoUri,
&i.TosUri,
&i.PolicyUri,
&i.JwksUri,
&i.Jwks,
&i.SoftwareID,
&i.SoftwareVersion,
&i.RegistrationAccessToken,
&i.RegistrationClientUri,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getOAuth2ProviderAppsByUserID = `-- name: GetOAuth2ProviderAppsByUserID :many
SELECT
COUNT(DISTINCT oauth2_provider_app_tokens.id) as token_count,
oauth2_provider_apps.id, oauth2_provider_apps.created_at, oauth2_provider_apps.updated_at, oauth2_provider_apps.name, oauth2_provider_apps.icon, oauth2_provider_apps.callback_url, oauth2_provider_apps.redirect_uris, oauth2_provider_apps.client_type, oauth2_provider_apps.dynamically_registered, oauth2_provider_apps.client_id_issued_at, oauth2_provider_apps.client_secret_expires_at, oauth2_provider_apps.grant_types, oauth2_provider_apps.response_types, oauth2_provider_apps.token_endpoint_auth_method, oauth2_provider_apps.scope, oauth2_provider_apps.contacts, oauth2_provider_apps.client_uri, oauth2_provider_apps.logo_uri, oauth2_provider_apps.tos_uri, oauth2_provider_apps.policy_uri, oauth2_provider_apps.jwks_uri, oauth2_provider_apps.jwks, oauth2_provider_apps.software_id, oauth2_provider_apps.software_version, oauth2_provider_apps.registration_access_token, oauth2_provider_apps.registration_client_uri
FROM oauth2_provider_app_tokens
INNER JOIN oauth2_provider_apps
ON oauth2_provider_apps.id = oauth2_provider_app_tokens.app_id
WHERE
oauth2_provider_app_tokens.user_id = $1
GROUP BY
oauth2_provider_apps.id
`
type GetOAuth2ProviderAppsByUserIDRow struct {
TokenCount int64 `db:"token_count" json:"token_count"`
OAuth2ProviderApp OAuth2ProviderApp `db:"oauth2_provider_app" json:"oauth2_provider_app"`
}
// Joins directly on oauth2_provider_app_tokens.app_id rather than through
// app_secret_id, since app_secret_id is NULL for public (secretless) clients
// and would silently exclude their tokens from this listing.
func (q *sqlQuerier) GetOAuth2ProviderAppsByUserID(ctx context.Context, userID uuid.UUID) ([]GetOAuth2ProviderAppsByUserIDRow, error) {
rows, err := q.db.QueryContext(ctx, getOAuth2ProviderAppsByUserID, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetOAuth2ProviderAppsByUserIDRow
for rows.Next() {
var i GetOAuth2ProviderAppsByUserIDRow
if err := rows.Scan(
&i.TokenCount,
&i.OAuth2ProviderApp.ID,
&i.OAuth2ProviderApp.CreatedAt,
&i.OAuth2ProviderApp.UpdatedAt,
&i.OAuth2ProviderApp.Name,
&i.OAuth2ProviderApp.Icon,
&i.OAuth2ProviderApp.CallbackURL,
pq.Array(&i.OAuth2ProviderApp.RedirectUris),
&i.OAuth2ProviderApp.ClientType,
&i.OAuth2ProviderApp.DynamicallyRegistered,
&i.OAuth2ProviderApp.ClientIDIssuedAt,
&i.OAuth2ProviderApp.ClientSecretExpiresAt,
pq.Array(&i.OAuth2ProviderApp.GrantTypes),
pq.Array(&i.OAuth2ProviderApp.ResponseTypes),
&i.OAuth2ProviderApp.TokenEndpointAuthMethod,
&i.OAuth2ProviderApp.Scope,
pq.Array(&i.OAuth2ProviderApp.Contacts),
&i.OAuth2ProviderApp.ClientUri,
&i.OAuth2ProviderApp.LogoUri,
&i.OAuth2ProviderApp.TosUri,
&i.OAuth2ProviderApp.PolicyUri,
&i.OAuth2ProviderApp.JwksUri,
&i.OAuth2ProviderApp.Jwks,
&i.OAuth2ProviderApp.SoftwareID,
&i.OAuth2ProviderApp.SoftwareVersion,
&i.OAuth2ProviderApp.RegistrationAccessToken,
&i.OAuth2ProviderApp.RegistrationClientUri,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertOAuth2ProviderApp = `-- name: InsertOAuth2ProviderApp :one
INSERT INTO oauth2_provider_apps (
id,
created_at,
updated_at,
name,
icon,
callback_url,
redirect_uris,
client_type,
dynamically_registered,
client_id_issued_at,
client_secret_expires_at,
grant_types,
response_types,
token_endpoint_auth_method,
scope,
contacts,
client_uri,
logo_uri,
tos_uri,
policy_uri,
jwks_uri,
jwks,
software_id,
software_version,
registration_access_token,
registration_client_uri
) VALUES(
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
$9,
$10,
$11,
$12,
$13,
$14,
$15,
$16,
$17,
$18,
$19,
$20,
$21,
$22,
$23,
$24,
$25,
$26
) RETURNING id, created_at, updated_at, name, icon, callback_url, redirect_uris, client_type, dynamically_registered, client_id_issued_at, client_secret_expires_at, grant_types, response_types, token_endpoint_auth_method, scope, contacts, client_uri, logo_uri, tos_uri, policy_uri, jwks_uri, jwks, software_id, software_version, registration_access_token, registration_client_uri
`
type InsertOAuth2ProviderAppParams struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Name string `db:"name" json:"name"`
Icon string `db:"icon" json:"icon"`
CallbackURL string `db:"callback_url" json:"callback_url"`
RedirectUris []string `db:"redirect_uris" json:"redirect_uris"`
ClientType string `db:"client_type" json:"client_type"`
DynamicallyRegistered sql.NullBool `db:"dynamically_registered" json:"dynamically_registered"`
ClientIDIssuedAt sql.NullTime `db:"client_id_issued_at" json:"client_id_issued_at"`
ClientSecretExpiresAt sql.NullTime `db:"client_secret_expires_at" json:"client_secret_expires_at"`
GrantTypes []string `db:"grant_types" json:"grant_types"`
ResponseTypes []string `db:"response_types" json:"response_types"`
TokenEndpointAuthMethod sql.NullString `db:"token_endpoint_auth_method" json:"token_endpoint_auth_method"`
Scope sql.NullString `db:"scope" json:"scope"`
Contacts []string `db:"contacts" json:"contacts"`
ClientUri sql.NullString `db:"client_uri" json:"client_uri"`
LogoUri sql.NullString `db:"logo_uri" json:"logo_uri"`
TosUri sql.NullString `db:"tos_uri" json:"tos_uri"`
PolicyUri sql.NullString `db:"policy_uri" json:"policy_uri"`
JwksUri sql.NullString `db:"jwks_uri" json:"jwks_uri"`
Jwks pqtype.NullRawMessage `db:"jwks" json:"jwks"`
SoftwareID sql.NullString `db:"software_id" json:"software_id"`
SoftwareVersion sql.NullString `db:"software_version" json:"software_version"`
RegistrationAccessToken []byte `db:"registration_access_token" json:"registration_access_token"`
RegistrationClientUri sql.NullString `db:"registration_client_uri" json:"registration_client_uri"`
}
func (q *sqlQuerier) InsertOAuth2ProviderApp(ctx context.Context, arg InsertOAuth2ProviderAppParams) (OAuth2ProviderApp, error) {
row := q.db.QueryRowContext(ctx, insertOAuth2ProviderApp,
arg.ID,
arg.CreatedAt,
arg.UpdatedAt,
arg.Name,
arg.Icon,
arg.CallbackURL,
pq.Array(arg.RedirectUris),
arg.ClientType,
arg.DynamicallyRegistered,
arg.ClientIDIssuedAt,
arg.ClientSecretExpiresAt,
pq.Array(arg.GrantTypes),
pq.Array(arg.ResponseTypes),
arg.TokenEndpointAuthMethod,
arg.Scope,
pq.Array(arg.Contacts),
arg.ClientUri,
arg.LogoUri,
arg.TosUri,
arg.PolicyUri,
arg.JwksUri,
arg.Jwks,
arg.SoftwareID,
arg.SoftwareVersion,
arg.RegistrationAccessToken,
arg.RegistrationClientUri,
)
var i OAuth2ProviderApp
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Name,
&i.Icon,
&i.CallbackURL,
pq.Array(&i.RedirectUris),
&i.ClientType,
&i.DynamicallyRegistered,
&i.ClientIDIssuedAt,
&i.ClientSecretExpiresAt,
pq.Array(&i.GrantTypes),
pq.Array(&i.ResponseTypes),
&i.TokenEndpointAuthMethod,
&i.Scope,
pq.Array(&i.Contacts),
&i.ClientUri,
&i.LogoUri,
&i.TosUri,
&i.PolicyUri,
&i.JwksUri,
&i.Jwks,
&i.SoftwareID,
&i.SoftwareVersion,
&i.RegistrationAccessToken,
&i.RegistrationClientUri,
)
return i, err
}
const insertOAuth2ProviderAppCode = `-- name: InsertOAuth2ProviderAppCode :one
INSERT INTO oauth2_provider_app_codes (
id,
created_at,
expires_at,
secret_prefix,
hashed_secret,
app_id,
user_id,
resource_uri,
code_challenge,
code_challenge_method,
state_hash,
redirect_uri,
scope
) VALUES(
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
$9,
$10,
$11,
$12,
$13
) RETURNING id, created_at, expires_at, secret_prefix, hashed_secret, user_id, app_id, resource_uri, code_challenge, code_challenge_method, state_hash, redirect_uri, scope
`
type InsertOAuth2ProviderAppCodeParams struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
ExpiresAt time.Time `db:"expires_at" json:"expires_at"`
SecretPrefix []byte `db:"secret_prefix" json:"secret_prefix"`
HashedSecret []byte `db:"hashed_secret" json:"hashed_secret"`
AppID uuid.UUID `db:"app_id" json:"app_id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
ResourceUri sql.NullString `db:"resource_uri" json:"resource_uri"`
CodeChallenge sql.NullString `db:"code_challenge" json:"code_challenge"`
CodeChallengeMethod sql.NullString `db:"code_challenge_method" json:"code_challenge_method"`
StateHash sql.NullString `db:"state_hash" json:"state_hash"`
RedirectUri sql.NullString `db:"redirect_uri" json:"redirect_uri"`
Scope string `db:"scope" json:"scope"`
}
func (q *sqlQuerier) InsertOAuth2ProviderAppCode(ctx context.Context, arg InsertOAuth2ProviderAppCodeParams) (OAuth2ProviderAppCode, error) {
row := q.db.QueryRowContext(ctx, insertOAuth2ProviderAppCode,
arg.ID,
arg.CreatedAt,
arg.ExpiresAt,
arg.SecretPrefix,
arg.HashedSecret,
arg.AppID,
arg.UserID,
arg.ResourceUri,
arg.CodeChallenge,
arg.CodeChallengeMethod,
arg.StateHash,
arg.RedirectUri,
arg.Scope,
)
var i OAuth2ProviderAppCode
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.ExpiresAt,
&i.SecretPrefix,
&i.HashedSecret,
&i.UserID,
&i.AppID,
&i.ResourceUri,
&i.CodeChallenge,
&i.CodeChallengeMethod,
&i.StateHash,
&i.RedirectUri,
&i.Scope,
)
return i, err
}
const insertOAuth2ProviderAppSecret = `-- name: InsertOAuth2ProviderAppSecret :one
INSERT INTO oauth2_provider_app_secrets (
id,
created_at,
secret_prefix,
hashed_secret,
display_secret,
app_id
) VALUES(
$1,
$2,
$3,
$4,
$5,
$6
) RETURNING id, created_at, last_used_at, hashed_secret, display_secret, app_id, secret_prefix
`
type InsertOAuth2ProviderAppSecretParams struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
SecretPrefix []byte `db:"secret_prefix" json:"secret_prefix"`
HashedSecret []byte `db:"hashed_secret" json:"hashed_secret"`
DisplaySecret string `db:"display_secret" json:"display_secret"`
AppID uuid.UUID `db:"app_id" json:"app_id"`
}
func (q *sqlQuerier) InsertOAuth2ProviderAppSecret(ctx context.Context, arg InsertOAuth2ProviderAppSecretParams) (OAuth2ProviderAppSecret, error) {
row := q.db.QueryRowContext(ctx, insertOAuth2ProviderAppSecret,
arg.ID,
arg.CreatedAt,
arg.SecretPrefix,
arg.HashedSecret,
arg.DisplaySecret,
arg.AppID,
)
var i OAuth2ProviderAppSecret
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.LastUsedAt,
&i.HashedSecret,
&i.DisplaySecret,
&i.AppID,
&i.SecretPrefix,
)
return i, err
}
const insertOAuth2ProviderAppToken = `-- name: InsertOAuth2ProviderAppToken :one
INSERT INTO oauth2_provider_app_tokens (
id,
created_at,
expires_at,
hash_prefix,
refresh_hash,
app_id,
app_secret_id,
api_key_id,
user_id,
audience,
scope
) VALUES(
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
$9,
$10,
$11
) RETURNING id, created_at, expires_at, hash_prefix, refresh_hash, app_secret_id, api_key_id, audience, user_id, app_id, scope
`
type InsertOAuth2ProviderAppTokenParams struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
ExpiresAt time.Time `db:"expires_at" json:"expires_at"`
HashPrefix []byte `db:"hash_prefix" json:"hash_prefix"`
RefreshHash []byte `db:"refresh_hash" json:"refresh_hash"`
AppID uuid.UUID `db:"app_id" json:"app_id"`
AppSecretID uuid.NullUUID `db:"app_secret_id" json:"app_secret_id"`
APIKeyID string `db:"api_key_id" json:"api_key_id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
Audience sql.NullString `db:"audience" json:"audience"`
Scope string `db:"scope" json:"scope"`
}
func (q *sqlQuerier) InsertOAuth2ProviderAppToken(ctx context.Context, arg InsertOAuth2ProviderAppTokenParams) (OAuth2ProviderAppToken, error) {
row := q.db.QueryRowContext(ctx, insertOAuth2ProviderAppToken,
arg.ID,
arg.CreatedAt,
arg.ExpiresAt,
arg.HashPrefix,
arg.RefreshHash,
arg.AppID,
arg.AppSecretID,
arg.APIKeyID,
arg.UserID,
arg.Audience,
arg.Scope,
)
var i OAuth2ProviderAppToken
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.ExpiresAt,
&i.HashPrefix,
&i.RefreshHash,
&i.AppSecretID,
&i.APIKeyID,
&i.Audience,
&i.UserID,
&i.AppID,
&i.Scope,
)
return i, err
}
const updateOAuth2ProviderAppByClientID = `-- name: UpdateOAuth2ProviderAppByClientID :one
UPDATE oauth2_provider_apps SET
updated_at = $2,
name = $3,
icon = $4,
callback_url = $5,
redirect_uris = $6,
client_type = $7,
client_secret_expires_at = $8,
grant_types = $9,
response_types = $10,
token_endpoint_auth_method = $11,
scope = $12,
contacts = $13,
client_uri = $14,
logo_uri = $15,
tos_uri = $16,
policy_uri = $17,
jwks_uri = $18,
jwks = $19,
software_id = $20,
software_version = $21
WHERE id = $1 RETURNING id, created_at, updated_at, name, icon, callback_url, redirect_uris, client_type, dynamically_registered, client_id_issued_at, client_secret_expires_at, grant_types, response_types, token_endpoint_auth_method, scope, contacts, client_uri, logo_uri, tos_uri, policy_uri, jwks_uri, jwks, software_id, software_version, registration_access_token, registration_client_uri
`
type UpdateOAuth2ProviderAppByClientIDParams struct {
ID uuid.UUID `db:"id" json:"id"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Name string `db:"name" json:"name"`
Icon string `db:"icon" json:"icon"`
CallbackURL string `db:"callback_url" json:"callback_url"`
RedirectUris []string `db:"redirect_uris" json:"redirect_uris"`
ClientType string `db:"client_type" json:"client_type"`
ClientSecretExpiresAt sql.NullTime `db:"client_secret_expires_at" json:"client_secret_expires_at"`
GrantTypes []string `db:"grant_types" json:"grant_types"`
ResponseTypes []string `db:"response_types" json:"response_types"`
TokenEndpointAuthMethod sql.NullString `db:"token_endpoint_auth_method" json:"token_endpoint_auth_method"`
Scope sql.NullString `db:"scope" json:"scope"`
Contacts []string `db:"contacts" json:"contacts"`
ClientUri sql.NullString `db:"client_uri" json:"client_uri"`
LogoUri sql.NullString `db:"logo_uri" json:"logo_uri"`
TosUri sql.NullString `db:"tos_uri" json:"tos_uri"`
PolicyUri sql.NullString `db:"policy_uri" json:"policy_uri"`
JwksUri sql.NullString `db:"jwks_uri" json:"jwks_uri"`
Jwks pqtype.NullRawMessage `db:"jwks" json:"jwks"`
SoftwareID sql.NullString `db:"software_id" json:"software_id"`
SoftwareVersion sql.NullString `db:"software_version" json:"software_version"`
}
func (q *sqlQuerier) UpdateOAuth2ProviderAppByClientID(ctx context.Context, arg UpdateOAuth2ProviderAppByClientIDParams) (OAuth2ProviderApp, error) {
row := q.db.QueryRowContext(ctx, updateOAuth2ProviderAppByClientID,
arg.ID,
arg.UpdatedAt,
arg.Name,
arg.Icon,
arg.CallbackURL,
pq.Array(arg.RedirectUris),
arg.ClientType,
arg.ClientSecretExpiresAt,
pq.Array(arg.GrantTypes),
pq.Array(arg.ResponseTypes),
arg.TokenEndpointAuthMethod,
arg.Scope,
pq.Array(arg.Contacts),
arg.ClientUri,
arg.LogoUri,
arg.TosUri,
arg.PolicyUri,
arg.JwksUri,
arg.Jwks,
arg.SoftwareID,
arg.SoftwareVersion,
)
var i OAuth2ProviderApp
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Name,
&i.Icon,
&i.CallbackURL,
pq.Array(&i.RedirectUris),
&i.ClientType,
&i.DynamicallyRegistered,
&i.ClientIDIssuedAt,
&i.ClientSecretExpiresAt,
pq.Array(&i.GrantTypes),
pq.Array(&i.ResponseTypes),
&i.TokenEndpointAuthMethod,
&i.Scope,
pq.Array(&i.Contacts),
&i.ClientUri,
&i.LogoUri,
&i.TosUri,
&i.PolicyUri,
&i.JwksUri,
&i.Jwks,
&i.SoftwareID,
&i.SoftwareVersion,
&i.RegistrationAccessToken,
&i.RegistrationClientUri,
)
return i, err
}
const updateOAuth2ProviderAppByID = `-- name: UpdateOAuth2ProviderAppByID :one
UPDATE oauth2_provider_apps SET
updated_at = $2,
name = $3,
icon = $4,
callback_url = $5,
redirect_uris = $6,
client_type = $7,
dynamically_registered = $8,
client_secret_expires_at = $9,
grant_types = $10,
response_types = $11,
token_endpoint_auth_method = $12,
scope = $13,
contacts = $14,
client_uri = $15,
logo_uri = $16,
tos_uri = $17,
policy_uri = $18,
jwks_uri = $19,
jwks = $20,
software_id = $21,
software_version = $22
WHERE id = $1 RETURNING id, created_at, updated_at, name, icon, callback_url, redirect_uris, client_type, dynamically_registered, client_id_issued_at, client_secret_expires_at, grant_types, response_types, token_endpoint_auth_method, scope, contacts, client_uri, logo_uri, tos_uri, policy_uri, jwks_uri, jwks, software_id, software_version, registration_access_token, registration_client_uri
`
type UpdateOAuth2ProviderAppByIDParams struct {
ID uuid.UUID `db:"id" json:"id"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Name string `db:"name" json:"name"`
Icon string `db:"icon" json:"icon"`
CallbackURL string `db:"callback_url" json:"callback_url"`
RedirectUris []string `db:"redirect_uris" json:"redirect_uris"`
ClientType string `db:"client_type" json:"client_type"`
DynamicallyRegistered sql.NullBool `db:"dynamically_registered" json:"dynamically_registered"`
ClientSecretExpiresAt sql.NullTime `db:"client_secret_expires_at" json:"client_secret_expires_at"`
GrantTypes []string `db:"grant_types" json:"grant_types"`
ResponseTypes []string `db:"response_types" json:"response_types"`
TokenEndpointAuthMethod sql.NullString `db:"token_endpoint_auth_method" json:"token_endpoint_auth_method"`
Scope sql.NullString `db:"scope" json:"scope"`
Contacts []string `db:"contacts" json:"contacts"`
ClientUri sql.NullString `db:"client_uri" json:"client_uri"`
LogoUri sql.NullString `db:"logo_uri" json:"logo_uri"`
TosUri sql.NullString `db:"tos_uri" json:"tos_uri"`
PolicyUri sql.NullString `db:"policy_uri" json:"policy_uri"`
JwksUri sql.NullString `db:"jwks_uri" json:"jwks_uri"`
Jwks pqtype.NullRawMessage `db:"jwks" json:"jwks"`
SoftwareID sql.NullString `db:"software_id" json:"software_id"`
SoftwareVersion sql.NullString `db:"software_version" json:"software_version"`
}
func (q *sqlQuerier) UpdateOAuth2ProviderAppByID(ctx context.Context, arg UpdateOAuth2ProviderAppByIDParams) (OAuth2ProviderApp, error) {
row := q.db.QueryRowContext(ctx, updateOAuth2ProviderAppByID,
arg.ID,
arg.UpdatedAt,
arg.Name,
arg.Icon,
arg.CallbackURL,
pq.Array(arg.RedirectUris),
arg.ClientType,
arg.DynamicallyRegistered,
arg.ClientSecretExpiresAt,
pq.Array(arg.GrantTypes),
pq.Array(arg.ResponseTypes),
arg.TokenEndpointAuthMethod,
arg.Scope,
pq.Array(arg.Contacts),
arg.ClientUri,
arg.LogoUri,
arg.TosUri,
arg.PolicyUri,
arg.JwksUri,
arg.Jwks,
arg.SoftwareID,
arg.SoftwareVersion,
)
var i OAuth2ProviderApp
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Name,
&i.Icon,
&i.CallbackURL,
pq.Array(&i.RedirectUris),
&i.ClientType,
&i.DynamicallyRegistered,
&i.ClientIDIssuedAt,
&i.ClientSecretExpiresAt,
pq.Array(&i.GrantTypes),
pq.Array(&i.ResponseTypes),
&i.TokenEndpointAuthMethod,
&i.Scope,
pq.Array(&i.Contacts),
&i.ClientUri,
&i.LogoUri,
&i.TosUri,
&i.PolicyUri,
&i.JwksUri,
&i.Jwks,
&i.SoftwareID,
&i.SoftwareVersion,
&i.RegistrationAccessToken,
&i.RegistrationClientUri,
)
return i, err
}
const deleteOrganizationMember = `-- name: DeleteOrganizationMember :exec
DELETE
FROM
organization_members
WHERE
organization_id = $1 AND
user_id = $2
`
type DeleteOrganizationMemberParams struct {
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
}
func (q *sqlQuerier) DeleteOrganizationMember(ctx context.Context, arg DeleteOrganizationMemberParams) error {
_, err := q.db.ExecContext(ctx, deleteOrganizationMember, arg.OrganizationID, arg.UserID)
return err
}
const getOrganizationIDsByMemberIDs = `-- name: GetOrganizationIDsByMemberIDs :many
SELECT
user_id, array_agg(organization_id) :: uuid [ ] AS "organization_IDs"
FROM
organization_members
WHERE
user_id = ANY($1 :: uuid [ ])
GROUP BY
user_id
`
type GetOrganizationIDsByMemberIDsRow struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
OrganizationIDs []uuid.UUID `db:"organization_IDs" json:"organization_IDs"`
}
func (q *sqlQuerier) GetOrganizationIDsByMemberIDs(ctx context.Context, ids []uuid.UUID) ([]GetOrganizationIDsByMemberIDsRow, error) {
rows, err := q.db.QueryContext(ctx, getOrganizationIDsByMemberIDs, pq.Array(ids))
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetOrganizationIDsByMemberIDsRow
for rows.Next() {
var i GetOrganizationIDsByMemberIDsRow
if err := rows.Scan(&i.UserID, pq.Array(&i.OrganizationIDs)); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertOrganizationMember = `-- name: InsertOrganizationMember :one
INSERT INTO
organization_members (
organization_id,
user_id,
created_at,
updated_at,
roles
)
VALUES
($1, $2, $3, $4, $5) RETURNING user_id, organization_id, created_at, updated_at, roles
`
type InsertOrganizationMemberParams struct {
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Roles []string `db:"roles" json:"roles"`
}
func (q *sqlQuerier) InsertOrganizationMember(ctx context.Context, arg InsertOrganizationMemberParams) (OrganizationMember, error) {
row := q.db.QueryRowContext(ctx, insertOrganizationMember,
arg.OrganizationID,
arg.UserID,
arg.CreatedAt,
arg.UpdatedAt,
pq.Array(arg.Roles),
)
var i OrganizationMember
err := row.Scan(
&i.UserID,
&i.OrganizationID,
&i.CreatedAt,
&i.UpdatedAt,
pq.Array(&i.Roles),
)
return i, err
}
const organizationMembers = `-- name: OrganizationMembers :many
SELECT
organization_members.user_id, organization_members.organization_id, organization_members.created_at, organization_members.updated_at, organization_members.roles,
users.username, users.avatar_url, users.name, users.email, users.rbac_roles as "global_roles",
users.last_seen_at, users.status, users.login_type, users.is_service_account,
users.created_at as user_created_at, users.updated_at as user_updated_at
FROM
organization_members
INNER JOIN
users ON organization_members.user_id = users.id AND users.deleted = false
WHERE
-- Filter by organization id
CASE
WHEN $1 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
organization_id = $1
ELSE true
END
-- Filter by user id
AND CASE
WHEN $2 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
user_id = $2
ELSE true
END
-- Filter by system type
AND CASE
WHEN $3::bool THEN TRUE
ELSE
is_system = false
END
-- Filter by github user ID. Note that this requires a join on the users table.
AND CASE
WHEN $4 :: bigint != 0 THEN
users.github_com_user_id = $4
ELSE true
END
`
type OrganizationMembersParams struct {
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
IncludeSystem bool `db:"include_system" json:"include_system"`
GithubUserID int64 `db:"github_user_id" json:"github_user_id"`
}
type OrganizationMembersRow struct {
OrganizationMember OrganizationMember `db:"organization_member" json:"organization_member"`
Username string `db:"username" json:"username"`
AvatarURL string `db:"avatar_url" json:"avatar_url"`
Name string `db:"name" json:"name"`
Email string `db:"email" json:"email"`
GlobalRoles pq.StringArray `db:"global_roles" json:"global_roles"`
LastSeenAt time.Time `db:"last_seen_at" json:"last_seen_at"`
Status UserStatus `db:"status" json:"status"`
LoginType LoginType `db:"login_type" json:"login_type"`
IsServiceAccount bool `db:"is_service_account" json:"is_service_account"`
UserCreatedAt time.Time `db:"user_created_at" json:"user_created_at"`
UserUpdatedAt time.Time `db:"user_updated_at" json:"user_updated_at"`
}
// Arguments are optional with uuid.Nil to ignore.
// - Use just 'organization_id' to get all members of an org
// - Use just 'user_id' to get all orgs a user is a member of
// - Use both to get a specific org member row
func (q *sqlQuerier) OrganizationMembers(ctx context.Context, arg OrganizationMembersParams) ([]OrganizationMembersRow, error) {
rows, err := q.db.QueryContext(ctx, organizationMembers,
arg.OrganizationID,
arg.UserID,
arg.IncludeSystem,
arg.GithubUserID,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []OrganizationMembersRow
for rows.Next() {
var i OrganizationMembersRow
if err := rows.Scan(
&i.OrganizationMember.UserID,
&i.OrganizationMember.OrganizationID,
&i.OrganizationMember.CreatedAt,
&i.OrganizationMember.UpdatedAt,
pq.Array(&i.OrganizationMember.Roles),
&i.Username,
&i.AvatarURL,
&i.Name,
&i.Email,
&i.GlobalRoles,
&i.LastSeenAt,
&i.Status,
&i.LoginType,
&i.IsServiceAccount,
&i.UserCreatedAt,
&i.UserUpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const paginatedOrganizationMembers = `-- name: PaginatedOrganizationMembers :many
SELECT
organization_members.user_id, organization_members.organization_id, organization_members.created_at, organization_members.updated_at, organization_members.roles,
users.username, users.avatar_url, users.name, users.email, users.rbac_roles as "global_roles",
users.last_seen_at, users.status, users.login_type, users.is_service_account,
users.created_at as user_created_at, users.updated_at as user_updated_at,
COUNT(*) OVER() AS count
FROM
organization_members
INNER JOIN
users ON organization_members.user_id = users.id AND users.deleted = false
WHERE
CASE
-- This allows using the last element on a page as effectively a cursor.
-- This is an important option for scripts that need to paginate without
-- duplicating or missing data.
WHEN $1 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN (
-- The pagination cursor is the last ID of the previous page.
-- The query is ordered by the username field, so select all
-- rows after the cursor.
(LOWER(users.username)) > (
SELECT
LOWER(users.username)
FROM
organization_members
INNER JOIN
users ON organization_members.user_id = users.id
WHERE
organization_members.user_id = $1
)
)
ELSE true
END
-- Start filters
-- Filter by organization id
AND CASE
WHEN $2 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
organization_id = $2
ELSE true
END
-- Filter by email, username, or name (display name)
AND CASE
WHEN $3 :: text != '' THEN (
users.email ILIKE concat('%', $3, '%')
OR users.username ILIKE concat('%', $3, '%')
OR users.name ILIKE concat('%', $3, '%')
)
ELSE true
END
-- Filter by name (display name)
AND CASE
WHEN $4 :: text != '' THEN
users.name ILIKE concat('%', $4, '%')
ELSE true
END
-- Filter by exact username
AND CASE
WHEN $5 :: text != '' THEN
lower(users.username) = lower($5)
ELSE true
END
-- Filter by exact email
AND CASE
WHEN $6 :: text != '' THEN
lower(users.email) = lower($6)
ELSE true
END
-- Filter by status
AND CASE
-- @status needs to be a text because it can be empty, If it was
-- user_status enum, it would not.
WHEN cardinality($7 :: user_status[]) > 0 THEN
users.status = ANY($7 :: user_status[])
ELSE true
END
-- Filter by global rbac_roles
AND CASE
-- @rbac_role allows filtering by rbac roles. If 'member' is included, show everyone, as
-- everyone is a member.
WHEN cardinality($8 :: text[]) > 0 AND 'member' != ANY($8 :: text[]) THEN
users.rbac_roles && $8 :: text[]
ELSE true
END
-- Filter by last_seen
AND CASE
WHEN $9 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
users.last_seen_at <= $9
ELSE true
END
AND CASE
WHEN $10 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
users.last_seen_at >= $10
ELSE true
END
-- Filter by created_at (user creation date, not date added to org)
AND CASE
WHEN $11 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
users.created_at <= $11
ELSE true
END
AND CASE
WHEN $12 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
users.created_at >= $12
ELSE true
END
-- Filter by system type
AND CASE
WHEN $13::bool THEN TRUE
ELSE users.is_system = false
END
-- Filter by github.com user ID
AND CASE
WHEN $14 :: bigint != 0 THEN
users.github_com_user_id = $14
ELSE true
END
-- Filter by login_type
AND CASE
WHEN cardinality($15 :: login_type[]) > 0 THEN
users.login_type = ANY($15 :: login_type[])
ELSE true
END
-- Filter by service account.
AND CASE
WHEN $16 :: boolean IS NOT NULL THEN
users.is_service_account = $16 :: boolean
ELSE true
END
-- End of filters
ORDER BY
-- Deterministic and consistent ordering of all users. This is to ensure consistent pagination.
LOWER(users.username) ASC OFFSET $17
LIMIT
-- A null limit means "no limit", so 0 means return all
NULLIF($18 :: int, 0)
`
type PaginatedOrganizationMembersParams struct {
AfterID uuid.UUID `db:"after_id" json:"after_id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
Search string `db:"search" json:"search"`
Name string `db:"name" json:"name"`
ExactUsername string `db:"exact_username" json:"exact_username"`
ExactEmail string `db:"exact_email" json:"exact_email"`
Status []UserStatus `db:"status" json:"status"`
RbacRole []string `db:"rbac_role" json:"rbac_role"`
LastSeenBefore time.Time `db:"last_seen_before" json:"last_seen_before"`
LastSeenAfter time.Time `db:"last_seen_after" json:"last_seen_after"`
CreatedBefore time.Time `db:"created_before" json:"created_before"`
CreatedAfter time.Time `db:"created_after" json:"created_after"`
IncludeSystem bool `db:"include_system" json:"include_system"`
GithubComUserID int64 `db:"github_com_user_id" json:"github_com_user_id"`
LoginType []LoginType `db:"login_type" json:"login_type"`
IsServiceAccount sql.NullBool `db:"is_service_account" json:"is_service_account"`
OffsetOpt int32 `db:"offset_opt" json:"offset_opt"`
LimitOpt int32 `db:"limit_opt" json:"limit_opt"`
}
type PaginatedOrganizationMembersRow struct {
OrganizationMember OrganizationMember `db:"organization_member" json:"organization_member"`
Username string `db:"username" json:"username"`
AvatarURL string `db:"avatar_url" json:"avatar_url"`
Name string `db:"name" json:"name"`
Email string `db:"email" json:"email"`
GlobalRoles pq.StringArray `db:"global_roles" json:"global_roles"`
LastSeenAt time.Time `db:"last_seen_at" json:"last_seen_at"`
Status UserStatus `db:"status" json:"status"`
LoginType LoginType `db:"login_type" json:"login_type"`
IsServiceAccount bool `db:"is_service_account" json:"is_service_account"`
UserCreatedAt time.Time `db:"user_created_at" json:"user_created_at"`
UserUpdatedAt time.Time `db:"user_updated_at" json:"user_updated_at"`
Count int64 `db:"count" json:"count"`
}
func (q *sqlQuerier) PaginatedOrganizationMembers(ctx context.Context, arg PaginatedOrganizationMembersParams) ([]PaginatedOrganizationMembersRow, error) {
rows, err := q.db.QueryContext(ctx, paginatedOrganizationMembers,
arg.AfterID,
arg.OrganizationID,
arg.Search,
arg.Name,
arg.ExactUsername,
arg.ExactEmail,
pq.Array(arg.Status),
pq.Array(arg.RbacRole),
arg.LastSeenBefore,
arg.LastSeenAfter,
arg.CreatedBefore,
arg.CreatedAfter,
arg.IncludeSystem,
arg.GithubComUserID,
pq.Array(arg.LoginType),
arg.IsServiceAccount,
arg.OffsetOpt,
arg.LimitOpt,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []PaginatedOrganizationMembersRow
for rows.Next() {
var i PaginatedOrganizationMembersRow
if err := rows.Scan(
&i.OrganizationMember.UserID,
&i.OrganizationMember.OrganizationID,
&i.OrganizationMember.CreatedAt,
&i.OrganizationMember.UpdatedAt,
pq.Array(&i.OrganizationMember.Roles),
&i.Username,
&i.AvatarURL,
&i.Name,
&i.Email,
&i.GlobalRoles,
&i.LastSeenAt,
&i.Status,
&i.LoginType,
&i.IsServiceAccount,
&i.UserCreatedAt,
&i.UserUpdatedAt,
&i.Count,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateMemberRoles = `-- name: UpdateMemberRoles :one
UPDATE
organization_members
SET
-- Remove all duplicates from the roles.
roles = ARRAY(SELECT DISTINCT UNNEST($1 :: text[]))
WHERE
user_id = $2
AND organization_id = $3
RETURNING user_id, organization_id, created_at, updated_at, roles
`
type UpdateMemberRolesParams struct {
GrantedRoles []string `db:"granted_roles" json:"granted_roles"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
OrgID uuid.UUID `db:"org_id" json:"org_id"`
}
func (q *sqlQuerier) UpdateMemberRoles(ctx context.Context, arg UpdateMemberRolesParams) (OrganizationMember, error) {
row := q.db.QueryRowContext(ctx, updateMemberRoles, pq.Array(arg.GrantedRoles), arg.UserID, arg.OrgID)
var i OrganizationMember
err := row.Scan(
&i.UserID,
&i.OrganizationID,
&i.CreatedAt,
&i.UpdatedAt,
pq.Array(&i.Roles),
)
return i, err
}
const getDefaultOrganization = `-- name: GetDefaultOrganization :one
SELECT
id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, shareable_workspace_owners, default_org_member_roles
FROM
organizations
WHERE
is_default = true
LIMIT
1
`
func (q *sqlQuerier) GetDefaultOrganization(ctx context.Context) (Organization, error) {
row := q.db.QueryRowContext(ctx, getDefaultOrganization)
var i Organization
err := row.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.CreatedAt,
&i.UpdatedAt,
&i.IsDefault,
&i.DisplayName,
&i.Icon,
&i.Deleted,
&i.ShareableWorkspaceOwners,
pq.Array(&i.DefaultOrgMemberRoles),
)
return i, err
}
const getOrganizationByID = `-- name: GetOrganizationByID :one
SELECT
id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, shareable_workspace_owners, default_org_member_roles
FROM
organizations
WHERE
id = $1
`
func (q *sqlQuerier) GetOrganizationByID(ctx context.Context, id uuid.UUID) (Organization, error) {
row := q.db.QueryRowContext(ctx, getOrganizationByID, id)
var i Organization
err := row.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.CreatedAt,
&i.UpdatedAt,
&i.IsDefault,
&i.DisplayName,
&i.Icon,
&i.Deleted,
&i.ShareableWorkspaceOwners,
pq.Array(&i.DefaultOrgMemberRoles),
)
return i, err
}
const getOrganizationByName = `-- name: GetOrganizationByName :one
SELECT
id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, shareable_workspace_owners, default_org_member_roles
FROM
organizations
WHERE
-- Optionally include deleted organizations
deleted = $1 AND
LOWER("name") = LOWER($2)
LIMIT
1
`
type GetOrganizationByNameParams struct {
Deleted bool `db:"deleted" json:"deleted"`
Name string `db:"name" json:"name"`
}
func (q *sqlQuerier) GetOrganizationByName(ctx context.Context, arg GetOrganizationByNameParams) (Organization, error) {
row := q.db.QueryRowContext(ctx, getOrganizationByName, arg.Deleted, arg.Name)
var i Organization
err := row.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.CreatedAt,
&i.UpdatedAt,
&i.IsDefault,
&i.DisplayName,
&i.Icon,
&i.Deleted,
&i.ShareableWorkspaceOwners,
pq.Array(&i.DefaultOrgMemberRoles),
)
return i, err
}
const getOrganizationResourceCountByID = `-- name: GetOrganizationResourceCountByID :one
SELECT
(
SELECT
count(*)
FROM
workspaces
WHERE
workspaces.organization_id = $1
AND workspaces.deleted = FALSE) AS workspace_count,
(
SELECT
count(*)
FROM
GROUPS
WHERE
groups.organization_id = $1) AS group_count,
(
SELECT
count(*)
FROM
templates
WHERE
templates.organization_id = $1
AND templates.deleted = FALSE) AS template_count,
(
SELECT
count(*)
FROM
organization_members
LEFT JOIN users ON organization_members.user_id = users.id
WHERE
organization_members.organization_id = $1
AND users.deleted = FALSE) AS member_count,
(
SELECT
count(*)
FROM
provisioner_keys
WHERE
provisioner_keys.organization_id = $1) AS provisioner_key_count
`
type GetOrganizationResourceCountByIDRow struct {
WorkspaceCount int64 `db:"workspace_count" json:"workspace_count"`
GroupCount int64 `db:"group_count" json:"group_count"`
TemplateCount int64 `db:"template_count" json:"template_count"`
MemberCount int64 `db:"member_count" json:"member_count"`
ProvisionerKeyCount int64 `db:"provisioner_key_count" json:"provisioner_key_count"`
}
func (q *sqlQuerier) GetOrganizationResourceCountByID(ctx context.Context, organizationID uuid.UUID) (GetOrganizationResourceCountByIDRow, error) {
row := q.db.QueryRowContext(ctx, getOrganizationResourceCountByID, organizationID)
var i GetOrganizationResourceCountByIDRow
err := row.Scan(
&i.WorkspaceCount,
&i.GroupCount,
&i.TemplateCount,
&i.MemberCount,
&i.ProvisionerKeyCount,
)
return i, err
}
const getOrganizations = `-- name: GetOrganizations :many
SELECT
id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, shareable_workspace_owners, default_org_member_roles
FROM
organizations
WHERE
-- Optionally include deleted organizations
deleted = $1
-- Filter by ids
AND CASE
WHEN array_length($2 :: uuid[], 1) > 0 THEN
id = ANY($2)
ELSE true
END
AND CASE
WHEN $3::text != '' THEN
LOWER("name") = LOWER($3)
ELSE true
END
`
type GetOrganizationsParams struct {
Deleted bool `db:"deleted" json:"deleted"`
IDs []uuid.UUID `db:"ids" json:"ids"`
Name string `db:"name" json:"name"`
}
func (q *sqlQuerier) GetOrganizations(ctx context.Context, arg GetOrganizationsParams) ([]Organization, error) {
rows, err := q.db.QueryContext(ctx, getOrganizations, arg.Deleted, pq.Array(arg.IDs), arg.Name)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Organization
for rows.Next() {
var i Organization
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.CreatedAt,
&i.UpdatedAt,
&i.IsDefault,
&i.DisplayName,
&i.Icon,
&i.Deleted,
&i.ShareableWorkspaceOwners,
pq.Array(&i.DefaultOrgMemberRoles),
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getOrganizationsByUserID = `-- name: GetOrganizationsByUserID :many
SELECT
id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, shareable_workspace_owners, default_org_member_roles
FROM
organizations
WHERE
-- Optionally provide a filter for deleted organizations.
CASE WHEN
$2 :: boolean IS NULL THEN
true
ELSE
deleted = $2
END AND
id = ANY(
SELECT
organization_id
FROM
organization_members
WHERE
user_id = $1
)
`
type GetOrganizationsByUserIDParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
Deleted sql.NullBool `db:"deleted" json:"deleted"`
}
func (q *sqlQuerier) GetOrganizationsByUserID(ctx context.Context, arg GetOrganizationsByUserIDParams) ([]Organization, error) {
rows, err := q.db.QueryContext(ctx, getOrganizationsByUserID, arg.UserID, arg.Deleted)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Organization
for rows.Next() {
var i Organization
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.CreatedAt,
&i.UpdatedAt,
&i.IsDefault,
&i.DisplayName,
&i.Icon,
&i.Deleted,
&i.ShareableWorkspaceOwners,
pq.Array(&i.DefaultOrgMemberRoles),
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertOrganization = `-- name: InsertOrganization :one
INSERT INTO
organizations (id, "name", display_name, description, icon, created_at, updated_at, is_default, default_org_member_roles)
VALUES
-- If no organizations exist, and this is the first, make it the default.
($1, $2, $3, $4, $5, $6, $7, (SELECT TRUE FROM organizations LIMIT 1) IS NULL, $8) RETURNING id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, shareable_workspace_owners, default_org_member_roles
`
type InsertOrganizationParams struct {
ID uuid.UUID `db:"id" json:"id"`
Name string `db:"name" json:"name"`
DisplayName string `db:"display_name" json:"display_name"`
Description string `db:"description" json:"description"`
Icon string `db:"icon" json:"icon"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
DefaultOrgMemberRoles []string `db:"default_org_member_roles" json:"default_org_member_roles"`
}
func (q *sqlQuerier) InsertOrganization(ctx context.Context, arg InsertOrganizationParams) (Organization, error) {
row := q.db.QueryRowContext(ctx, insertOrganization,
arg.ID,
arg.Name,
arg.DisplayName,
arg.Description,
arg.Icon,
arg.CreatedAt,
arg.UpdatedAt,
pq.Array(arg.DefaultOrgMemberRoles),
)
var i Organization
err := row.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.CreatedAt,
&i.UpdatedAt,
&i.IsDefault,
&i.DisplayName,
&i.Icon,
&i.Deleted,
&i.ShareableWorkspaceOwners,
pq.Array(&i.DefaultOrgMemberRoles),
)
return i, err
}
const updateOrganization = `-- name: UpdateOrganization :one
UPDATE
organizations
SET
updated_at = $1,
name = $2,
display_name = $3,
description = $4,
icon = $5,
default_org_member_roles = $6
WHERE
id = $7
RETURNING id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, shareable_workspace_owners, default_org_member_roles
`
type UpdateOrganizationParams struct {
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Name string `db:"name" json:"name"`
DisplayName string `db:"display_name" json:"display_name"`
Description string `db:"description" json:"description"`
Icon string `db:"icon" json:"icon"`
DefaultOrgMemberRoles []string `db:"default_org_member_roles" json:"default_org_member_roles"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateOrganization(ctx context.Context, arg UpdateOrganizationParams) (Organization, error) {
row := q.db.QueryRowContext(ctx, updateOrganization,
arg.UpdatedAt,
arg.Name,
arg.DisplayName,
arg.Description,
arg.Icon,
pq.Array(arg.DefaultOrgMemberRoles),
arg.ID,
)
var i Organization
err := row.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.CreatedAt,
&i.UpdatedAt,
&i.IsDefault,
&i.DisplayName,
&i.Icon,
&i.Deleted,
&i.ShareableWorkspaceOwners,
pq.Array(&i.DefaultOrgMemberRoles),
)
return i, err
}
const updateOrganizationDeletedByID = `-- name: UpdateOrganizationDeletedByID :exec
UPDATE organizations
SET
deleted = true,
updated_at = $1
WHERE
id = $2 AND
is_default = false
`
type UpdateOrganizationDeletedByIDParams struct {
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateOrganizationDeletedByID(ctx context.Context, arg UpdateOrganizationDeletedByIDParams) error {
_, err := q.db.ExecContext(ctx, updateOrganizationDeletedByID, arg.UpdatedAt, arg.ID)
return err
}
const updateOrganizationWorkspaceSharingSettings = `-- name: UpdateOrganizationWorkspaceSharingSettings :one
UPDATE
organizations
SET
shareable_workspace_owners = $1,
updated_at = $2
WHERE
id = $3
RETURNING id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, shareable_workspace_owners, default_org_member_roles
`
type UpdateOrganizationWorkspaceSharingSettingsParams struct {
ShareableWorkspaceOwners ShareableWorkspaceOwners `db:"shareable_workspace_owners" json:"shareable_workspace_owners"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateOrganizationWorkspaceSharingSettings(ctx context.Context, arg UpdateOrganizationWorkspaceSharingSettingsParams) (Organization, error) {
row := q.db.QueryRowContext(ctx, updateOrganizationWorkspaceSharingSettings, arg.ShareableWorkspaceOwners, arg.UpdatedAt, arg.ID)
var i Organization
err := row.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.CreatedAt,
&i.UpdatedAt,
&i.IsDefault,
&i.DisplayName,
&i.Icon,
&i.Deleted,
&i.ShareableWorkspaceOwners,
pq.Array(&i.DefaultOrgMemberRoles),
)
return i, err
}
const getParameterSchemasByJobID = `-- name: GetParameterSchemasByJobID :many
SELECT
id, created_at, job_id, name, description, default_source_scheme, default_source_value, allow_override_source, default_destination_scheme, allow_override_destination, default_refresh, redisplay_value, validation_error, validation_condition, validation_type_system, validation_value_type, index
FROM
parameter_schemas
WHERE
job_id = $1
ORDER BY
index
`
func (q *sqlQuerier) GetParameterSchemasByJobID(ctx context.Context, jobID uuid.UUID) ([]ParameterSchema, error) {
rows, err := q.db.QueryContext(ctx, getParameterSchemasByJobID, jobID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ParameterSchema
for rows.Next() {
var i ParameterSchema
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.JobID,
&i.Name,
&i.Description,
&i.DefaultSourceScheme,
&i.DefaultSourceValue,
&i.AllowOverrideSource,
&i.DefaultDestinationScheme,
&i.AllowOverrideDestination,
&i.DefaultRefresh,
&i.RedisplayValue,
&i.ValidationError,
&i.ValidationCondition,
&i.ValidationTypeSystem,
&i.ValidationValueType,
&i.Index,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const claimPrebuiltWorkspace = `-- name: ClaimPrebuiltWorkspace :one
UPDATE workspaces w
SET owner_id = $1::uuid,
name = $2::text,
updated_at = $3::timestamptz,
-- Update autostart_schedule, next_start_at and ttl according to template and workspace-level
-- configurations, allowing the workspace to be managed by the lifecycle executor as expected.
autostart_schedule = $4,
next_start_at = $5,
ttl = $6,
-- Update last_used_at during claim to ensure the claimed workspace is treated as recently used.
-- This avoids unintended dormancy caused by prebuilds having stale usage timestamps.
last_used_at = $3::timestamptz,
-- Clear dormant and deletion timestamps as a safeguard to ensure a clean lifecycle state after claim.
-- These fields should not be set on prebuilds, but we defensively reset them here to prevent
-- accidental dormancy or deletion by the lifecycle executor.
dormant_at = NULL,
deleting_at = NULL
WHERE w.id IN (
SELECT p.id
FROM workspace_prebuilds p
INNER JOIN workspace_latest_builds b ON b.workspace_id = p.id
INNER JOIN templates t ON p.template_id = t.id
WHERE (b.transition = 'start'::workspace_transition
AND b.job_status IN ('succeeded'::provisioner_job_status))
-- The prebuilds system should never try to claim a prebuild for an inactive template version.
-- Nevertheless, this filter is here as a defensive measure:
AND b.template_version_id = t.active_version_id
AND p.current_preset_id = $7::uuid
AND p.ready
AND NOT t.deleted
LIMIT 1 FOR UPDATE OF p SKIP LOCKED -- Ensure that a concurrent request will not select the same prebuild.
)
RETURNING w.id, w.name
`
type ClaimPrebuiltWorkspaceParams struct {
NewUserID uuid.UUID `db:"new_user_id" json:"new_user_id"`
NewName string `db:"new_name" json:"new_name"`
Now time.Time `db:"now" json:"now"`
AutostartSchedule sql.NullString `db:"autostart_schedule" json:"autostart_schedule"`
NextStartAt sql.NullTime `db:"next_start_at" json:"next_start_at"`
WorkspaceTtl sql.NullInt64 `db:"workspace_ttl" json:"workspace_ttl"`
PresetID uuid.UUID `db:"preset_id" json:"preset_id"`
}
type ClaimPrebuiltWorkspaceRow struct {
ID uuid.UUID `db:"id" json:"id"`
Name string `db:"name" json:"name"`
}
func (q *sqlQuerier) ClaimPrebuiltWorkspace(ctx context.Context, arg ClaimPrebuiltWorkspaceParams) (ClaimPrebuiltWorkspaceRow, error) {
row := q.db.QueryRowContext(ctx, claimPrebuiltWorkspace,
arg.NewUserID,
arg.NewName,
arg.Now,
arg.AutostartSchedule,
arg.NextStartAt,
arg.WorkspaceTtl,
arg.PresetID,
)
var i ClaimPrebuiltWorkspaceRow
err := row.Scan(&i.ID, &i.Name)
return i, err
}
const countInProgressPrebuilds = `-- name: CountInProgressPrebuilds :many
SELECT t.id AS template_id, wpb.template_version_id, wpb.transition, COUNT(wpb.transition)::int AS count, wlb.template_version_preset_id as preset_id
FROM workspace_latest_builds wlb
INNER JOIN workspace_prebuild_builds wpb ON wpb.id = wlb.id
-- We only need these counts for active template versions.
-- It doesn't influence whether we create or delete prebuilds
-- for inactive template versions. This is because we never create
-- prebuilds for inactive template versions, we always delete
-- running prebuilds for inactive template versions, and we ignore
-- prebuilds that are still building.
INNER JOIN templates t ON t.active_version_id = wlb.template_version_id
WHERE wlb.job_status IN ('pending'::provisioner_job_status, 'running'::provisioner_job_status)
-- AND NOT t.deleted -- We don't exclude deleted templates because there's no constraint in the DB preventing a soft deletion on a template while workspaces are running.
GROUP BY t.id, wpb.template_version_id, wpb.transition, wlb.template_version_preset_id
`
type CountInProgressPrebuildsRow struct {
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
Transition WorkspaceTransition `db:"transition" json:"transition"`
Count int32 `db:"count" json:"count"`
PresetID uuid.NullUUID `db:"preset_id" json:"preset_id"`
}
// CountInProgressPrebuilds returns the number of in-progress prebuilds, grouped by preset ID and transition.
// Prebuild considered in-progress if it's in the "pending", "starting", "stopping", or "deleting" state.
func (q *sqlQuerier) CountInProgressPrebuilds(ctx context.Context) ([]CountInProgressPrebuildsRow, error) {
rows, err := q.db.QueryContext(ctx, countInProgressPrebuilds)
if err != nil {
return nil, err
}
defer rows.Close()
var items []CountInProgressPrebuildsRow
for rows.Next() {
var i CountInProgressPrebuildsRow
if err := rows.Scan(
&i.TemplateID,
&i.TemplateVersionID,
&i.Transition,
&i.Count,
&i.PresetID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const countPendingNonActivePrebuilds = `-- name: CountPendingNonActivePrebuilds :many
SELECT
wpb.template_version_preset_id AS preset_id,
COUNT(*)::int AS count
FROM workspace_prebuild_builds wpb
INNER JOIN provisioner_jobs pj ON pj.id = wpb.job_id
INNER JOIN workspaces w ON w.id = wpb.workspace_id
INNER JOIN templates t ON t.id = w.template_id
WHERE
wpb.template_version_id != t.active_version_id
-- Only considers initial builds, i.e. created by the reconciliation loop
AND wpb.build_number = 1
-- Only consider 'start' transitions (provisioning), not 'stop'/'delete' (deprovisioning)
-- Deprovisioning jobs should complete naturally as they're already cleaning up resources
AND wpb.transition = 'start'::workspace_transition
-- Pending jobs that have not yet been picked up by a provisioner
AND pj.job_status = 'pending'::provisioner_job_status
AND pj.worker_id IS NULL
AND pj.canceled_at IS NULL
AND pj.completed_at IS NULL
GROUP BY wpb.template_version_preset_id
`
type CountPendingNonActivePrebuildsRow struct {
PresetID uuid.NullUUID `db:"preset_id" json:"preset_id"`
Count int32 `db:"count" json:"count"`
}
// CountPendingNonActivePrebuilds returns the number of pending prebuilds for non-active template versions
func (q *sqlQuerier) CountPendingNonActivePrebuilds(ctx context.Context) ([]CountPendingNonActivePrebuildsRow, error) {
rows, err := q.db.QueryContext(ctx, countPendingNonActivePrebuilds)
if err != nil {
return nil, err
}
defer rows.Close()
var items []CountPendingNonActivePrebuildsRow
for rows.Next() {
var i CountPendingNonActivePrebuildsRow
if err := rows.Scan(&i.PresetID, &i.Count); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const findMatchingPresetID = `-- name: FindMatchingPresetID :one
WITH provided_params AS (
SELECT
unnest($1::text[]) AS name,
unnest($2::text[]) AS value
),
preset_matches AS (
SELECT
tvp.id AS template_version_preset_id,
COALESCE(COUNT(tvpp.name), 0) AS total_preset_params,
COALESCE(COUNT(pp.name), 0) AS matching_params
FROM template_version_presets tvp
LEFT JOIN template_version_preset_parameters tvpp ON tvpp.template_version_preset_id = tvp.id
LEFT JOIN provided_params pp ON pp.name = tvpp.name AND pp.value = tvpp.value
WHERE tvp.template_version_id = $3
GROUP BY tvp.id
)
SELECT pm.template_version_preset_id
FROM preset_matches pm
WHERE pm.total_preset_params = pm.matching_params -- All preset parameters must match
ORDER BY pm.total_preset_params DESC -- Return the preset with the most parameters
LIMIT 1
`
type FindMatchingPresetIDParams struct {
ParameterNames []string `db:"parameter_names" json:"parameter_names"`
ParameterValues []string `db:"parameter_values" json:"parameter_values"`
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
}
// FindMatchingPresetID finds a preset ID that is the largest exact subset of the provided parameters.
// It returns the preset ID if a match is found, or NULL if no match is found.
// The query finds presets where all preset parameters are present in the provided parameters,
// and returns the preset with the most parameters (largest subset).
func (q *sqlQuerier) FindMatchingPresetID(ctx context.Context, arg FindMatchingPresetIDParams) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, findMatchingPresetID, pq.Array(arg.ParameterNames), pq.Array(arg.ParameterValues), arg.TemplateVersionID)
var template_version_preset_id uuid.UUID
err := row.Scan(&template_version_preset_id)
return template_version_preset_id, err
}
const getOrganizationsWithPrebuildStatus = `-- name: GetOrganizationsWithPrebuildStatus :many
WITH orgs_with_prebuilds AS (
-- Get unique organizations that have presets with prebuilds configured
SELECT DISTINCT o.id, o.name
FROM organizations o
INNER JOIN templates t ON t.organization_id = o.id
INNER JOIN template_versions tv ON tv.template_id = t.id
INNER JOIN template_version_presets tvp ON tvp.template_version_id = tv.id
WHERE tvp.desired_instances IS NOT NULL
),
prebuild_user_membership AS (
-- Check if the user is a member of the organizations
SELECT om.organization_id
FROM organization_members om
INNER JOIN orgs_with_prebuilds owp ON owp.id = om.organization_id
WHERE om.user_id = $1::uuid
),
prebuild_groups AS (
-- Check if the organizations have the prebuilds group
SELECT g.organization_id, g.id as group_id
FROM groups g
INNER JOIN orgs_with_prebuilds owp ON owp.id = g.organization_id
WHERE g.name = $2::text
),
prebuild_group_membership AS (
-- Check if the user is in the prebuilds group
SELECT pg.organization_id
FROM prebuild_groups pg
INNER JOIN group_members gm ON gm.group_id = pg.group_id
WHERE gm.user_id = $1::uuid
)
SELECT
owp.id AS organization_id,
owp.name AS organization_name,
(pum.organization_id IS NOT NULL)::boolean AS has_prebuild_user,
pg.group_id AS prebuilds_group_id,
(pgm.organization_id IS NOT NULL)::boolean AS has_prebuild_user_in_group
FROM orgs_with_prebuilds owp
LEFT JOIN prebuild_groups pg ON pg.organization_id = owp.id
LEFT JOIN prebuild_user_membership pum ON pum.organization_id = owp.id
LEFT JOIN prebuild_group_membership pgm ON pgm.organization_id = owp.id
`
type GetOrganizationsWithPrebuildStatusParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
GroupName string `db:"group_name" json:"group_name"`
}
type GetOrganizationsWithPrebuildStatusRow struct {
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
OrganizationName string `db:"organization_name" json:"organization_name"`
HasPrebuildUser bool `db:"has_prebuild_user" json:"has_prebuild_user"`
PrebuildsGroupID uuid.NullUUID `db:"prebuilds_group_id" json:"prebuilds_group_id"`
HasPrebuildUserInGroup bool `db:"has_prebuild_user_in_group" json:"has_prebuild_user_in_group"`
}
// GetOrganizationsWithPrebuildStatus returns organizations with prebuilds configured and their
// membership status for the prebuilds system user (org membership, group existence, group membership).
func (q *sqlQuerier) GetOrganizationsWithPrebuildStatus(ctx context.Context, arg GetOrganizationsWithPrebuildStatusParams) ([]GetOrganizationsWithPrebuildStatusRow, error) {
rows, err := q.db.QueryContext(ctx, getOrganizationsWithPrebuildStatus, arg.UserID, arg.GroupName)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetOrganizationsWithPrebuildStatusRow
for rows.Next() {
var i GetOrganizationsWithPrebuildStatusRow
if err := rows.Scan(
&i.OrganizationID,
&i.OrganizationName,
&i.HasPrebuildUser,
&i.PrebuildsGroupID,
&i.HasPrebuildUserInGroup,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getPrebuildMetrics = `-- name: GetPrebuildMetrics :many
SELECT
t.name as template_name,
tvp.name as preset_name,
o.name as organization_name,
COUNT(*) as created_count,
COUNT(*) FILTER (WHERE pj.job_status = 'failed'::provisioner_job_status) as failed_count,
COUNT(*) FILTER (
WHERE w.owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid -- The system user responsible for prebuilds.
) as claimed_count
FROM workspaces w
INNER JOIN workspace_prebuild_builds wpb ON wpb.workspace_id = w.id
INNER JOIN templates t ON t.id = w.template_id
INNER JOIN template_version_presets tvp ON tvp.id = wpb.template_version_preset_id
INNER JOIN provisioner_jobs pj ON pj.id = wpb.job_id
INNER JOIN organizations o ON o.id = w.organization_id
WHERE NOT t.deleted AND wpb.build_number = 1
GROUP BY t.name, tvp.name, o.name
ORDER BY t.name, tvp.name, o.name
`
type GetPrebuildMetricsRow struct {
TemplateName string `db:"template_name" json:"template_name"`
PresetName string `db:"preset_name" json:"preset_name"`
OrganizationName string `db:"organization_name" json:"organization_name"`
CreatedCount int64 `db:"created_count" json:"created_count"`
FailedCount int64 `db:"failed_count" json:"failed_count"`
ClaimedCount int64 `db:"claimed_count" json:"claimed_count"`
}
func (q *sqlQuerier) GetPrebuildMetrics(ctx context.Context) ([]GetPrebuildMetricsRow, error) {
rows, err := q.db.QueryContext(ctx, getPrebuildMetrics)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetPrebuildMetricsRow
for rows.Next() {
var i GetPrebuildMetricsRow
if err := rows.Scan(
&i.TemplateName,
&i.PresetName,
&i.OrganizationName,
&i.CreatedCount,
&i.FailedCount,
&i.ClaimedCount,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getPresetsAtFailureLimit = `-- name: GetPresetsAtFailureLimit :many
WITH filtered_builds AS (
-- Only select builds which are for prebuild creations
SELECT wlb.template_version_id, wlb.created_at, tvp.id AS preset_id, wlb.job_status, tvp.desired_instances
FROM template_version_presets tvp
INNER JOIN workspace_latest_builds wlb ON wlb.template_version_preset_id = tvp.id
INNER JOIN workspaces w ON wlb.workspace_id = w.id
INNER JOIN template_versions tv ON wlb.template_version_id = tv.id
INNER JOIN templates t ON tv.template_id = t.id AND t.active_version_id = tv.id
WHERE tvp.desired_instances IS NOT NULL -- Consider only presets that have a prebuild configuration.
AND wlb.transition = 'start'::workspace_transition
AND w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'
),
time_sorted_builds AS (
-- Group builds by preset, then sort each group by created_at.
SELECT fb.template_version_id, fb.created_at, fb.preset_id, fb.job_status, fb.desired_instances,
ROW_NUMBER() OVER (PARTITION BY fb.preset_id ORDER BY fb.created_at DESC) as rn
FROM filtered_builds fb
)
SELECT
tsb.template_version_id,
tsb.preset_id
FROM time_sorted_builds tsb
WHERE tsb.rn <= $1::bigint
AND tsb.job_status = 'failed'::provisioner_job_status
GROUP BY tsb.template_version_id, tsb.preset_id
HAVING COUNT(*) = $1::bigint
`
type GetPresetsAtFailureLimitRow struct {
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
PresetID uuid.UUID `db:"preset_id" json:"preset_id"`
}
// GetPresetsAtFailureLimit groups workspace builds by preset ID.
// Each preset is associated with exactly one template version ID.
// For each preset, the query checks the last hard_limit builds.
// If all of them failed, the preset is considered to have hit the hard failure limit.
// The query returns a list of preset IDs that have reached this failure threshold.
// Only active template versions with configured presets are considered.
// For each preset, check the last hard_limit builds.
// If all of them failed, the preset is considered to have hit the hard failure limit.
func (q *sqlQuerier) GetPresetsAtFailureLimit(ctx context.Context, hardLimit int64) ([]GetPresetsAtFailureLimitRow, error) {
rows, err := q.db.QueryContext(ctx, getPresetsAtFailureLimit, hardLimit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetPresetsAtFailureLimitRow
for rows.Next() {
var i GetPresetsAtFailureLimitRow
if err := rows.Scan(&i.TemplateVersionID, &i.PresetID); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getPresetsBackoff = `-- name: GetPresetsBackoff :many
WITH filtered_builds AS (
-- Only select builds which are for prebuild creations
SELECT wlb.template_version_id, wlb.created_at, tvp.id AS preset_id, wlb.job_status, tvp.desired_instances
FROM template_version_presets tvp
INNER JOIN workspace_latest_builds wlb ON wlb.template_version_preset_id = tvp.id
INNER JOIN workspaces w ON wlb.workspace_id = w.id
INNER JOIN template_versions tv ON wlb.template_version_id = tv.id
INNER JOIN templates t ON tv.template_id = t.id AND t.active_version_id = tv.id
WHERE tvp.desired_instances IS NOT NULL -- Consider only presets that have a prebuild configuration.
AND wlb.transition = 'start'::workspace_transition
AND w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'
AND NOT t.deleted
),
time_sorted_builds AS (
-- Group builds by preset, then sort each group by created_at.
SELECT fb.template_version_id, fb.created_at, fb.preset_id, fb.job_status, fb.desired_instances,
ROW_NUMBER() OVER (PARTITION BY fb.preset_id ORDER BY fb.created_at DESC) as rn
FROM filtered_builds fb
),
failed_count AS (
-- Count failed builds per preset in the given period
SELECT preset_id, COUNT(*) AS num_failed
FROM filtered_builds
WHERE job_status = 'failed'::provisioner_job_status
AND created_at >= $1::timestamptz
GROUP BY preset_id
)
SELECT
tsb.template_version_id,
tsb.preset_id,
COALESCE(fc.num_failed, 0)::int AS num_failed,
MAX(tsb.created_at)::timestamptz AS last_build_at
FROM time_sorted_builds tsb
LEFT JOIN failed_count fc ON fc.preset_id = tsb.preset_id
WHERE tsb.rn <= tsb.desired_instances -- Fetch the last N builds, where N is the number of desired instances; if any fail, we backoff
AND tsb.job_status = 'failed'::provisioner_job_status
AND created_at >= $1::timestamptz
GROUP BY tsb.template_version_id, tsb.preset_id, fc.num_failed
`
type GetPresetsBackoffRow struct {
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
PresetID uuid.UUID `db:"preset_id" json:"preset_id"`
NumFailed int32 `db:"num_failed" json:"num_failed"`
LastBuildAt time.Time `db:"last_build_at" json:"last_build_at"`
}
// GetPresetsBackoff groups workspace builds by preset ID.
// Each preset is associated with exactly one template version ID.
// For each group, the query checks up to N of the most recent jobs that occurred within the
// lookback period, where N equals the number of desired instances for the corresponding preset.
// If at least one of the job within a group has failed, we should backoff on the corresponding preset ID.
// Query returns a list of preset IDs for which we should backoff.
// Only active template versions with configured presets are considered.
// We also return the number of failed workspace builds that occurred during the lookback period.
//
// NOTE:
// - To **decide whether to back off**, we look at up to the N most recent builds (within the defined lookback period).
// - To **calculate the number of failed builds**, we consider all builds within the defined lookback period.
//
// The number of failed builds is used downstream to determine the backoff duration.
func (q *sqlQuerier) GetPresetsBackoff(ctx context.Context, lookback time.Time) ([]GetPresetsBackoffRow, error) {
rows, err := q.db.QueryContext(ctx, getPresetsBackoff, lookback)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetPresetsBackoffRow
for rows.Next() {
var i GetPresetsBackoffRow
if err := rows.Scan(
&i.TemplateVersionID,
&i.PresetID,
&i.NumFailed,
&i.LastBuildAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getRunningPrebuiltWorkspaces = `-- name: GetRunningPrebuiltWorkspaces :many
WITH latest_prebuilds AS (
-- All workspaces that match the following criteria:
-- 1. Owned by prebuilds user
-- 2. Not deleted
-- 3. Latest build is a 'start' transition
-- 4. Latest build was successful
SELECT
workspaces.id,
workspaces.name,
workspaces.template_id,
workspace_latest_builds.template_version_id,
workspace_latest_builds.job_id,
workspaces.created_at
FROM workspace_latest_builds
JOIN workspaces ON workspaces.id = workspace_latest_builds.workspace_id
WHERE workspace_latest_builds.transition = 'start'::workspace_transition
AND workspace_latest_builds.job_status = 'succeeded'::provisioner_job_status
AND workspaces.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID
AND NOT workspaces.deleted
),
workspace_latest_presets AS (
-- For each of the above workspaces, the preset_id of the most recent
-- successful start transition.
SELECT DISTINCT ON (latest_prebuilds.id)
latest_prebuilds.id AS workspace_id,
workspace_builds.template_version_preset_id AS current_preset_id
FROM latest_prebuilds
JOIN workspace_builds ON workspace_builds.workspace_id = latest_prebuilds.id
WHERE workspace_builds.transition = 'start'::workspace_transition
AND workspace_builds.template_version_preset_id IS NOT NULL
ORDER BY latest_prebuilds.id, workspace_builds.build_number DESC
),
ready_agents AS (
-- For each of the above workspaces, check if all agents are ready.
SELECT
latest_prebuilds.job_id,
BOOL_AND(workspace_agents.lifecycle_state = 'ready'::workspace_agent_lifecycle_state)::boolean AS ready
FROM latest_prebuilds
JOIN workspace_resources ON workspace_resources.job_id = latest_prebuilds.job_id
JOIN workspace_agents ON workspace_agents.resource_id = workspace_resources.id
WHERE workspace_agents.deleted = false
AND workspace_agents.parent_id IS NULL
GROUP BY latest_prebuilds.job_id
)
SELECT
latest_prebuilds.id,
latest_prebuilds.name,
latest_prebuilds.template_id,
latest_prebuilds.template_version_id,
workspace_latest_presets.current_preset_id,
COALESCE(ready_agents.ready, false)::boolean AS ready,
latest_prebuilds.created_at
FROM latest_prebuilds
LEFT JOIN ready_agents ON ready_agents.job_id = latest_prebuilds.job_id
LEFT JOIN workspace_latest_presets ON workspace_latest_presets.workspace_id = latest_prebuilds.id
ORDER BY latest_prebuilds.id
`
type GetRunningPrebuiltWorkspacesRow struct {
ID uuid.UUID `db:"id" json:"id"`
Name string `db:"name" json:"name"`
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
CurrentPresetID uuid.NullUUID `db:"current_preset_id" json:"current_preset_id"`
Ready bool `db:"ready" json:"ready"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
func (q *sqlQuerier) GetRunningPrebuiltWorkspaces(ctx context.Context) ([]GetRunningPrebuiltWorkspacesRow, error) {
rows, err := q.db.QueryContext(ctx, getRunningPrebuiltWorkspaces)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetRunningPrebuiltWorkspacesRow
for rows.Next() {
var i GetRunningPrebuiltWorkspacesRow
if err := rows.Scan(
&i.ID,
&i.Name,
&i.TemplateID,
&i.TemplateVersionID,
&i.CurrentPresetID,
&i.Ready,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getTemplatePresetsWithPrebuilds = `-- name: GetTemplatePresetsWithPrebuilds :many
SELECT
t.id AS template_id,
t.name AS template_name,
o.id AS organization_id,
o.name AS organization_name,
tv.id AS template_version_id,
tv.name AS template_version_name,
tv.id = t.active_version_id AS using_active_version,
tvp.id,
tvp.name,
tvp.desired_instances AS desired_instances,
tvp.scheduling_timezone,
tvp.invalidate_after_secs AS ttl,
tvp.prebuild_status,
tvp.last_invalidated_at,
t.deleted,
t.deprecated != '' AS deprecated
FROM templates t
INNER JOIN template_versions tv ON tv.template_id = t.id
INNER JOIN template_version_presets tvp ON tvp.template_version_id = tv.id
INNER JOIN organizations o ON o.id = t.organization_id
WHERE tvp.desired_instances IS NOT NULL -- Consider only presets that have a prebuild configuration.
-- AND NOT t.deleted -- We don't exclude deleted templates because there's no constraint in the DB preventing a soft deletion on a template while workspaces are running.
AND (t.id = $1::uuid OR $1 IS NULL)
`
type GetTemplatePresetsWithPrebuildsRow struct {
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
TemplateName string `db:"template_name" json:"template_name"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
OrganizationName string `db:"organization_name" json:"organization_name"`
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
TemplateVersionName string `db:"template_version_name" json:"template_version_name"`
UsingActiveVersion bool `db:"using_active_version" json:"using_active_version"`
ID uuid.UUID `db:"id" json:"id"`
Name string `db:"name" json:"name"`
DesiredInstances sql.NullInt32 `db:"desired_instances" json:"desired_instances"`
SchedulingTimezone string `db:"scheduling_timezone" json:"scheduling_timezone"`
Ttl sql.NullInt32 `db:"ttl" json:"ttl"`
PrebuildStatus PrebuildStatus `db:"prebuild_status" json:"prebuild_status"`
LastInvalidatedAt sql.NullTime `db:"last_invalidated_at" json:"last_invalidated_at"`
Deleted bool `db:"deleted" json:"deleted"`
Deprecated bool `db:"deprecated" json:"deprecated"`
}
// GetTemplatePresetsWithPrebuilds retrieves template versions with configured presets and prebuilds.
// It also returns the number of desired instances for each preset.
// If template_id is specified, only template versions associated with that template will be returned.
func (q *sqlQuerier) GetTemplatePresetsWithPrebuilds(ctx context.Context, templateID uuid.NullUUID) ([]GetTemplatePresetsWithPrebuildsRow, error) {
rows, err := q.db.QueryContext(ctx, getTemplatePresetsWithPrebuilds, templateID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetTemplatePresetsWithPrebuildsRow
for rows.Next() {
var i GetTemplatePresetsWithPrebuildsRow
if err := rows.Scan(
&i.TemplateID,
&i.TemplateName,
&i.OrganizationID,
&i.OrganizationName,
&i.TemplateVersionID,
&i.TemplateVersionName,
&i.UsingActiveVersion,
&i.ID,
&i.Name,
&i.DesiredInstances,
&i.SchedulingTimezone,
&i.Ttl,
&i.PrebuildStatus,
&i.LastInvalidatedAt,
&i.Deleted,
&i.Deprecated,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updatePrebuildProvisionerJobWithCancel = `-- name: UpdatePrebuildProvisionerJobWithCancel :many
WITH jobs_to_cancel AS (
SELECT pj.id, w.id AS workspace_id, w.template_id, wpb.template_version_preset_id
FROM provisioner_jobs pj
INNER JOIN workspace_prebuild_builds wpb ON wpb.job_id = pj.id
INNER JOIN workspaces w ON w.id = wpb.workspace_id
INNER JOIN templates t ON t.id = w.template_id
WHERE
wpb.template_version_id != t.active_version_id
AND wpb.template_version_preset_id = $2
-- Only considers initial builds, i.e. created by the reconciliation loop
AND wpb.build_number = 1
-- Only consider 'start' transitions (provisioning), not 'stop'/'delete' (deprovisioning)
-- Deprovisioning jobs should complete naturally as they're already cleaning up resources
AND wpb.transition = 'start'::workspace_transition
-- Pending jobs that have not yet been picked up by a provisioner
AND pj.job_status = 'pending'::provisioner_job_status
AND pj.worker_id IS NULL
AND pj.canceled_at IS NULL
AND pj.completed_at IS NULL
)
UPDATE provisioner_jobs
SET
canceled_at = $1::timestamptz,
completed_at = $1::timestamptz
FROM jobs_to_cancel
WHERE provisioner_jobs.id = jobs_to_cancel.id
RETURNING jobs_to_cancel.id, jobs_to_cancel.workspace_id, jobs_to_cancel.template_id, jobs_to_cancel.template_version_preset_id
`
type UpdatePrebuildProvisionerJobWithCancelParams struct {
Now time.Time `db:"now" json:"now"`
PresetID uuid.NullUUID `db:"preset_id" json:"preset_id"`
}
type UpdatePrebuildProvisionerJobWithCancelRow struct {
ID uuid.UUID `db:"id" json:"id"`
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
TemplateVersionPresetID uuid.NullUUID `db:"template_version_preset_id" json:"template_version_preset_id"`
}
// Cancels all pending provisioner jobs for prebuilt workspaces on a specific preset from an
// inactive template version.
// This is an optimization to clean up stale pending jobs.
func (q *sqlQuerier) UpdatePrebuildProvisionerJobWithCancel(ctx context.Context, arg UpdatePrebuildProvisionerJobWithCancelParams) ([]UpdatePrebuildProvisionerJobWithCancelRow, error) {
rows, err := q.db.QueryContext(ctx, updatePrebuildProvisionerJobWithCancel, arg.Now, arg.PresetID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []UpdatePrebuildProvisionerJobWithCancelRow
for rows.Next() {
var i UpdatePrebuildProvisionerJobWithCancelRow
if err := rows.Scan(
&i.ID,
&i.WorkspaceID,
&i.TemplateID,
&i.TemplateVersionPresetID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getActivePresetPrebuildSchedules = `-- name: GetActivePresetPrebuildSchedules :many
SELECT
tvpps.id, tvpps.preset_id, tvpps.cron_expression, tvpps.desired_instances
FROM
template_version_preset_prebuild_schedules tvpps
INNER JOIN template_version_presets tvp ON tvp.id = tvpps.preset_id
INNER JOIN template_versions tv ON tv.id = tvp.template_version_id
INNER JOIN templates t ON t.id = tv.template_id
WHERE
-- Template version is active, and template is not deleted or deprecated
tv.id = t.active_version_id
AND NOT t.deleted
AND t.deprecated = ''
`
func (q *sqlQuerier) GetActivePresetPrebuildSchedules(ctx context.Context) ([]TemplateVersionPresetPrebuildSchedule, error) {
rows, err := q.db.QueryContext(ctx, getActivePresetPrebuildSchedules)
if err != nil {
return nil, err
}
defer rows.Close()
var items []TemplateVersionPresetPrebuildSchedule
for rows.Next() {
var i TemplateVersionPresetPrebuildSchedule
if err := rows.Scan(
&i.ID,
&i.PresetID,
&i.CronExpression,
&i.DesiredInstances,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getPresetByID = `-- name: GetPresetByID :one
SELECT tvp.id, tvp.template_version_id, tvp.name, tvp.created_at, tvp.desired_instances, tvp.invalidate_after_secs, tvp.prebuild_status, tvp.scheduling_timezone, tvp.is_default, tvp.description, tvp.icon, tvp.last_invalidated_at, tv.template_id, tv.organization_id FROM
template_version_presets tvp
INNER JOIN template_versions tv ON tvp.template_version_id = tv.id
WHERE tvp.id = $1
`
type GetPresetByIDRow struct {
ID uuid.UUID `db:"id" json:"id"`
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
Name string `db:"name" json:"name"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
DesiredInstances sql.NullInt32 `db:"desired_instances" json:"desired_instances"`
InvalidateAfterSecs sql.NullInt32 `db:"invalidate_after_secs" json:"invalidate_after_secs"`
PrebuildStatus PrebuildStatus `db:"prebuild_status" json:"prebuild_status"`
SchedulingTimezone string `db:"scheduling_timezone" json:"scheduling_timezone"`
IsDefault bool `db:"is_default" json:"is_default"`
Description string `db:"description" json:"description"`
Icon string `db:"icon" json:"icon"`
LastInvalidatedAt sql.NullTime `db:"last_invalidated_at" json:"last_invalidated_at"`
TemplateID uuid.NullUUID `db:"template_id" json:"template_id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
}
func (q *sqlQuerier) GetPresetByID(ctx context.Context, presetID uuid.UUID) (GetPresetByIDRow, error) {
row := q.db.QueryRowContext(ctx, getPresetByID, presetID)
var i GetPresetByIDRow
err := row.Scan(
&i.ID,
&i.TemplateVersionID,
&i.Name,
&i.CreatedAt,
&i.DesiredInstances,
&i.InvalidateAfterSecs,
&i.PrebuildStatus,
&i.SchedulingTimezone,
&i.IsDefault,
&i.Description,
&i.Icon,
&i.LastInvalidatedAt,
&i.TemplateID,
&i.OrganizationID,
)
return i, err
}
const getPresetByWorkspaceBuildID = `-- name: GetPresetByWorkspaceBuildID :one
SELECT
template_version_presets.id, template_version_presets.template_version_id, template_version_presets.name, template_version_presets.created_at, template_version_presets.desired_instances, template_version_presets.invalidate_after_secs, template_version_presets.prebuild_status, template_version_presets.scheduling_timezone, template_version_presets.is_default, template_version_presets.description, template_version_presets.icon, template_version_presets.last_invalidated_at
FROM
template_version_presets
INNER JOIN workspace_builds ON workspace_builds.template_version_preset_id = template_version_presets.id
WHERE
workspace_builds.id = $1
`
func (q *sqlQuerier) GetPresetByWorkspaceBuildID(ctx context.Context, workspaceBuildID uuid.UUID) (TemplateVersionPreset, error) {
row := q.db.QueryRowContext(ctx, getPresetByWorkspaceBuildID, workspaceBuildID)
var i TemplateVersionPreset
err := row.Scan(
&i.ID,
&i.TemplateVersionID,
&i.Name,
&i.CreatedAt,
&i.DesiredInstances,
&i.InvalidateAfterSecs,
&i.PrebuildStatus,
&i.SchedulingTimezone,
&i.IsDefault,
&i.Description,
&i.Icon,
&i.LastInvalidatedAt,
)
return i, err
}
const getPresetParametersByPresetID = `-- name: GetPresetParametersByPresetID :many
SELECT
tvpp.id, tvpp.template_version_preset_id, tvpp.name, tvpp.value
FROM
template_version_preset_parameters tvpp
WHERE
tvpp.template_version_preset_id = $1
`
func (q *sqlQuerier) GetPresetParametersByPresetID(ctx context.Context, presetID uuid.UUID) ([]TemplateVersionPresetParameter, error) {
rows, err := q.db.QueryContext(ctx, getPresetParametersByPresetID, presetID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []TemplateVersionPresetParameter
for rows.Next() {
var i TemplateVersionPresetParameter
if err := rows.Scan(
&i.ID,
&i.TemplateVersionPresetID,
&i.Name,
&i.Value,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getPresetParametersByTemplateVersionID = `-- name: GetPresetParametersByTemplateVersionID :many
SELECT
template_version_preset_parameters.id, template_version_preset_parameters.template_version_preset_id, template_version_preset_parameters.name, template_version_preset_parameters.value
FROM
template_version_preset_parameters
INNER JOIN template_version_presets ON template_version_preset_parameters.template_version_preset_id = template_version_presets.id
WHERE
template_version_presets.template_version_id = $1
`
func (q *sqlQuerier) GetPresetParametersByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionPresetParameter, error) {
rows, err := q.db.QueryContext(ctx, getPresetParametersByTemplateVersionID, templateVersionID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []TemplateVersionPresetParameter
for rows.Next() {
var i TemplateVersionPresetParameter
if err := rows.Scan(
&i.ID,
&i.TemplateVersionPresetID,
&i.Name,
&i.Value,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getPresetsByTemplateVersionID = `-- name: GetPresetsByTemplateVersionID :many
SELECT
id, template_version_id, name, created_at, desired_instances, invalidate_after_secs, prebuild_status, scheduling_timezone, is_default, description, icon, last_invalidated_at
FROM
template_version_presets
WHERE
template_version_id = $1
`
func (q *sqlQuerier) GetPresetsByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionPreset, error) {
rows, err := q.db.QueryContext(ctx, getPresetsByTemplateVersionID, templateVersionID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []TemplateVersionPreset
for rows.Next() {
var i TemplateVersionPreset
if err := rows.Scan(
&i.ID,
&i.TemplateVersionID,
&i.Name,
&i.CreatedAt,
&i.DesiredInstances,
&i.InvalidateAfterSecs,
&i.PrebuildStatus,
&i.SchedulingTimezone,
&i.IsDefault,
&i.Description,
&i.Icon,
&i.LastInvalidatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertPreset = `-- name: InsertPreset :one
INSERT INTO template_version_presets (
id,
template_version_id,
name,
created_at,
desired_instances,
invalidate_after_secs,
scheduling_timezone,
is_default,
description,
icon,
last_invalidated_at
)
VALUES (
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
$9,
$10,
$11
) RETURNING id, template_version_id, name, created_at, desired_instances, invalidate_after_secs, prebuild_status, scheduling_timezone, is_default, description, icon, last_invalidated_at
`
type InsertPresetParams struct {
ID uuid.UUID `db:"id" json:"id"`
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
Name string `db:"name" json:"name"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
DesiredInstances sql.NullInt32 `db:"desired_instances" json:"desired_instances"`
InvalidateAfterSecs sql.NullInt32 `db:"invalidate_after_secs" json:"invalidate_after_secs"`
SchedulingTimezone string `db:"scheduling_timezone" json:"scheduling_timezone"`
IsDefault bool `db:"is_default" json:"is_default"`
Description string `db:"description" json:"description"`
Icon string `db:"icon" json:"icon"`
LastInvalidatedAt sql.NullTime `db:"last_invalidated_at" json:"last_invalidated_at"`
}
func (q *sqlQuerier) InsertPreset(ctx context.Context, arg InsertPresetParams) (TemplateVersionPreset, error) {
row := q.db.QueryRowContext(ctx, insertPreset,
arg.ID,
arg.TemplateVersionID,
arg.Name,
arg.CreatedAt,
arg.DesiredInstances,
arg.InvalidateAfterSecs,
arg.SchedulingTimezone,
arg.IsDefault,
arg.Description,
arg.Icon,
arg.LastInvalidatedAt,
)
var i TemplateVersionPreset
err := row.Scan(
&i.ID,
&i.TemplateVersionID,
&i.Name,
&i.CreatedAt,
&i.DesiredInstances,
&i.InvalidateAfterSecs,
&i.PrebuildStatus,
&i.SchedulingTimezone,
&i.IsDefault,
&i.Description,
&i.Icon,
&i.LastInvalidatedAt,
)
return i, err
}
const insertPresetParameters = `-- name: InsertPresetParameters :many
INSERT INTO
template_version_preset_parameters (template_version_preset_id, name, value)
SELECT
$1,
unnest($2 :: TEXT[]),
unnest($3 :: TEXT[])
RETURNING id, template_version_preset_id, name, value
`
type InsertPresetParametersParams struct {
TemplateVersionPresetID uuid.UUID `db:"template_version_preset_id" json:"template_version_preset_id"`
Names []string `db:"names" json:"names"`
Values []string `db:"values" json:"values"`
}
func (q *sqlQuerier) InsertPresetParameters(ctx context.Context, arg InsertPresetParametersParams) ([]TemplateVersionPresetParameter, error) {
rows, err := q.db.QueryContext(ctx, insertPresetParameters, arg.TemplateVersionPresetID, pq.Array(arg.Names), pq.Array(arg.Values))
if err != nil {
return nil, err
}
defer rows.Close()
var items []TemplateVersionPresetParameter
for rows.Next() {
var i TemplateVersionPresetParameter
if err := rows.Scan(
&i.ID,
&i.TemplateVersionPresetID,
&i.Name,
&i.Value,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertPresetPrebuildSchedule = `-- name: InsertPresetPrebuildSchedule :one
INSERT INTO template_version_preset_prebuild_schedules (
preset_id,
cron_expression,
desired_instances
)
VALUES (
$1,
$2,
$3
) RETURNING id, preset_id, cron_expression, desired_instances
`
type InsertPresetPrebuildScheduleParams struct {
PresetID uuid.UUID `db:"preset_id" json:"preset_id"`
CronExpression string `db:"cron_expression" json:"cron_expression"`
DesiredInstances int32 `db:"desired_instances" json:"desired_instances"`
}
func (q *sqlQuerier) InsertPresetPrebuildSchedule(ctx context.Context, arg InsertPresetPrebuildScheduleParams) (TemplateVersionPresetPrebuildSchedule, error) {
row := q.db.QueryRowContext(ctx, insertPresetPrebuildSchedule, arg.PresetID, arg.CronExpression, arg.DesiredInstances)
var i TemplateVersionPresetPrebuildSchedule
err := row.Scan(
&i.ID,
&i.PresetID,
&i.CronExpression,
&i.DesiredInstances,
)
return i, err
}
const updatePresetPrebuildStatus = `-- name: UpdatePresetPrebuildStatus :exec
UPDATE template_version_presets
SET prebuild_status = $1
WHERE id = $2
`
type UpdatePresetPrebuildStatusParams struct {
Status PrebuildStatus `db:"status" json:"status"`
PresetID uuid.UUID `db:"preset_id" json:"preset_id"`
}
func (q *sqlQuerier) UpdatePresetPrebuildStatus(ctx context.Context, arg UpdatePresetPrebuildStatusParams) error {
_, err := q.db.ExecContext(ctx, updatePresetPrebuildStatus, arg.Status, arg.PresetID)
return err
}
const updatePresetsLastInvalidatedAt = `-- name: UpdatePresetsLastInvalidatedAt :many
UPDATE
template_version_presets tvp
SET
last_invalidated_at = $1
FROM
templates t
JOIN template_versions tv ON tv.id = t.active_version_id
WHERE
t.id = $2
AND tvp.template_version_id = tv.id
RETURNING
t.name AS template_name,
tv.name AS template_version_name,
tvp.name AS template_version_preset_name
`
type UpdatePresetsLastInvalidatedAtParams struct {
LastInvalidatedAt sql.NullTime `db:"last_invalidated_at" json:"last_invalidated_at"`
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
}
type UpdatePresetsLastInvalidatedAtRow struct {
TemplateName string `db:"template_name" json:"template_name"`
TemplateVersionName string `db:"template_version_name" json:"template_version_name"`
TemplateVersionPresetName string `db:"template_version_preset_name" json:"template_version_preset_name"`
}
func (q *sqlQuerier) UpdatePresetsLastInvalidatedAt(ctx context.Context, arg UpdatePresetsLastInvalidatedAtParams) ([]UpdatePresetsLastInvalidatedAtRow, error) {
rows, err := q.db.QueryContext(ctx, updatePresetsLastInvalidatedAt, arg.LastInvalidatedAt, arg.TemplateID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []UpdatePresetsLastInvalidatedAtRow
for rows.Next() {
var i UpdatePresetsLastInvalidatedAtRow
if err := rows.Scan(&i.TemplateName, &i.TemplateVersionName, &i.TemplateVersionPresetName); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const deleteOldProvisionerDaemons = `-- name: DeleteOldProvisionerDaemons :exec
DELETE FROM provisioner_daemons WHERE (
(created_at < (NOW() - INTERVAL '7 days') AND last_seen_at IS NULL) OR
(last_seen_at IS NOT NULL AND last_seen_at < (NOW() - INTERVAL '7 days'))
)
`
// Delete provisioner daemons that have been created at least a week ago
// and have not connected to coderd since a week.
// A provisioner daemon with "zeroed" last_seen_at column indicates possible
// connectivity issues (no provisioner daemon activity since registration).
func (q *sqlQuerier) DeleteOldProvisionerDaemons(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, deleteOldProvisionerDaemons)
return err
}
const getEligibleProvisionerDaemonsByProvisionerJobIDs = `-- name: GetEligibleProvisionerDaemonsByProvisionerJobIDs :many
SELECT DISTINCT
provisioner_jobs.id as job_id, provisioner_daemons.id, provisioner_daemons.created_at, provisioner_daemons.name, provisioner_daemons.provisioners, provisioner_daemons.replica_id, provisioner_daemons.tags, provisioner_daemons.last_seen_at, provisioner_daemons.version, provisioner_daemons.api_version, provisioner_daemons.organization_id, provisioner_daemons.key_id
FROM
provisioner_jobs
JOIN
provisioner_daemons ON provisioner_daemons.organization_id = provisioner_jobs.organization_id
AND provisioner_tagset_contains(provisioner_daemons.tags::tagset, provisioner_jobs.tags::tagset)
AND provisioner_jobs.provisioner = ANY(provisioner_daemons.provisioners)
WHERE
provisioner_jobs.id = ANY($1 :: uuid[])
`
type GetEligibleProvisionerDaemonsByProvisionerJobIDsRow struct {
JobID uuid.UUID `db:"job_id" json:"job_id"`
ProvisionerDaemon ProvisionerDaemon `db:"provisioner_daemon" json:"provisioner_daemon"`
}
func (q *sqlQuerier) GetEligibleProvisionerDaemonsByProvisionerJobIDs(ctx context.Context, provisionerJobIds []uuid.UUID) ([]GetEligibleProvisionerDaemonsByProvisionerJobIDsRow, error) {
rows, err := q.db.QueryContext(ctx, getEligibleProvisionerDaemonsByProvisionerJobIDs, pq.Array(provisionerJobIds))
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetEligibleProvisionerDaemonsByProvisionerJobIDsRow
for rows.Next() {
var i GetEligibleProvisionerDaemonsByProvisionerJobIDsRow
if err := rows.Scan(
&i.JobID,
&i.ProvisionerDaemon.ID,
&i.ProvisionerDaemon.CreatedAt,
&i.ProvisionerDaemon.Name,
pq.Array(&i.ProvisionerDaemon.Provisioners),
&i.ProvisionerDaemon.ReplicaID,
&i.ProvisionerDaemon.Tags,
&i.ProvisionerDaemon.LastSeenAt,
&i.ProvisionerDaemon.Version,
&i.ProvisionerDaemon.APIVersion,
&i.ProvisionerDaemon.OrganizationID,
&i.ProvisionerDaemon.KeyID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getProvisionerDaemons = `-- name: GetProvisionerDaemons :many
SELECT
id, created_at, name, provisioners, replica_id, tags, last_seen_at, version, api_version, organization_id, key_id
FROM
provisioner_daemons
`
func (q *sqlQuerier) GetProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error) {
rows, err := q.db.QueryContext(ctx, getProvisionerDaemons)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ProvisionerDaemon
for rows.Next() {
var i ProvisionerDaemon
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.Name,
pq.Array(&i.Provisioners),
&i.ReplicaID,
&i.Tags,
&i.LastSeenAt,
&i.Version,
&i.APIVersion,
&i.OrganizationID,
&i.KeyID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getProvisionerDaemonsByOrganization = `-- name: GetProvisionerDaemonsByOrganization :many
SELECT
id, created_at, name, provisioners, replica_id, tags, last_seen_at, version, api_version, organization_id, key_id
FROM
provisioner_daemons
WHERE
-- This is the original search criteria:
organization_id = $1 :: uuid
AND
-- adding support for searching by tags:
($2 :: tagset = 'null' :: tagset OR provisioner_tagset_contains(provisioner_daemons.tags::tagset, $2::tagset))
`
type GetProvisionerDaemonsByOrganizationParams struct {
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
WantTags StringMap `db:"want_tags" json:"want_tags"`
}
func (q *sqlQuerier) GetProvisionerDaemonsByOrganization(ctx context.Context, arg GetProvisionerDaemonsByOrganizationParams) ([]ProvisionerDaemon, error) {
rows, err := q.db.QueryContext(ctx, getProvisionerDaemonsByOrganization, arg.OrganizationID, arg.WantTags)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ProvisionerDaemon
for rows.Next() {
var i ProvisionerDaemon
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.Name,
pq.Array(&i.Provisioners),
&i.ReplicaID,
&i.Tags,
&i.LastSeenAt,
&i.Version,
&i.APIVersion,
&i.OrganizationID,
&i.KeyID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getProvisionerDaemonsWithStatusByOrganization = `-- name: GetProvisionerDaemonsWithStatusByOrganization :many
SELECT
pd.id, pd.created_at, pd.name, pd.provisioners, pd.replica_id, pd.tags, pd.last_seen_at, pd.version, pd.api_version, pd.organization_id, pd.key_id,
CASE
WHEN current_job.id IS NOT NULL THEN 'busy'::provisioner_daemon_status
WHEN (COALESCE($1::bool, false) = true
OR 'offline'::provisioner_daemon_status = ANY($2::provisioner_daemon_status[]))
AND (pd.last_seen_at IS NULL OR pd.last_seen_at < (NOW() - ($3::bigint || ' ms')::interval))
THEN 'offline'::provisioner_daemon_status
ELSE 'idle'::provisioner_daemon_status
END AS status,
pk.name AS key_name,
-- NOTE(mafredri): sqlc.embed doesn't support nullable tables nor renaming them.
current_job.id AS current_job_id,
current_job.job_status AS current_job_status,
previous_job.id AS previous_job_id,
previous_job.job_status AS previous_job_status,
COALESCE(current_template.name, ''::text) AS current_job_template_name,
COALESCE(current_template.display_name, ''::text) AS current_job_template_display_name,
COALESCE(current_template.icon, ''::text) AS current_job_template_icon,
COALESCE(previous_template.name, ''::text) AS previous_job_template_name,
COALESCE(previous_template.display_name, ''::text) AS previous_job_template_display_name,
COALESCE(previous_template.icon, ''::text) AS previous_job_template_icon
FROM
provisioner_daemons pd
JOIN
provisioner_keys pk ON pk.id = pd.key_id
LEFT JOIN
provisioner_jobs current_job ON (
current_job.worker_id = pd.id
AND current_job.organization_id = pd.organization_id
AND current_job.completed_at IS NULL
)
LEFT JOIN
provisioner_jobs previous_job ON (
previous_job.id = (
SELECT
id
FROM
provisioner_jobs
WHERE
worker_id = pd.id
AND organization_id = pd.organization_id
AND completed_at IS NOT NULL
ORDER BY
completed_at DESC
LIMIT 1
)
AND previous_job.organization_id = pd.organization_id
)
LEFT JOIN
workspace_builds current_build ON current_build.id = CASE WHEN current_job.input ? 'workspace_build_id' THEN (current_job.input->>'workspace_build_id')::uuid END
LEFT JOIN
-- We should always have a template version, either explicitly or implicitly via workspace build.
template_versions current_version ON (
current_version.id = CASE WHEN current_job.input ? 'template_version_id' THEN (current_job.input->>'template_version_id')::uuid ELSE current_build.template_version_id END
AND current_version.organization_id = pd.organization_id
)
LEFT JOIN
templates current_template ON (
current_template.id = current_version.template_id
AND current_template.organization_id = pd.organization_id
)
LEFT JOIN
workspace_builds previous_build ON previous_build.id = CASE WHEN previous_job.input ? 'workspace_build_id' THEN (previous_job.input->>'workspace_build_id')::uuid END
LEFT JOIN
-- We should always have a template version, either explicitly or implicitly via workspace build.
template_versions previous_version ON (
previous_version.id = CASE WHEN previous_job.input ? 'template_version_id' THEN (previous_job.input->>'template_version_id')::uuid ELSE previous_build.template_version_id END
AND previous_version.organization_id = pd.organization_id
)
LEFT JOIN
templates previous_template ON (
previous_template.id = previous_version.template_id
AND previous_template.organization_id = pd.organization_id
)
WHERE
pd.organization_id = $4::uuid
AND (COALESCE(array_length($5::uuid[], 1), 0) = 0 OR pd.id = ANY($5::uuid[]))
AND ($6::tagset = 'null'::tagset OR provisioner_tagset_contains(pd.tags::tagset, $6::tagset))
-- Filter by max age if provided
AND (
$7::bigint IS NULL
OR pd.last_seen_at IS NULL
OR pd.last_seen_at >= (NOW() - ($7::bigint || ' ms')::interval)
)
AND (
-- Always include online daemons
(pd.last_seen_at IS NOT NULL AND pd.last_seen_at >= (NOW() - ($3::bigint || ' ms')::interval))
-- Include offline daemons if offline param is true or 'offline' status is requested
OR (
(pd.last_seen_at IS NULL OR pd.last_seen_at < (NOW() - ($3::bigint || ' ms')::interval))
AND (
COALESCE($1::bool, false) = true
OR 'offline'::provisioner_daemon_status = ANY($2::provisioner_daemon_status[])
)
)
)
AND (
-- Filter daemons by any statuses if provided
COALESCE(array_length($2::provisioner_daemon_status[], 1), 0) = 0
OR (current_job.id IS NOT NULL AND 'busy'::provisioner_daemon_status = ANY($2::provisioner_daemon_status[]))
OR (current_job.id IS NULL AND 'idle'::provisioner_daemon_status = ANY($2::provisioner_daemon_status[]))
OR (
'offline'::provisioner_daemon_status = ANY($2::provisioner_daemon_status[])
AND (pd.last_seen_at IS NULL OR pd.last_seen_at < (NOW() - ($3::bigint || ' ms')::interval))
)
OR (
COALESCE($1::bool, false) = true
AND (pd.last_seen_at IS NULL OR pd.last_seen_at < (NOW() - ($3::bigint || ' ms')::interval))
)
)
ORDER BY
pd.created_at DESC
LIMIT
$8::int
`
type GetProvisionerDaemonsWithStatusByOrganizationParams struct {
Offline sql.NullBool `db:"offline" json:"offline"`
Statuses []ProvisionerDaemonStatus `db:"statuses" json:"statuses"`
StaleIntervalMS int64 `db:"stale_interval_ms" json:"stale_interval_ms"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
IDs []uuid.UUID `db:"ids" json:"ids"`
Tags StringMap `db:"tags" json:"tags"`
MaxAgeMs sql.NullInt64 `db:"max_age_ms" json:"max_age_ms"`
Limit sql.NullInt32 `db:"limit" json:"limit"`
}
type GetProvisionerDaemonsWithStatusByOrganizationRow struct {
ProvisionerDaemon ProvisionerDaemon `db:"provisioner_daemon" json:"provisioner_daemon"`
Status ProvisionerDaemonStatus `db:"status" json:"status"`
KeyName string `db:"key_name" json:"key_name"`
CurrentJobID uuid.NullUUID `db:"current_job_id" json:"current_job_id"`
CurrentJobStatus NullProvisionerJobStatus `db:"current_job_status" json:"current_job_status"`
PreviousJobID uuid.NullUUID `db:"previous_job_id" json:"previous_job_id"`
PreviousJobStatus NullProvisionerJobStatus `db:"previous_job_status" json:"previous_job_status"`
CurrentJobTemplateName string `db:"current_job_template_name" json:"current_job_template_name"`
CurrentJobTemplateDisplayName string `db:"current_job_template_display_name" json:"current_job_template_display_name"`
CurrentJobTemplateIcon string `db:"current_job_template_icon" json:"current_job_template_icon"`
PreviousJobTemplateName string `db:"previous_job_template_name" json:"previous_job_template_name"`
PreviousJobTemplateDisplayName string `db:"previous_job_template_display_name" json:"previous_job_template_display_name"`
PreviousJobTemplateIcon string `db:"previous_job_template_icon" json:"previous_job_template_icon"`
}
// Current job information.
// Previous job information.
func (q *sqlQuerier) GetProvisionerDaemonsWithStatusByOrganization(ctx context.Context, arg GetProvisionerDaemonsWithStatusByOrganizationParams) ([]GetProvisionerDaemonsWithStatusByOrganizationRow, error) {
rows, err := q.db.QueryContext(ctx, getProvisionerDaemonsWithStatusByOrganization,
arg.Offline,
pq.Array(arg.Statuses),
arg.StaleIntervalMS,
arg.OrganizationID,
pq.Array(arg.IDs),
arg.Tags,
arg.MaxAgeMs,
arg.Limit,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetProvisionerDaemonsWithStatusByOrganizationRow
for rows.Next() {
var i GetProvisionerDaemonsWithStatusByOrganizationRow
if err := rows.Scan(
&i.ProvisionerDaemon.ID,
&i.ProvisionerDaemon.CreatedAt,
&i.ProvisionerDaemon.Name,
pq.Array(&i.ProvisionerDaemon.Provisioners),
&i.ProvisionerDaemon.ReplicaID,
&i.ProvisionerDaemon.Tags,
&i.ProvisionerDaemon.LastSeenAt,
&i.ProvisionerDaemon.Version,
&i.ProvisionerDaemon.APIVersion,
&i.ProvisionerDaemon.OrganizationID,
&i.ProvisionerDaemon.KeyID,
&i.Status,
&i.KeyName,
&i.CurrentJobID,
&i.CurrentJobStatus,
&i.PreviousJobID,
&i.PreviousJobStatus,
&i.CurrentJobTemplateName,
&i.CurrentJobTemplateDisplayName,
&i.CurrentJobTemplateIcon,
&i.PreviousJobTemplateName,
&i.PreviousJobTemplateDisplayName,
&i.PreviousJobTemplateIcon,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateProvisionerDaemonLastSeenAt = `-- name: UpdateProvisionerDaemonLastSeenAt :exec
UPDATE provisioner_daemons
SET
last_seen_at = $1
WHERE
id = $2
AND
last_seen_at <= $1
`
type UpdateProvisionerDaemonLastSeenAtParams struct {
LastSeenAt sql.NullTime `db:"last_seen_at" json:"last_seen_at"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg UpdateProvisionerDaemonLastSeenAtParams) error {
_, err := q.db.ExecContext(ctx, updateProvisionerDaemonLastSeenAt, arg.LastSeenAt, arg.ID)
return err
}
const upsertProvisionerDaemon = `-- name: UpsertProvisionerDaemon :one
INSERT INTO
provisioner_daemons (
id,
created_at,
"name",
provisioners,
tags,
last_seen_at,
"version",
organization_id,
api_version,
key_id
)
VALUES (
gen_random_uuid(),
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
$9
) ON CONFLICT("organization_id", "name", LOWER(COALESCE(tags ->> 'owner'::text, ''::text))) DO UPDATE SET
provisioners = $3,
tags = $4,
last_seen_at = $5,
"version" = $6,
api_version = $8,
organization_id = $7,
key_id = $9
RETURNING id, created_at, name, provisioners, replica_id, tags, last_seen_at, version, api_version, organization_id, key_id
`
type UpsertProvisionerDaemonParams struct {
CreatedAt time.Time `db:"created_at" json:"created_at"`
Name string `db:"name" json:"name"`
Provisioners []ProvisionerType `db:"provisioners" json:"provisioners"`
Tags StringMap `db:"tags" json:"tags"`
LastSeenAt sql.NullTime `db:"last_seen_at" json:"last_seen_at"`
Version string `db:"version" json:"version"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
APIVersion string `db:"api_version" json:"api_version"`
KeyID uuid.UUID `db:"key_id" json:"key_id"`
}
func (q *sqlQuerier) UpsertProvisionerDaemon(ctx context.Context, arg UpsertProvisionerDaemonParams) (ProvisionerDaemon, error) {
row := q.db.QueryRowContext(ctx, upsertProvisionerDaemon,
arg.CreatedAt,
arg.Name,
pq.Array(arg.Provisioners),
arg.Tags,
arg.LastSeenAt,
arg.Version,
arg.OrganizationID,
arg.APIVersion,
arg.KeyID,
)
var i ProvisionerDaemon
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.Name,
pq.Array(&i.Provisioners),
&i.ReplicaID,
&i.Tags,
&i.LastSeenAt,
&i.Version,
&i.APIVersion,
&i.OrganizationID,
&i.KeyID,
)
return i, err
}
const getProvisionerLogsAfterID = `-- name: GetProvisionerLogsAfterID :many
SELECT
job_id, created_at, source, level, stage, output, id
FROM
provisioner_job_logs
WHERE
job_id = $1
AND (
id > $2
) ORDER BY id ASC
`
type GetProvisionerLogsAfterIDParams struct {
JobID uuid.UUID `db:"job_id" json:"job_id"`
CreatedAfter int64 `db:"created_after" json:"created_after"`
}
func (q *sqlQuerier) GetProvisionerLogsAfterID(ctx context.Context, arg GetProvisionerLogsAfterIDParams) ([]ProvisionerJobLog, error) {
rows, err := q.db.QueryContext(ctx, getProvisionerLogsAfterID, arg.JobID, arg.CreatedAfter)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ProvisionerJobLog
for rows.Next() {
var i ProvisionerJobLog
if err := rows.Scan(
&i.JobID,
&i.CreatedAt,
&i.Source,
&i.Level,
&i.Stage,
&i.Output,
&i.ID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertProvisionerJobLogs = `-- name: InsertProvisionerJobLogs :many
INSERT INTO
provisioner_job_logs
SELECT
$1 :: uuid AS job_id,
unnest($2 :: timestamptz [ ]) AS created_at,
unnest($3 :: log_source [ ]) AS source,
unnest($4 :: log_level [ ]) AS LEVEL,
unnest($5 :: VARCHAR(128) [ ]) AS stage,
unnest($6 :: VARCHAR(1024) [ ]) AS output RETURNING job_id, created_at, source, level, stage, output, id
`
type InsertProvisionerJobLogsParams struct {
JobID uuid.UUID `db:"job_id" json:"job_id"`
CreatedAt []time.Time `db:"created_at" json:"created_at"`
Source []LogSource `db:"source" json:"source"`
Level []LogLevel `db:"level" json:"level"`
Stage []string `db:"stage" json:"stage"`
Output []string `db:"output" json:"output"`
}
func (q *sqlQuerier) InsertProvisionerJobLogs(ctx context.Context, arg InsertProvisionerJobLogsParams) ([]ProvisionerJobLog, error) {
rows, err := q.db.QueryContext(ctx, insertProvisionerJobLogs,
arg.JobID,
pq.Array(arg.CreatedAt),
pq.Array(arg.Source),
pq.Array(arg.Level),
pq.Array(arg.Stage),
pq.Array(arg.Output),
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ProvisionerJobLog
for rows.Next() {
var i ProvisionerJobLog
if err := rows.Scan(
&i.JobID,
&i.CreatedAt,
&i.Source,
&i.Level,
&i.Stage,
&i.Output,
&i.ID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateProvisionerJobLogsLength = `-- name: UpdateProvisionerJobLogsLength :exec
UPDATE
provisioner_jobs
SET
logs_length = logs_length + $2
WHERE
id = $1
`
type UpdateProvisionerJobLogsLengthParams struct {
ID uuid.UUID `db:"id" json:"id"`
LogsLength int32 `db:"logs_length" json:"logs_length"`
}
func (q *sqlQuerier) UpdateProvisionerJobLogsLength(ctx context.Context, arg UpdateProvisionerJobLogsLengthParams) error {
_, err := q.db.ExecContext(ctx, updateProvisionerJobLogsLength, arg.ID, arg.LogsLength)
return err
}
const updateProvisionerJobLogsOverflowed = `-- name: UpdateProvisionerJobLogsOverflowed :exec
UPDATE
provisioner_jobs
SET
logs_overflowed = $2
WHERE
id = $1
`
type UpdateProvisionerJobLogsOverflowedParams struct {
ID uuid.UUID `db:"id" json:"id"`
LogsOverflowed bool `db:"logs_overflowed" json:"logs_overflowed"`
}
func (q *sqlQuerier) UpdateProvisionerJobLogsOverflowed(ctx context.Context, arg UpdateProvisionerJobLogsOverflowedParams) error {
_, err := q.db.ExecContext(ctx, updateProvisionerJobLogsOverflowed, arg.ID, arg.LogsOverflowed)
return err
}
const acquireProvisionerJob = `-- name: AcquireProvisionerJob :one
UPDATE
provisioner_jobs
SET
started_at = $1,
updated_at = $1,
worker_id = $2
WHERE
id = (
SELECT
id
FROM
provisioner_jobs AS potential_job
WHERE
potential_job.started_at IS NULL
AND potential_job.completed_at IS NULL
AND potential_job.organization_id = $3
-- Ensure the caller has the correct provisioner.
AND potential_job.provisioner = ANY($4 :: provisioner_type [ ])
-- elsewhere, we use the tagset type, but here we use jsonb for backward compatibility
-- they are aliases and the code that calls this query already relies on a different type
AND provisioner_tagset_contains($5 :: jsonb, potential_job.tags :: jsonb)
ORDER BY
-- Ensure that human-initiated jobs are prioritized over prebuilds.
potential_job.initiator_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid ASC,
potential_job.created_at ASC
FOR UPDATE
SKIP LOCKED
LIMIT
1
) RETURNING id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status, logs_length, logs_overflowed
`
type AcquireProvisionerJobParams struct {
StartedAt sql.NullTime `db:"started_at" json:"started_at"`
WorkerID uuid.NullUUID `db:"worker_id" json:"worker_id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
Types []ProvisionerType `db:"types" json:"types"`
ProvisionerTags json.RawMessage `db:"provisioner_tags" json:"provisioner_tags"`
}
// Acquires the lock for a single job that isn't started, completed,
// canceled, and that matches an array of provisioner types.
//
// SKIP LOCKED is used to jump over locked rows. This prevents
// multiple provisioners from acquiring the same jobs. See:
// https://www.postgresql.org/docs/9.5/sql-select.html#SQL-FOR-UPDATE-SHARE
func (q *sqlQuerier) AcquireProvisionerJob(ctx context.Context, arg AcquireProvisionerJobParams) (ProvisionerJob, error) {
row := q.db.QueryRowContext(ctx, acquireProvisionerJob,
arg.StartedAt,
arg.WorkerID,
arg.OrganizationID,
pq.Array(arg.Types),
arg.ProvisionerTags,
)
var i ProvisionerJob
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.StartedAt,
&i.CanceledAt,
&i.CompletedAt,
&i.Error,
&i.OrganizationID,
&i.InitiatorID,
&i.Provisioner,
&i.StorageMethod,
&i.Type,
&i.Input,
&i.WorkerID,
&i.FileID,
&i.Tags,
&i.ErrorCode,
&i.TraceMetadata,
&i.JobStatus,
&i.LogsLength,
&i.LogsOverflowed,
)
return i, err
}
const getProvisionerJobByID = `-- name: GetProvisionerJobByID :one
SELECT
id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status, logs_length, logs_overflowed
FROM
provisioner_jobs
WHERE
id = $1
`
func (q *sqlQuerier) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (ProvisionerJob, error) {
row := q.db.QueryRowContext(ctx, getProvisionerJobByID, id)
var i ProvisionerJob
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.StartedAt,
&i.CanceledAt,
&i.CompletedAt,
&i.Error,
&i.OrganizationID,
&i.InitiatorID,
&i.Provisioner,
&i.StorageMethod,
&i.Type,
&i.Input,
&i.WorkerID,
&i.FileID,
&i.Tags,
&i.ErrorCode,
&i.TraceMetadata,
&i.JobStatus,
&i.LogsLength,
&i.LogsOverflowed,
)
return i, err
}
const getProvisionerJobByIDForUpdate = `-- name: GetProvisionerJobByIDForUpdate :one
SELECT
id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status, logs_length, logs_overflowed
FROM
provisioner_jobs
WHERE
id = $1
FOR UPDATE
SKIP LOCKED
`
// Gets a single provisioner job by ID for update.
// This is used to securely reap jobs that have been hung/pending for a long time.
func (q *sqlQuerier) GetProvisionerJobByIDForUpdate(ctx context.Context, id uuid.UUID) (ProvisionerJob, error) {
row := q.db.QueryRowContext(ctx, getProvisionerJobByIDForUpdate, id)
var i ProvisionerJob
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.StartedAt,
&i.CanceledAt,
&i.CompletedAt,
&i.Error,
&i.OrganizationID,
&i.InitiatorID,
&i.Provisioner,
&i.StorageMethod,
&i.Type,
&i.Input,
&i.WorkerID,
&i.FileID,
&i.Tags,
&i.ErrorCode,
&i.TraceMetadata,
&i.JobStatus,
&i.LogsLength,
&i.LogsOverflowed,
)
return i, err
}
const getProvisionerJobByIDWithLock = `-- name: GetProvisionerJobByIDWithLock :one
SELECT
id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status, logs_length, logs_overflowed
FROM
provisioner_jobs
WHERE
id = $1
FOR UPDATE
`
// Gets a provisioner job by ID with exclusive lock.
// Blocks until the row is available for update.
func (q *sqlQuerier) GetProvisionerJobByIDWithLock(ctx context.Context, id uuid.UUID) (ProvisionerJob, error) {
row := q.db.QueryRowContext(ctx, getProvisionerJobByIDWithLock, id)
var i ProvisionerJob
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.StartedAt,
&i.CanceledAt,
&i.CompletedAt,
&i.Error,
&i.OrganizationID,
&i.InitiatorID,
&i.Provisioner,
&i.StorageMethod,
&i.Type,
&i.Input,
&i.WorkerID,
&i.FileID,
&i.Tags,
&i.ErrorCode,
&i.TraceMetadata,
&i.JobStatus,
&i.LogsLength,
&i.LogsOverflowed,
)
return i, err
}
const getProvisionerJobTimingsByJobID = `-- name: GetProvisionerJobTimingsByJobID :many
SELECT job_id, started_at, ended_at, stage, source, action, resource FROM provisioner_job_timings
WHERE job_id = $1
ORDER BY started_at ASC
`
func (q *sqlQuerier) GetProvisionerJobTimingsByJobID(ctx context.Context, jobID uuid.UUID) ([]ProvisionerJobTiming, error) {
rows, err := q.db.QueryContext(ctx, getProvisionerJobTimingsByJobID, jobID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ProvisionerJobTiming
for rows.Next() {
var i ProvisionerJobTiming
if err := rows.Scan(
&i.JobID,
&i.StartedAt,
&i.EndedAt,
&i.Stage,
&i.Source,
&i.Action,
&i.Resource,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getProvisionerJobsByIDsWithQueuePosition = `-- name: GetProvisionerJobsByIDsWithQueuePosition :many
WITH filtered_provisioner_jobs AS (
-- Step 1: Filter provisioner_jobs
SELECT
id, created_at, tags
FROM
provisioner_jobs
WHERE
id = ANY($1 :: uuid [ ]) -- Apply filter early to reduce dataset size before expensive JOIN
),
pending_jobs AS (
-- Step 2: Extract only pending jobs
SELECT
id, initiator_id, created_at, tags
FROM
provisioner_jobs
WHERE
job_status = 'pending'
),
unique_daemon_tags AS (
SELECT DISTINCT tags FROM provisioner_daemons pd
WHERE pd.last_seen_at IS NOT NULL
AND pd.last_seen_at >= (NOW() - ($2::bigint || ' ms')::interval)
),
relevant_daemon_tags AS (
SELECT udt.tags
FROM unique_daemon_tags udt
WHERE EXISTS (
SELECT 1 FROM filtered_provisioner_jobs fpj
WHERE provisioner_tagset_contains(udt.tags, fpj.tags)
)
),
ranked_jobs AS (
-- Step 3: Rank only pending jobs based on provisioner availability
SELECT
pj.id,
pj.created_at,
ROW_NUMBER() OVER (PARTITION BY rdt.tags ORDER BY pj.initiator_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid ASC, pj.created_at ASC) AS queue_position,
COUNT(*) OVER (PARTITION BY rdt.tags) AS queue_size
FROM
pending_jobs pj
INNER JOIN
relevant_daemon_tags rdt
ON
provisioner_tagset_contains(rdt.tags, pj.tags)
),
final_jobs AS (
-- Step 4: Compute best queue position and max queue size per job
SELECT
fpj.id,
fpj.created_at,
COALESCE(MIN(rj.queue_position), 0) :: BIGINT AS queue_position, -- Best queue position across provisioners
COALESCE(MAX(rj.queue_size), 0) :: BIGINT AS queue_size -- Max queue size across provisioners
FROM
filtered_provisioner_jobs fpj -- Use the pre-filtered dataset instead of full provisioner_jobs
LEFT JOIN ranked_jobs rj
ON fpj.id = rj.id -- Join with the ranking jobs CTE to assign a rank to each specified provisioner job.
GROUP BY
fpj.id, fpj.created_at
)
SELECT
-- Step 5: Final SELECT with INNER JOIN provisioner_jobs
fj.id,
fj.created_at,
pj.id, pj.created_at, pj.updated_at, pj.started_at, pj.canceled_at, pj.completed_at, pj.error, pj.organization_id, pj.initiator_id, pj.provisioner, pj.storage_method, pj.type, pj.input, pj.worker_id, pj.file_id, pj.tags, pj.error_code, pj.trace_metadata, pj.job_status, pj.logs_length, pj.logs_overflowed,
fj.queue_position,
fj.queue_size
FROM
final_jobs fj
INNER JOIN provisioner_jobs pj
ON fj.id = pj.id -- Ensure we retrieve full details from ` + "`" + `provisioner_jobs` + "`" + `.
-- JOIN with pj is required for sqlc.embed(pj) to compile successfully.
ORDER BY
fj.created_at
`
type GetProvisionerJobsByIDsWithQueuePositionParams struct {
IDs []uuid.UUID `db:"ids" json:"ids"`
StaleIntervalMS int64 `db:"stale_interval_ms" json:"stale_interval_ms"`
}
type GetProvisionerJobsByIDsWithQueuePositionRow struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
ProvisionerJob ProvisionerJob `db:"provisioner_job" json:"provisioner_job"`
QueuePosition int64 `db:"queue_position" json:"queue_position"`
QueueSize int64 `db:"queue_size" json:"queue_size"`
}
func (q *sqlQuerier) GetProvisionerJobsByIDsWithQueuePosition(ctx context.Context, arg GetProvisionerJobsByIDsWithQueuePositionParams) ([]GetProvisionerJobsByIDsWithQueuePositionRow, error) {
rows, err := q.db.QueryContext(ctx, getProvisionerJobsByIDsWithQueuePosition, pq.Array(arg.IDs), arg.StaleIntervalMS)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetProvisionerJobsByIDsWithQueuePositionRow
for rows.Next() {
var i GetProvisionerJobsByIDsWithQueuePositionRow
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.ProvisionerJob.ID,
&i.ProvisionerJob.CreatedAt,
&i.ProvisionerJob.UpdatedAt,
&i.ProvisionerJob.StartedAt,
&i.ProvisionerJob.CanceledAt,
&i.ProvisionerJob.CompletedAt,
&i.ProvisionerJob.Error,
&i.ProvisionerJob.OrganizationID,
&i.ProvisionerJob.InitiatorID,
&i.ProvisionerJob.Provisioner,
&i.ProvisionerJob.StorageMethod,
&i.ProvisionerJob.Type,
&i.ProvisionerJob.Input,
&i.ProvisionerJob.WorkerID,
&i.ProvisionerJob.FileID,
&i.ProvisionerJob.Tags,
&i.ProvisionerJob.ErrorCode,
&i.ProvisionerJob.TraceMetadata,
&i.ProvisionerJob.JobStatus,
&i.ProvisionerJob.LogsLength,
&i.ProvisionerJob.LogsOverflowed,
&i.QueuePosition,
&i.QueueSize,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner = `-- name: GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner :many
WITH pending_jobs AS (
SELECT
id, initiator_id, created_at
FROM
provisioner_jobs
WHERE
started_at IS NULL
AND
canceled_at IS NULL
AND
completed_at IS NULL
AND
error IS NULL
),
queue_position AS (
SELECT
id,
ROW_NUMBER() OVER (ORDER BY initiator_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid ASC, created_at ASC) AS queue_position
FROM
pending_jobs
),
queue_size AS (
SELECT COUNT(*) AS count FROM pending_jobs
)
SELECT
pj.id, pj.created_at, pj.updated_at, pj.started_at, pj.canceled_at, pj.completed_at, pj.error, pj.organization_id, pj.initiator_id, pj.provisioner, pj.storage_method, pj.type, pj.input, pj.worker_id, pj.file_id, pj.tags, pj.error_code, pj.trace_metadata, pj.job_status, pj.logs_length, pj.logs_overflowed,
COALESCE(qp.queue_position, 0) AS queue_position,
COALESCE(qs.count, 0) AS queue_size,
-- Use subquery to utilize ORDER BY in array_agg since it cannot be
-- combined with FILTER.
(
SELECT
-- Order for stable output.
array_agg(pd.id ORDER BY pd.created_at ASC)::uuid[]
FROM
provisioner_daemons pd
WHERE
-- See AcquireProvisionerJob.
pj.started_at IS NULL
AND pj.organization_id = pd.organization_id
AND pj.provisioner = ANY(pd.provisioners)
AND provisioner_tagset_contains(pd.tags, pj.tags)
) AS available_workers,
-- Include template and workspace information.
COALESCE(tv.name, '') AS template_version_name,
t.id AS template_id,
COALESCE(t.name, '') AS template_name,
COALESCE(t.display_name, '') AS template_display_name,
COALESCE(t.icon, '') AS template_icon,
w.id AS workspace_id,
COALESCE(w.name, '') AS workspace_name,
-- Include the name of the provisioner_daemon associated to the job
COALESCE(pd.name, '') AS worker_name,
wb.transition as workspace_build_transition
FROM
provisioner_jobs pj
LEFT JOIN
queue_position qp ON qp.id = pj.id
LEFT JOIN
queue_size qs ON TRUE
LEFT JOIN
workspace_builds wb ON wb.id = CASE WHEN pj.input ? 'workspace_build_id' THEN (pj.input->>'workspace_build_id')::uuid END
LEFT JOIN
workspaces w ON (
w.id = wb.workspace_id
AND w.organization_id = pj.organization_id
)
LEFT JOIN
-- We should always have a template version, either explicitly or implicitly via workspace build.
template_versions tv ON (
tv.id = CASE WHEN pj.input ? 'template_version_id' THEN (pj.input->>'template_version_id')::uuid ELSE wb.template_version_id END
AND tv.organization_id = pj.organization_id
)
LEFT JOIN
templates t ON (
t.id = tv.template_id
AND t.organization_id = pj.organization_id
)
LEFT JOIN
-- Join to get the daemon name corresponding to the job's worker_id
provisioner_daemons pd ON pd.id = pj.worker_id
WHERE
pj.organization_id = $1::uuid
AND (COALESCE(array_length($2::uuid[], 1), 0) = 0 OR pj.id = ANY($2::uuid[]))
AND (COALESCE(array_length($3::provisioner_job_status[], 1), 0) = 0 OR pj.job_status = ANY($3::provisioner_job_status[]))
AND ($4::tagset = 'null'::tagset OR provisioner_tagset_contains(pj.tags::tagset, $4::tagset))
AND ($5::uuid = '00000000-0000-0000-0000-000000000000'::uuid OR pj.initiator_id = $5::uuid)
GROUP BY
pj.id,
qp.queue_position,
qs.count,
tv.name,
t.id,
t.name,
t.display_name,
t.icon,
w.id,
w.name,
pd.name,
wb.transition
ORDER BY
pj.created_at DESC
LIMIT
$6::int
`
type GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams struct {
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
IDs []uuid.UUID `db:"ids" json:"ids"`
Status []ProvisionerJobStatus `db:"status" json:"status"`
Tags StringMap `db:"tags" json:"tags"`
InitiatorID uuid.UUID `db:"initiator_id" json:"initiator_id"`
Limit sql.NullInt32 `db:"limit" json:"limit"`
}
type GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow struct {
ProvisionerJob ProvisionerJob `db:"provisioner_job" json:"provisioner_job"`
QueuePosition int64 `db:"queue_position" json:"queue_position"`
QueueSize int64 `db:"queue_size" json:"queue_size"`
AvailableWorkers []uuid.UUID `db:"available_workers" json:"available_workers"`
TemplateVersionName string `db:"template_version_name" json:"template_version_name"`
TemplateID uuid.NullUUID `db:"template_id" json:"template_id"`
TemplateName string `db:"template_name" json:"template_name"`
TemplateDisplayName string `db:"template_display_name" json:"template_display_name"`
TemplateIcon string `db:"template_icon" json:"template_icon"`
WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"`
WorkspaceName string `db:"workspace_name" json:"workspace_name"`
WorkerName string `db:"worker_name" json:"worker_name"`
WorkspaceBuildTransition NullWorkspaceTransition `db:"workspace_build_transition" json:"workspace_build_transition"`
}
func (q *sqlQuerier) GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner(ctx context.Context, arg GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams) ([]GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow, error) {
rows, err := q.db.QueryContext(ctx, getProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner,
arg.OrganizationID,
pq.Array(arg.IDs),
pq.Array(arg.Status),
arg.Tags,
arg.InitiatorID,
arg.Limit,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow
for rows.Next() {
var i GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow
if err := rows.Scan(
&i.ProvisionerJob.ID,
&i.ProvisionerJob.CreatedAt,
&i.ProvisionerJob.UpdatedAt,
&i.ProvisionerJob.StartedAt,
&i.ProvisionerJob.CanceledAt,
&i.ProvisionerJob.CompletedAt,
&i.ProvisionerJob.Error,
&i.ProvisionerJob.OrganizationID,
&i.ProvisionerJob.InitiatorID,
&i.ProvisionerJob.Provisioner,
&i.ProvisionerJob.StorageMethod,
&i.ProvisionerJob.Type,
&i.ProvisionerJob.Input,
&i.ProvisionerJob.WorkerID,
&i.ProvisionerJob.FileID,
&i.ProvisionerJob.Tags,
&i.ProvisionerJob.ErrorCode,
&i.ProvisionerJob.TraceMetadata,
&i.ProvisionerJob.JobStatus,
&i.ProvisionerJob.LogsLength,
&i.ProvisionerJob.LogsOverflowed,
&i.QueuePosition,
&i.QueueSize,
pq.Array(&i.AvailableWorkers),
&i.TemplateVersionName,
&i.TemplateID,
&i.TemplateName,
&i.TemplateDisplayName,
&i.TemplateIcon,
&i.WorkspaceID,
&i.WorkspaceName,
&i.WorkerName,
&i.WorkspaceBuildTransition,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getProvisionerJobsCreatedAfter = `-- name: GetProvisionerJobsCreatedAfter :many
SELECT id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status, logs_length, logs_overflowed FROM provisioner_jobs WHERE created_at > $1
`
func (q *sqlQuerier) GetProvisionerJobsCreatedAfter(ctx context.Context, createdAt time.Time) ([]ProvisionerJob, error) {
rows, err := q.db.QueryContext(ctx, getProvisionerJobsCreatedAfter, createdAt)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ProvisionerJob
for rows.Next() {
var i ProvisionerJob
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.StartedAt,
&i.CanceledAt,
&i.CompletedAt,
&i.Error,
&i.OrganizationID,
&i.InitiatorID,
&i.Provisioner,
&i.StorageMethod,
&i.Type,
&i.Input,
&i.WorkerID,
&i.FileID,
&i.Tags,
&i.ErrorCode,
&i.TraceMetadata,
&i.JobStatus,
&i.LogsLength,
&i.LogsOverflowed,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getProvisionerJobsToBeReaped = `-- name: GetProvisionerJobsToBeReaped :many
SELECT
id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status, logs_length, logs_overflowed
FROM
provisioner_jobs
WHERE
(
-- If the job has not been started before @pending_since, reap it.
updated_at < $1
AND started_at IS NULL
AND completed_at IS NULL
)
OR
(
-- If the job has been started but not completed before @hung_since, reap it.
updated_at < $2
AND started_at IS NOT NULL
AND completed_at IS NULL
)
ORDER BY random()
LIMIT $3
`
type GetProvisionerJobsToBeReapedParams struct {
PendingSince time.Time `db:"pending_since" json:"pending_since"`
HungSince time.Time `db:"hung_since" json:"hung_since"`
MaxJobs int32 `db:"max_jobs" json:"max_jobs"`
}
// To avoid repeatedly attempting to reap the same jobs, we randomly order and limit to @max_jobs.
func (q *sqlQuerier) GetProvisionerJobsToBeReaped(ctx context.Context, arg GetProvisionerJobsToBeReapedParams) ([]ProvisionerJob, error) {
rows, err := q.db.QueryContext(ctx, getProvisionerJobsToBeReaped, arg.PendingSince, arg.HungSince, arg.MaxJobs)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ProvisionerJob
for rows.Next() {
var i ProvisionerJob
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.StartedAt,
&i.CanceledAt,
&i.CompletedAt,
&i.Error,
&i.OrganizationID,
&i.InitiatorID,
&i.Provisioner,
&i.StorageMethod,
&i.Type,
&i.Input,
&i.WorkerID,
&i.FileID,
&i.Tags,
&i.ErrorCode,
&i.TraceMetadata,
&i.JobStatus,
&i.LogsLength,
&i.LogsOverflowed,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertProvisionerJob = `-- name: InsertProvisionerJob :one
INSERT INTO
provisioner_jobs (
id,
created_at,
updated_at,
organization_id,
initiator_id,
provisioner,
storage_method,
file_id,
"type",
"input",
tags,
trace_metadata,
logs_overflowed
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status, logs_length, logs_overflowed
`
type InsertProvisionerJobParams struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
InitiatorID uuid.UUID `db:"initiator_id" json:"initiator_id"`
Provisioner ProvisionerType `db:"provisioner" json:"provisioner"`
StorageMethod ProvisionerStorageMethod `db:"storage_method" json:"storage_method"`
FileID uuid.UUID `db:"file_id" json:"file_id"`
Type ProvisionerJobType `db:"type" json:"type"`
Input json.RawMessage `db:"input" json:"input"`
Tags StringMap `db:"tags" json:"tags"`
TraceMetadata pqtype.NullRawMessage `db:"trace_metadata" json:"trace_metadata"`
LogsOverflowed bool `db:"logs_overflowed" json:"logs_overflowed"`
}
func (q *sqlQuerier) InsertProvisionerJob(ctx context.Context, arg InsertProvisionerJobParams) (ProvisionerJob, error) {
row := q.db.QueryRowContext(ctx, insertProvisionerJob,
arg.ID,
arg.CreatedAt,
arg.UpdatedAt,
arg.OrganizationID,
arg.InitiatorID,
arg.Provisioner,
arg.StorageMethod,
arg.FileID,
arg.Type,
arg.Input,
arg.Tags,
arg.TraceMetadata,
arg.LogsOverflowed,
)
var i ProvisionerJob
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.StartedAt,
&i.CanceledAt,
&i.CompletedAt,
&i.Error,
&i.OrganizationID,
&i.InitiatorID,
&i.Provisioner,
&i.StorageMethod,
&i.Type,
&i.Input,
&i.WorkerID,
&i.FileID,
&i.Tags,
&i.ErrorCode,
&i.TraceMetadata,
&i.JobStatus,
&i.LogsLength,
&i.LogsOverflowed,
)
return i, err
}
const insertProvisionerJobTimings = `-- name: InsertProvisionerJobTimings :many
INSERT INTO provisioner_job_timings (job_id, started_at, ended_at, stage, source, action, resource)
SELECT
$1::uuid AS provisioner_job_id,
unnest($2::timestamptz[]),
unnest($3::timestamptz[]),
unnest($4::provisioner_job_timing_stage[]),
unnest($5::text[]),
unnest($6::text[]),
unnest($7::text[])
RETURNING job_id, started_at, ended_at, stage, source, action, resource
`
type InsertProvisionerJobTimingsParams struct {
JobID uuid.UUID `db:"job_id" json:"job_id"`
StartedAt []time.Time `db:"started_at" json:"started_at"`
EndedAt []time.Time `db:"ended_at" json:"ended_at"`
Stage []ProvisionerJobTimingStage `db:"stage" json:"stage"`
Source []string `db:"source" json:"source"`
Action []string `db:"action" json:"action"`
Resource []string `db:"resource" json:"resource"`
}
func (q *sqlQuerier) InsertProvisionerJobTimings(ctx context.Context, arg InsertProvisionerJobTimingsParams) ([]ProvisionerJobTiming, error) {
rows, err := q.db.QueryContext(ctx, insertProvisionerJobTimings,
arg.JobID,
pq.Array(arg.StartedAt),
pq.Array(arg.EndedAt),
pq.Array(arg.Stage),
pq.Array(arg.Source),
pq.Array(arg.Action),
pq.Array(arg.Resource),
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ProvisionerJobTiming
for rows.Next() {
var i ProvisionerJobTiming
if err := rows.Scan(
&i.JobID,
&i.StartedAt,
&i.EndedAt,
&i.Stage,
&i.Source,
&i.Action,
&i.Resource,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateProvisionerJobByID = `-- name: UpdateProvisionerJobByID :exec
UPDATE
provisioner_jobs
SET
updated_at = $2
WHERE
id = $1
`
type UpdateProvisionerJobByIDParams struct {
ID uuid.UUID `db:"id" json:"id"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
func (q *sqlQuerier) UpdateProvisionerJobByID(ctx context.Context, arg UpdateProvisionerJobByIDParams) error {
_, err := q.db.ExecContext(ctx, updateProvisionerJobByID, arg.ID, arg.UpdatedAt)
return err
}
const updateProvisionerJobWithCancelByID = `-- name: UpdateProvisionerJobWithCancelByID :exec
UPDATE
provisioner_jobs
SET
canceled_at = $2,
completed_at = $3
WHERE
id = $1
`
type UpdateProvisionerJobWithCancelByIDParams struct {
ID uuid.UUID `db:"id" json:"id"`
CanceledAt sql.NullTime `db:"canceled_at" json:"canceled_at"`
CompletedAt sql.NullTime `db:"completed_at" json:"completed_at"`
}
func (q *sqlQuerier) UpdateProvisionerJobWithCancelByID(ctx context.Context, arg UpdateProvisionerJobWithCancelByIDParams) error {
_, err := q.db.ExecContext(ctx, updateProvisionerJobWithCancelByID, arg.ID, arg.CanceledAt, arg.CompletedAt)
return err
}
const updateProvisionerJobWithCompleteByID = `-- name: UpdateProvisionerJobWithCompleteByID :exec
UPDATE
provisioner_jobs
SET
updated_at = $2,
completed_at = $3,
error = $4,
error_code = $5
WHERE
id = $1
`
type UpdateProvisionerJobWithCompleteByIDParams struct {
ID uuid.UUID `db:"id" json:"id"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
CompletedAt sql.NullTime `db:"completed_at" json:"completed_at"`
Error sql.NullString `db:"error" json:"error"`
ErrorCode sql.NullString `db:"error_code" json:"error_code"`
}
func (q *sqlQuerier) UpdateProvisionerJobWithCompleteByID(ctx context.Context, arg UpdateProvisionerJobWithCompleteByIDParams) error {
_, err := q.db.ExecContext(ctx, updateProvisionerJobWithCompleteByID,
arg.ID,
arg.UpdatedAt,
arg.CompletedAt,
arg.Error,
arg.ErrorCode,
)
return err
}
const updateProvisionerJobWithCompleteWithStartedAtByID = `-- name: UpdateProvisionerJobWithCompleteWithStartedAtByID :exec
UPDATE
provisioner_jobs
SET
updated_at = $2,
completed_at = $3,
error = $4,
error_code = $5,
started_at = $6
WHERE
id = $1
`
type UpdateProvisionerJobWithCompleteWithStartedAtByIDParams struct {
ID uuid.UUID `db:"id" json:"id"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
CompletedAt sql.NullTime `db:"completed_at" json:"completed_at"`
Error sql.NullString `db:"error" json:"error"`
ErrorCode sql.NullString `db:"error_code" json:"error_code"`
StartedAt sql.NullTime `db:"started_at" json:"started_at"`
}
func (q *sqlQuerier) UpdateProvisionerJobWithCompleteWithStartedAtByID(ctx context.Context, arg UpdateProvisionerJobWithCompleteWithStartedAtByIDParams) error {
_, err := q.db.ExecContext(ctx, updateProvisionerJobWithCompleteWithStartedAtByID,
arg.ID,
arg.UpdatedAt,
arg.CompletedAt,
arg.Error,
arg.ErrorCode,
arg.StartedAt,
)
return err
}
const deleteProvisionerKey = `-- name: DeleteProvisionerKey :exec
DELETE FROM
provisioner_keys
WHERE
id = $1
`
func (q *sqlQuerier) DeleteProvisionerKey(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteProvisionerKey, id)
return err
}
const getProvisionerKeyByHashedSecret = `-- name: GetProvisionerKeyByHashedSecret :one
SELECT
id, created_at, organization_id, name, hashed_secret, tags
FROM
provisioner_keys
WHERE
hashed_secret = $1
`
func (q *sqlQuerier) GetProvisionerKeyByHashedSecret(ctx context.Context, hashedSecret []byte) (ProvisionerKey, error) {
row := q.db.QueryRowContext(ctx, getProvisionerKeyByHashedSecret, hashedSecret)
var i ProvisionerKey
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.OrganizationID,
&i.Name,
&i.HashedSecret,
&i.Tags,
)
return i, err
}
const getProvisionerKeyByID = `-- name: GetProvisionerKeyByID :one
SELECT
id, created_at, organization_id, name, hashed_secret, tags
FROM
provisioner_keys
WHERE
id = $1
`
func (q *sqlQuerier) GetProvisionerKeyByID(ctx context.Context, id uuid.UUID) (ProvisionerKey, error) {
row := q.db.QueryRowContext(ctx, getProvisionerKeyByID, id)
var i ProvisionerKey
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.OrganizationID,
&i.Name,
&i.HashedSecret,
&i.Tags,
)
return i, err
}
const getProvisionerKeyByName = `-- name: GetProvisionerKeyByName :one
SELECT
id, created_at, organization_id, name, hashed_secret, tags
FROM
provisioner_keys
WHERE
organization_id = $1
AND
lower(name) = lower($2)
`
type GetProvisionerKeyByNameParams struct {
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
Name string `db:"name" json:"name"`
}
func (q *sqlQuerier) GetProvisionerKeyByName(ctx context.Context, arg GetProvisionerKeyByNameParams) (ProvisionerKey, error) {
row := q.db.QueryRowContext(ctx, getProvisionerKeyByName, arg.OrganizationID, arg.Name)
var i ProvisionerKey
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.OrganizationID,
&i.Name,
&i.HashedSecret,
&i.Tags,
)
return i, err
}
const insertProvisionerKey = `-- name: InsertProvisionerKey :one
INSERT INTO
provisioner_keys (
id,
created_at,
organization_id,
name,
hashed_secret,
tags
)
VALUES
($1, $2, $3, lower($6), $4, $5) RETURNING id, created_at, organization_id, name, hashed_secret, tags
`
type InsertProvisionerKeyParams struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
HashedSecret []byte `db:"hashed_secret" json:"hashed_secret"`
Tags StringMap `db:"tags" json:"tags"`
Name string `db:"name" json:"name"`
}
func (q *sqlQuerier) InsertProvisionerKey(ctx context.Context, arg InsertProvisionerKeyParams) (ProvisionerKey, error) {
row := q.db.QueryRowContext(ctx, insertProvisionerKey,
arg.ID,
arg.CreatedAt,
arg.OrganizationID,
arg.HashedSecret,
arg.Tags,
arg.Name,
)
var i ProvisionerKey
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.OrganizationID,
&i.Name,
&i.HashedSecret,
&i.Tags,
)
return i, err
}
const listProvisionerKeysByOrganization = `-- name: ListProvisionerKeysByOrganization :many
SELECT
id, created_at, organization_id, name, hashed_secret, tags
FROM
provisioner_keys
WHERE
organization_id = $1
`
func (q *sqlQuerier) ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerKey, error) {
rows, err := q.db.QueryContext(ctx, listProvisionerKeysByOrganization, organizationID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ProvisionerKey
for rows.Next() {
var i ProvisionerKey
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.OrganizationID,
&i.Name,
&i.HashedSecret,
&i.Tags,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listProvisionerKeysByOrganizationExcludeReserved = `-- name: ListProvisionerKeysByOrganizationExcludeReserved :many
SELECT
id, created_at, organization_id, name, hashed_secret, tags
FROM
provisioner_keys
WHERE
organization_id = $1
AND
-- exclude reserved built-in key
id != '00000000-0000-0000-0000-000000000001'::uuid
AND
-- exclude reserved user-auth key
id != '00000000-0000-0000-0000-000000000002'::uuid
AND
-- exclude reserved psk key
id != '00000000-0000-0000-0000-000000000003'::uuid
`
func (q *sqlQuerier) ListProvisionerKeysByOrganizationExcludeReserved(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerKey, error) {
rows, err := q.db.QueryContext(ctx, listProvisionerKeysByOrganizationExcludeReserved, organizationID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ProvisionerKey
for rows.Next() {
var i ProvisionerKey
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.OrganizationID,
&i.Name,
&i.HashedSecret,
&i.Tags,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const lockProvisionerKeyByIDForShare = `-- name: LockProvisionerKeyByIDForShare :one
SELECT
id
FROM
provisioner_keys
WHERE
id = $1
FOR KEY SHARE
`
// Locks the provisioner key row with FOR KEY SHARE for the remainder of the
// current transaction. FOR KEY SHARE conflicts with DELETE, so while the lock
// is held the key cannot be deleted, and a committed deletion is observed as
// no rows by later calls.
func (q *sqlQuerier) LockProvisionerKeyByIDForShare(ctx context.Context, id uuid.UUID) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, lockProvisionerKeyByIDForShare, id)
var id_2 uuid.UUID
err := row.Scan(&id_2)
return id_2, err
}
const getWorkspaceProxies = `-- name: GetWorkspaceProxies :many
SELECT
id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret, region_id, derp_enabled, derp_only, version
FROM
workspace_proxies
WHERE
deleted = false
`
func (q *sqlQuerier) GetWorkspaceProxies(ctx context.Context) ([]WorkspaceProxy, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceProxies)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceProxy
for rows.Next() {
var i WorkspaceProxy
if err := rows.Scan(
&i.ID,
&i.Name,
&i.DisplayName,
&i.Icon,
&i.Url,
&i.WildcardHostname,
&i.CreatedAt,
&i.UpdatedAt,
&i.Deleted,
&i.TokenHashedSecret,
&i.RegionID,
&i.DerpEnabled,
&i.DerpOnly,
&i.Version,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceProxyByHostname = `-- name: GetWorkspaceProxyByHostname :one
SELECT
id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret, region_id, derp_enabled, derp_only, version
FROM
workspace_proxies
WHERE
-- Validate that the @hostname has been sanitized and is not empty. This
-- doesn't prevent SQL injection (already prevented by using prepared
-- queries), but it does prevent carefully crafted hostnames from matching
-- when they shouldn't.
--
-- Periods don't need to be escaped because they're not special characters
-- in SQL matches unlike regular expressions.
$1 :: text SIMILAR TO '[a-zA-Z0-9._-]+' AND
deleted = false AND
-- Validate that the hostname matches either the wildcard hostname or the
-- access URL (ignoring scheme, port and path).
(
(
$2 :: bool = true AND
url SIMILAR TO '[^:]*://' || $1 :: text || '([:/]%)*'
) OR
(
$3 :: bool = true AND
$1 :: text LIKE replace(wildcard_hostname, '*', '%')
)
)
LIMIT
1
`
type GetWorkspaceProxyByHostnameParams struct {
Hostname string `db:"hostname" json:"hostname"`
AllowAccessUrl bool `db:"allow_access_url" json:"allow_access_url"`
AllowWildcardHostname bool `db:"allow_wildcard_hostname" json:"allow_wildcard_hostname"`
}
// Finds a workspace proxy that has an access URL or app hostname that matches
// the provided hostname. This is to check if a hostname matches any workspace
// proxy.
//
// The hostname must be sanitized to only contain [a-zA-Z0-9.-] before calling
// this query. The scheme, port and path should be stripped.
func (q *sqlQuerier) GetWorkspaceProxyByHostname(ctx context.Context, arg GetWorkspaceProxyByHostnameParams) (WorkspaceProxy, error) {
row := q.db.QueryRowContext(ctx, getWorkspaceProxyByHostname, arg.Hostname, arg.AllowAccessUrl, arg.AllowWildcardHostname)
var i WorkspaceProxy
err := row.Scan(
&i.ID,
&i.Name,
&i.DisplayName,
&i.Icon,
&i.Url,
&i.WildcardHostname,
&i.CreatedAt,
&i.UpdatedAt,
&i.Deleted,
&i.TokenHashedSecret,
&i.RegionID,
&i.DerpEnabled,
&i.DerpOnly,
&i.Version,
)
return i, err
}
const getWorkspaceProxyByID = `-- name: GetWorkspaceProxyByID :one
SELECT
id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret, region_id, derp_enabled, derp_only, version
FROM
workspace_proxies
WHERE
id = $1
LIMIT
1
`
func (q *sqlQuerier) GetWorkspaceProxyByID(ctx context.Context, id uuid.UUID) (WorkspaceProxy, error) {
row := q.db.QueryRowContext(ctx, getWorkspaceProxyByID, id)
var i WorkspaceProxy
err := row.Scan(
&i.ID,
&i.Name,
&i.DisplayName,
&i.Icon,
&i.Url,
&i.WildcardHostname,
&i.CreatedAt,
&i.UpdatedAt,
&i.Deleted,
&i.TokenHashedSecret,
&i.RegionID,
&i.DerpEnabled,
&i.DerpOnly,
&i.Version,
)
return i, err
}
const getWorkspaceProxyByName = `-- name: GetWorkspaceProxyByName :one
SELECT
id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret, region_id, derp_enabled, derp_only, version
FROM
workspace_proxies
WHERE
name = $1
AND deleted = false
LIMIT
1
`
func (q *sqlQuerier) GetWorkspaceProxyByName(ctx context.Context, name string) (WorkspaceProxy, error) {
row := q.db.QueryRowContext(ctx, getWorkspaceProxyByName, name)
var i WorkspaceProxy
err := row.Scan(
&i.ID,
&i.Name,
&i.DisplayName,
&i.Icon,
&i.Url,
&i.WildcardHostname,
&i.CreatedAt,
&i.UpdatedAt,
&i.Deleted,
&i.TokenHashedSecret,
&i.RegionID,
&i.DerpEnabled,
&i.DerpOnly,
&i.Version,
)
return i, err
}
const insertWorkspaceProxy = `-- name: InsertWorkspaceProxy :one
INSERT INTO
workspace_proxies (
id,
url,
wildcard_hostname,
name,
display_name,
icon,
derp_enabled,
derp_only,
token_hashed_secret,
created_at,
updated_at,
deleted
)
VALUES
($1, '', '', $2, $3, $4, $5, $6, $7, $8, $9, false) RETURNING id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret, region_id, derp_enabled, derp_only, version
`
type InsertWorkspaceProxyParams struct {
ID uuid.UUID `db:"id" json:"id"`
Name string `db:"name" json:"name"`
DisplayName string `db:"display_name" json:"display_name"`
Icon string `db:"icon" json:"icon"`
DerpEnabled bool `db:"derp_enabled" json:"derp_enabled"`
DerpOnly bool `db:"derp_only" json:"derp_only"`
TokenHashedSecret []byte `db:"token_hashed_secret" json:"token_hashed_secret"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
func (q *sqlQuerier) InsertWorkspaceProxy(ctx context.Context, arg InsertWorkspaceProxyParams) (WorkspaceProxy, error) {
row := q.db.QueryRowContext(ctx, insertWorkspaceProxy,
arg.ID,
arg.Name,
arg.DisplayName,
arg.Icon,
arg.DerpEnabled,
arg.DerpOnly,
arg.TokenHashedSecret,
arg.CreatedAt,
arg.UpdatedAt,
)
var i WorkspaceProxy
err := row.Scan(
&i.ID,
&i.Name,
&i.DisplayName,
&i.Icon,
&i.Url,
&i.WildcardHostname,
&i.CreatedAt,
&i.UpdatedAt,
&i.Deleted,
&i.TokenHashedSecret,
&i.RegionID,
&i.DerpEnabled,
&i.DerpOnly,
&i.Version,
)
return i, err
}
const registerWorkspaceProxy = `-- name: RegisterWorkspaceProxy :one
UPDATE
workspace_proxies
SET
url = $1 :: text,
wildcard_hostname = $2 :: text,
derp_enabled = $3 :: boolean,
derp_only = $4 :: boolean,
version = $5 :: text,
updated_at = Now()
WHERE
id = $6
RETURNING id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret, region_id, derp_enabled, derp_only, version
`
type RegisterWorkspaceProxyParams struct {
Url string `db:"url" json:"url"`
WildcardHostname string `db:"wildcard_hostname" json:"wildcard_hostname"`
DerpEnabled bool `db:"derp_enabled" json:"derp_enabled"`
DerpOnly bool `db:"derp_only" json:"derp_only"`
Version string `db:"version" json:"version"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) RegisterWorkspaceProxy(ctx context.Context, arg RegisterWorkspaceProxyParams) (WorkspaceProxy, error) {
row := q.db.QueryRowContext(ctx, registerWorkspaceProxy,
arg.Url,
arg.WildcardHostname,
arg.DerpEnabled,
arg.DerpOnly,
arg.Version,
arg.ID,
)
var i WorkspaceProxy
err := row.Scan(
&i.ID,
&i.Name,
&i.DisplayName,
&i.Icon,
&i.Url,
&i.WildcardHostname,
&i.CreatedAt,
&i.UpdatedAt,
&i.Deleted,
&i.TokenHashedSecret,
&i.RegionID,
&i.DerpEnabled,
&i.DerpOnly,
&i.Version,
)
return i, err
}
const updateWorkspaceProxy = `-- name: UpdateWorkspaceProxy :one
UPDATE
workspace_proxies
SET
-- These values should always be provided.
name = $1,
display_name = $2,
icon = $3,
-- Only update the token if a new one is provided.
-- So this is an optional field.
token_hashed_secret = CASE
WHEN length($4 :: bytea) > 0 THEN $4 :: bytea
ELSE workspace_proxies.token_hashed_secret
END,
-- Always update this timestamp.
updated_at = Now()
WHERE
id = $5
RETURNING id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret, region_id, derp_enabled, derp_only, version
`
type UpdateWorkspaceProxyParams struct {
Name string `db:"name" json:"name"`
DisplayName string `db:"display_name" json:"display_name"`
Icon string `db:"icon" json:"icon"`
TokenHashedSecret []byte `db:"token_hashed_secret" json:"token_hashed_secret"`
ID uuid.UUID `db:"id" json:"id"`
}
// This allows editing the properties of a workspace proxy.
func (q *sqlQuerier) UpdateWorkspaceProxy(ctx context.Context, arg UpdateWorkspaceProxyParams) (WorkspaceProxy, error) {
row := q.db.QueryRowContext(ctx, updateWorkspaceProxy,
arg.Name,
arg.DisplayName,
arg.Icon,
arg.TokenHashedSecret,
arg.ID,
)
var i WorkspaceProxy
err := row.Scan(
&i.ID,
&i.Name,
&i.DisplayName,
&i.Icon,
&i.Url,
&i.WildcardHostname,
&i.CreatedAt,
&i.UpdatedAt,
&i.Deleted,
&i.TokenHashedSecret,
&i.RegionID,
&i.DerpEnabled,
&i.DerpOnly,
&i.Version,
)
return i, err
}
const updateWorkspaceProxyDeleted = `-- name: UpdateWorkspaceProxyDeleted :exec
UPDATE
workspace_proxies
SET
updated_at = Now(),
deleted = $1
WHERE
id = $2
`
type UpdateWorkspaceProxyDeletedParams struct {
Deleted bool `db:"deleted" json:"deleted"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateWorkspaceProxyDeleted(ctx context.Context, arg UpdateWorkspaceProxyDeletedParams) error {
_, err := q.db.ExecContext(ctx, updateWorkspaceProxyDeleted, arg.Deleted, arg.ID)
return err
}
const getQuotaAllowanceForUser = `-- name: GetQuotaAllowanceForUser :one
SELECT
coalesce(SUM(groups.quota_allowance), 0)::BIGINT
FROM
(
-- Select all groups this user is a member of. This will also include
-- the "Everyone" group for organizations the user is a member of.
SELECT user_id, user_email, user_username, user_hashed_password, user_created_at, user_updated_at, user_status, user_rbac_roles, user_login_type, user_avatar_url, user_deleted, user_last_seen_at, user_quiet_hours_schedule, user_name, user_github_com_user_id, user_is_system, user_is_service_account, organization_id, group_name, group_id FROM group_members_expanded
WHERE
$1 = user_id AND
$2 = group_members_expanded.organization_id
) AS members
INNER JOIN groups ON
members.group_id = groups.id
`
type GetQuotaAllowanceForUserParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
}
func (q *sqlQuerier) GetQuotaAllowanceForUser(ctx context.Context, arg GetQuotaAllowanceForUserParams) (int64, error) {
row := q.db.QueryRowContext(ctx, getQuotaAllowanceForUser, arg.UserID, arg.OrganizationID)
var column_1 int64
err := row.Scan(&column_1)
return column_1, err
}
const getQuotaConsumedForUser = `-- name: GetQuotaConsumedForUser :one
WITH latest_builds AS (
SELECT
DISTINCT ON
(wb.workspace_id) wb.workspace_id,
wb.daily_cost
FROM
workspace_builds wb
-- This INNER JOIN prevents a seq scan of the workspace_builds table.
-- Limit the rows to the absolute minimum required, which is all workspaces
-- in a given organization for a given user.
INNER JOIN
workspaces on wb.workspace_id = workspaces.id
WHERE
-- Only return workspaces that match the user + organization.
-- Quotas are calculated per user per organization.
NOT workspaces.deleted AND
workspaces.owner_id = $1 AND
workspaces.organization_id = $2
ORDER BY
wb.workspace_id,
wb.build_number DESC
)
SELECT
coalesce(SUM(daily_cost), 0)::BIGINT
FROM
latest_builds
`
type GetQuotaConsumedForUserParams struct {
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
}
func (q *sqlQuerier) GetQuotaConsumedForUser(ctx context.Context, arg GetQuotaConsumedForUserParams) (int64, error) {
row := q.db.QueryRowContext(ctx, getQuotaConsumedForUser, arg.OwnerID, arg.OrganizationID)
var column_1 int64
err := row.Scan(&column_1)
return column_1, err
}
const deleteReplicasUpdatedBefore = `-- name: DeleteReplicasUpdatedBefore :exec
DELETE FROM replicas WHERE updated_at < $1
`
func (q *sqlQuerier) DeleteReplicasUpdatedBefore(ctx context.Context, updatedAt time.Time) error {
_, err := q.db.ExecContext(ctx, deleteReplicasUpdatedBefore, updatedAt)
return err
}
const getReplicaByID = `-- name: GetReplicaByID :one
SELECT id, created_at, started_at, stopped_at, updated_at, hostname, region_id, relay_address, database_latency, version, error, "primary", cluster_host, nats_port FROM replicas WHERE id = $1
`
func (q *sqlQuerier) GetReplicaByID(ctx context.Context, id uuid.UUID) (Replica, error) {
row := q.db.QueryRowContext(ctx, getReplicaByID, id)
var i Replica
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.StartedAt,
&i.StoppedAt,
&i.UpdatedAt,
&i.Hostname,
&i.RegionID,
&i.RelayAddress,
&i.DatabaseLatency,
&i.Version,
&i.Error,
&i.Primary,
&i.ClusterHost,
&i.NATSPort,
)
return i, err
}
const getReplicasUpdatedAfter = `-- name: GetReplicasUpdatedAfter :many
SELECT id, created_at, started_at, stopped_at, updated_at, hostname, region_id, relay_address, database_latency, version, error, "primary", cluster_host, nats_port FROM replicas WHERE updated_at > $1 AND stopped_at IS NULL
`
func (q *sqlQuerier) GetReplicasUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]Replica, error) {
rows, err := q.db.QueryContext(ctx, getReplicasUpdatedAfter, updatedAt)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Replica
for rows.Next() {
var i Replica
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.StartedAt,
&i.StoppedAt,
&i.UpdatedAt,
&i.Hostname,
&i.RegionID,
&i.RelayAddress,
&i.DatabaseLatency,
&i.Version,
&i.Error,
&i.Primary,
&i.ClusterHost,
&i.NATSPort,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertReplica = `-- name: InsertReplica :one
INSERT INTO replicas (
id,
created_at,
started_at,
updated_at,
hostname,
region_id,
relay_address,
version,
database_latency,
"primary",
cluster_host,
nats_port
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id, created_at, started_at, stopped_at, updated_at, hostname, region_id, relay_address, database_latency, version, error, "primary", cluster_host, nats_port
`
type InsertReplicaParams struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
StartedAt time.Time `db:"started_at" json:"started_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Hostname string `db:"hostname" json:"hostname"`
RegionID int32 `db:"region_id" json:"region_id"`
RelayAddress string `db:"relay_address" json:"relay_address"`
Version string `db:"version" json:"version"`
DatabaseLatency int32 `db:"database_latency" json:"database_latency"`
Primary bool `db:"primary" json:"primary"`
ClusterHost string `db:"cluster_host" json:"cluster_host"`
NATSPort int32 `db:"nats_port" json:"nats_port"`
}
func (q *sqlQuerier) InsertReplica(ctx context.Context, arg InsertReplicaParams) (Replica, error) {
row := q.db.QueryRowContext(ctx, insertReplica,
arg.ID,
arg.CreatedAt,
arg.StartedAt,
arg.UpdatedAt,
arg.Hostname,
arg.RegionID,
arg.RelayAddress,
arg.Version,
arg.DatabaseLatency,
arg.Primary,
arg.ClusterHost,
arg.NATSPort,
)
var i Replica
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.StartedAt,
&i.StoppedAt,
&i.UpdatedAt,
&i.Hostname,
&i.RegionID,
&i.RelayAddress,
&i.DatabaseLatency,
&i.Version,
&i.Error,
&i.Primary,
&i.ClusterHost,
&i.NATSPort,
)
return i, err
}
const updateReplica = `-- name: UpdateReplica :one
UPDATE replicas SET
updated_at = $2,
started_at = $3,
stopped_at = $4,
relay_address = $5,
region_id = $6,
hostname = $7,
version = $8,
error = $9,
database_latency = $10,
"primary" = $11,
cluster_host = $12,
nats_port = $13
WHERE id = $1 RETURNING id, created_at, started_at, stopped_at, updated_at, hostname, region_id, relay_address, database_latency, version, error, "primary", cluster_host, nats_port
`
type UpdateReplicaParams struct {
ID uuid.UUID `db:"id" json:"id"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
StartedAt time.Time `db:"started_at" json:"started_at"`
StoppedAt sql.NullTime `db:"stopped_at" json:"stopped_at"`
RelayAddress string `db:"relay_address" json:"relay_address"`
RegionID int32 `db:"region_id" json:"region_id"`
Hostname string `db:"hostname" json:"hostname"`
Version string `db:"version" json:"version"`
Error string `db:"error" json:"error"`
DatabaseLatency int32 `db:"database_latency" json:"database_latency"`
Primary bool `db:"primary" json:"primary"`
ClusterHost string `db:"cluster_host" json:"cluster_host"`
NATSPort int32 `db:"nats_port" json:"nats_port"`
}
func (q *sqlQuerier) UpdateReplica(ctx context.Context, arg UpdateReplicaParams) (Replica, error) {
row := q.db.QueryRowContext(ctx, updateReplica,
arg.ID,
arg.UpdatedAt,
arg.StartedAt,
arg.StoppedAt,
arg.RelayAddress,
arg.RegionID,
arg.Hostname,
arg.Version,
arg.Error,
arg.DatabaseLatency,
arg.Primary,
arg.ClusterHost,
arg.NATSPort,
)
var i Replica
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.StartedAt,
&i.StoppedAt,
&i.UpdatedAt,
&i.Hostname,
&i.RegionID,
&i.RelayAddress,
&i.DatabaseLatency,
&i.Version,
&i.Error,
&i.Primary,
&i.ClusterHost,
&i.NATSPort,
)
return i, err
}
const customRoles = `-- name: CustomRoles :many
SELECT
name, display_name, site_permissions, org_permissions, user_permissions, created_at, updated_at, organization_id, id, is_system, member_permissions
FROM
custom_roles
WHERE
true
-- @lookup_roles will filter for exact (role_name, org_id) pairs
-- To do this manually in SQL, you can construct an array and cast it:
-- cast(ARRAY[('customrole','ece79dac-926e-44ca-9790-2ff7c5eb6e0c')] AS name_organization_pair[])
AND CASE WHEN array_length($1 :: name_organization_pair[], 1) > 0 THEN
-- Using 'coalesce' to avoid troubles with null literals being an empty string.
(name, coalesce(organization_id, '00000000-0000-0000-0000-000000000000' ::uuid)) = ANY ($1::name_organization_pair[])
ELSE true
END
-- This allows fetching all roles, or just site wide roles
AND CASE WHEN $2 :: boolean THEN
organization_id IS null
ELSE true
END
-- Allows fetching all roles to a particular organization
AND CASE WHEN $3 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
organization_id = $3
ELSE true
END
-- Filter system roles. By default, system roles are excluded.
-- System roles are managed by Coder and should be hidden from user-facing APIs.
-- The authorization system uses @include_system_roles = true to load them.
AND CASE WHEN $4 :: boolean THEN
true
ELSE
is_system = false
END
`
type CustomRolesParams struct {
LookupRoles []NameOrganizationPair `db:"lookup_roles" json:"lookup_roles"`
ExcludeOrgRoles bool `db:"exclude_org_roles" json:"exclude_org_roles"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
IncludeSystemRoles bool `db:"include_system_roles" json:"include_system_roles"`
}
func (q *sqlQuerier) CustomRoles(ctx context.Context, arg CustomRolesParams) ([]CustomRole, error) {
rows, err := q.db.QueryContext(ctx, customRoles,
pq.Array(arg.LookupRoles),
arg.ExcludeOrgRoles,
arg.OrganizationID,
arg.IncludeSystemRoles,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []CustomRole
for rows.Next() {
var i CustomRole
if err := rows.Scan(
&i.Name,
&i.DisplayName,
&i.SitePermissions,
&i.OrgPermissions,
&i.UserPermissions,
&i.CreatedAt,
&i.UpdatedAt,
&i.OrganizationID,
&i.ID,
&i.IsSystem,
&i.MemberPermissions,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const deleteCustomRole = `-- name: DeleteCustomRole :exec
DELETE FROM
custom_roles
WHERE
name = lower($1)
AND organization_id = $2
-- Prevents accidental deletion of system roles even if the API
-- layer check is bypassed due to a bug.
AND is_system = false
`
type DeleteCustomRoleParams struct {
Name string `db:"name" json:"name"`
OrganizationID uuid.NullUUID `db:"organization_id" json:"organization_id"`
}
func (q *sqlQuerier) DeleteCustomRole(ctx context.Context, arg DeleteCustomRoleParams) error {
_, err := q.db.ExecContext(ctx, deleteCustomRole, arg.Name, arg.OrganizationID)
return err
}
const insertCustomRole = `-- name: InsertCustomRole :one
INSERT INTO
custom_roles (
name,
display_name,
organization_id,
site_permissions,
org_permissions,
user_permissions,
member_permissions,
is_system,
created_at,
updated_at
)
VALUES (
-- Always force lowercase names
lower($1),
$2,
$3,
$4,
$5,
$6,
$7,
$8,
now(),
now()
)
RETURNING name, display_name, site_permissions, org_permissions, user_permissions, created_at, updated_at, organization_id, id, is_system, member_permissions
`
type InsertCustomRoleParams struct {
Name string `db:"name" json:"name"`
DisplayName string `db:"display_name" json:"display_name"`
OrganizationID uuid.NullUUID `db:"organization_id" json:"organization_id"`
SitePermissions CustomRolePermissions `db:"site_permissions" json:"site_permissions"`
OrgPermissions CustomRolePermissions `db:"org_permissions" json:"org_permissions"`
UserPermissions CustomRolePermissions `db:"user_permissions" json:"user_permissions"`
MemberPermissions CustomRolePermissions `db:"member_permissions" json:"member_permissions"`
IsSystem bool `db:"is_system" json:"is_system"`
}
func (q *sqlQuerier) InsertCustomRole(ctx context.Context, arg InsertCustomRoleParams) (CustomRole, error) {
row := q.db.QueryRowContext(ctx, insertCustomRole,
arg.Name,
arg.DisplayName,
arg.OrganizationID,
arg.SitePermissions,
arg.OrgPermissions,
arg.UserPermissions,
arg.MemberPermissions,
arg.IsSystem,
)
var i CustomRole
err := row.Scan(
&i.Name,
&i.DisplayName,
&i.SitePermissions,
&i.OrgPermissions,
&i.UserPermissions,
&i.CreatedAt,
&i.UpdatedAt,
&i.OrganizationID,
&i.ID,
&i.IsSystem,
&i.MemberPermissions,
)
return i, err
}
const updateCustomRole = `-- name: UpdateCustomRole :one
UPDATE
custom_roles
SET
display_name = $1,
site_permissions = $2,
org_permissions = $3,
user_permissions = $4,
member_permissions = $5,
updated_at = now()
WHERE
name = lower($6)
AND organization_id = $7
RETURNING name, display_name, site_permissions, org_permissions, user_permissions, created_at, updated_at, organization_id, id, is_system, member_permissions
`
type UpdateCustomRoleParams struct {
DisplayName string `db:"display_name" json:"display_name"`
SitePermissions CustomRolePermissions `db:"site_permissions" json:"site_permissions"`
OrgPermissions CustomRolePermissions `db:"org_permissions" json:"org_permissions"`
UserPermissions CustomRolePermissions `db:"user_permissions" json:"user_permissions"`
MemberPermissions CustomRolePermissions `db:"member_permissions" json:"member_permissions"`
Name string `db:"name" json:"name"`
OrganizationID uuid.NullUUID `db:"organization_id" json:"organization_id"`
}
func (q *sqlQuerier) UpdateCustomRole(ctx context.Context, arg UpdateCustomRoleParams) (CustomRole, error) {
row := q.db.QueryRowContext(ctx, updateCustomRole,
arg.DisplayName,
arg.SitePermissions,
arg.OrgPermissions,
arg.UserPermissions,
arg.MemberPermissions,
arg.Name,
arg.OrganizationID,
)
var i CustomRole
err := row.Scan(
&i.Name,
&i.DisplayName,
&i.SitePermissions,
&i.OrgPermissions,
&i.UserPermissions,
&i.CreatedAt,
&i.UpdatedAt,
&i.OrganizationID,
&i.ID,
&i.IsSystem,
&i.MemberPermissions,
)
return i, err
}
const deleteRuntimeConfig = `-- name: DeleteRuntimeConfig :exec
DELETE FROM site_configs
WHERE site_configs.key = $1
`
func (q *sqlQuerier) DeleteRuntimeConfig(ctx context.Context, key string) error {
_, err := q.db.ExecContext(ctx, deleteRuntimeConfig, key)
return err
}
const getAnnouncementBanners = `-- name: GetAnnouncementBanners :one
SELECT value FROM site_configs WHERE key = 'announcement_banners'
`
func (q *sqlQuerier) GetAnnouncementBanners(ctx context.Context) (string, error) {
row := q.db.QueryRowContext(ctx, getAnnouncementBanners)
var value string
err := row.Scan(&value)
return value, err
}
const getApplicationName = `-- name: GetApplicationName :one
SELECT value FROM site_configs WHERE key = 'application_name'
`
func (q *sqlQuerier) GetApplicationName(ctx context.Context) (string, error) {
row := q.db.QueryRowContext(ctx, getApplicationName)
var value string
err := row.Scan(&value)
return value, err
}
const getChatAdvisorConfig = `-- name: GetChatAdvisorConfig :one
SELECT
COALESCE((SELECT value FROM site_configs WHERE key = 'agents_advisor_config'), '{}') :: text AS advisor_config
`
// GetChatAdvisorConfig returns the deployment-wide runtime configuration
// for the experimental chat advisor as a JSON blob. Callers unmarshal the
// result into codersdk.AdvisorConfig. Returns '{}' when unset so zero
// values apply by default.
func (q *sqlQuerier) GetChatAdvisorConfig(ctx context.Context) (string, error) {
row := q.db.QueryRowContext(ctx, getChatAdvisorConfig)
var advisor_config string
err := row.Scan(&advisor_config)
return advisor_config, err
}
const getChatAutoArchiveDays = `-- name: GetChatAutoArchiveDays :one
SELECT COALESCE(
(SELECT value::integer FROM site_configs
WHERE key = 'agents_chat_auto_archive_days'),
$1::integer
) :: integer AS auto_archive_days
`
// Auto-archive window in days. 0 disables.
func (q *sqlQuerier) GetChatAutoArchiveDays(ctx context.Context, defaultAutoArchiveDays int32) (int32, error) {
row := q.db.QueryRowContext(ctx, getChatAutoArchiveDays, defaultAutoArchiveDays)
var auto_archive_days int32
err := row.Scan(&auto_archive_days)
return auto_archive_days, err
}
const getChatCompactionModelOverride = `-- name: GetChatCompactionModelOverride :one
SELECT
COALESCE((SELECT value FROM site_configs WHERE key = 'agents_chat_compaction_model_override'), '') :: text AS model_config_id
`
func (q *sqlQuerier) GetChatCompactionModelOverride(ctx context.Context) (string, error) {
row := q.db.QueryRowContext(ctx, getChatCompactionModelOverride)
var model_config_id string
err := row.Scan(&model_config_id)
return model_config_id, err
}
const getChatComputerUseProvider = `-- name: GetChatComputerUseProvider :one
SELECT
COALESCE((SELECT value FROM site_configs WHERE key = 'agents_computer_use_provider'), '') :: text AS provider
`
func (q *sqlQuerier) GetChatComputerUseProvider(ctx context.Context) (string, error) {
row := q.db.QueryRowContext(ctx, getChatComputerUseProvider)
var provider string
err := row.Scan(&provider)
return provider, err
}
const getChatDebugLoggingAllowUsers = `-- name: GetChatDebugLoggingAllowUsers :one
SELECT
COALESCE((SELECT value = 'true' FROM site_configs WHERE key = 'agents_chat_debug_logging_allow_users'), false) :: boolean AS allow_users
`
// GetChatDebugLoggingAllowUsers returns the runtime admin setting that
// allows users to opt into chat debug logging when the deployment does
// not already force debug logging on globally.
func (q *sqlQuerier) GetChatDebugLoggingAllowUsers(ctx context.Context) (bool, error) {
row := q.db.QueryRowContext(ctx, getChatDebugLoggingAllowUsers)
var allow_users bool
err := row.Scan(&allow_users)
return allow_users, err
}
const getChatDebugRetentionDays = `-- name: GetChatDebugRetentionDays :one
SELECT COALESCE(
(SELECT value::integer FROM site_configs
WHERE key = 'agents_chat_debug_retention_days'),
$1::integer
) :: integer AS debug_retention_days
`
// Chat debug run retention window in days. 0 disables.
func (q *sqlQuerier) GetChatDebugRetentionDays(ctx context.Context, defaultDebugRetentionDays int32) (int32, error) {
row := q.db.QueryRowContext(ctx, getChatDebugRetentionDays, defaultDebugRetentionDays)
var debug_retention_days int32
err := row.Scan(&debug_retention_days)
return debug_retention_days, err
}
const getChatDesktopEnabled = `-- name: GetChatDesktopEnabled :one
SELECT
COALESCE((SELECT value = 'true' FROM site_configs WHERE key = 'agents_desktop_enabled'), false) :: boolean AS enable_desktop
`
func (q *sqlQuerier) GetChatDesktopEnabled(ctx context.Context) (bool, error) {
row := q.db.QueryRowContext(ctx, getChatDesktopEnabled)
var enable_desktop bool
err := row.Scan(&enable_desktop)
return enable_desktop, err
}
const getChatExploreModelOverride = `-- name: GetChatExploreModelOverride :one
SELECT
COALESCE((SELECT value FROM site_configs WHERE key = 'agents_chat_explore_model_override'), '') :: text AS model_config_id
`
func (q *sqlQuerier) GetChatExploreModelOverride(ctx context.Context) (string, error) {
row := q.db.QueryRowContext(ctx, getChatExploreModelOverride)
var model_config_id string
err := row.Scan(&model_config_id)
return model_config_id, err
}
const getChatGeneralModelOverride = `-- name: GetChatGeneralModelOverride :one
SELECT
COALESCE((SELECT value FROM site_configs WHERE key = 'agents_chat_general_model_override'), '') :: text AS model_config_id
`
func (q *sqlQuerier) GetChatGeneralModelOverride(ctx context.Context) (string, error) {
row := q.db.QueryRowContext(ctx, getChatGeneralModelOverride)
var model_config_id string
err := row.Scan(&model_config_id)
return model_config_id, err
}
const getChatIncludeDefaultSystemPrompt = `-- name: GetChatIncludeDefaultSystemPrompt :one
SELECT
COALESCE(
(SELECT value = 'true' FROM site_configs WHERE key = 'agents_chat_include_default_system_prompt'),
NOT EXISTS (
SELECT 1
FROM site_configs
WHERE key = 'agents_chat_system_prompt'
AND value != ''
)
) :: boolean AS include_default_system_prompt
`
// GetChatIncludeDefaultSystemPrompt preserves the legacy default
// for deployments created before the explicit include-default toggle.
// When the toggle is unset, a non-empty custom prompt implies false;
// otherwise the setting defaults to true.
func (q *sqlQuerier) GetChatIncludeDefaultSystemPrompt(ctx context.Context) (bool, error) {
row := q.db.QueryRowContext(ctx, getChatIncludeDefaultSystemPrompt)
var include_default_system_prompt bool
err := row.Scan(&include_default_system_prompt)
return include_default_system_prompt, err
}
const getChatPersonalModelOverridesEnabled = `-- name: GetChatPersonalModelOverridesEnabled :one
SELECT
COALESCE((SELECT value = 'true' FROM site_configs WHERE key = 'agents_chat_personal_model_overrides_enabled'), false) :: boolean AS enabled
`
// GetChatPersonalModelOverridesEnabled returns whether users may configure
// personal chat model overrides. It defaults to false when unset.
func (q *sqlQuerier) GetChatPersonalModelOverridesEnabled(ctx context.Context) (bool, error) {
row := q.db.QueryRowContext(ctx, getChatPersonalModelOverridesEnabled)
var enabled bool
err := row.Scan(&enabled)
return enabled, err
}
const getChatPlanModeInstructions = `-- name: GetChatPlanModeInstructions :one
SELECT
COALESCE((SELECT value FROM site_configs WHERE key = 'agents_chat_plan_mode_instructions'), '') :: text AS plan_mode_instructions
`
func (q *sqlQuerier) GetChatPlanModeInstructions(ctx context.Context) (string, error) {
row := q.db.QueryRowContext(ctx, getChatPlanModeInstructions)
var plan_mode_instructions string
err := row.Scan(&plan_mode_instructions)
return plan_mode_instructions, err
}
const getChatRetentionDays = `-- name: GetChatRetentionDays :one
SELECT COALESCE(
(SELECT value::integer FROM site_configs
WHERE key = 'agents_chat_retention_days'),
30
) :: integer AS retention_days
`
// Returns the chat retention period in days. Chats archived longer
// than this and orphaned chat files older than this are purged by
// dbpurge. Returns 30 (days) when no value has been configured.
// A value of 0 disables chat purging entirely.
func (q *sqlQuerier) GetChatRetentionDays(ctx context.Context) (int32, error) {
row := q.db.QueryRowContext(ctx, getChatRetentionDays)
var retention_days int32
err := row.Scan(&retention_days)
return retention_days, err
}
const getChatSystemPrompt = `-- name: GetChatSystemPrompt :one
SELECT
COALESCE((SELECT value FROM site_configs WHERE key = 'agents_chat_system_prompt'), '') :: text AS chat_system_prompt
`
func (q *sqlQuerier) GetChatSystemPrompt(ctx context.Context) (string, error) {
row := q.db.QueryRowContext(ctx, getChatSystemPrompt)
var chat_system_prompt string
err := row.Scan(&chat_system_prompt)
return chat_system_prompt, err
}
const getChatSystemPromptConfig = `-- name: GetChatSystemPromptConfig :one
SELECT
COALESCE((SELECT value FROM site_configs WHERE key = 'agents_chat_system_prompt'), '') :: text AS chat_system_prompt,
COALESCE(
(SELECT value = 'true' FROM site_configs WHERE key = 'agents_chat_include_default_system_prompt'),
NOT EXISTS (
SELECT 1
FROM site_configs
WHERE key = 'agents_chat_system_prompt'
AND value != ''
)
) :: boolean AS include_default_system_prompt
`
type GetChatSystemPromptConfigRow struct {
ChatSystemPrompt string `db:"chat_system_prompt" json:"chat_system_prompt"`
IncludeDefaultSystemPrompt bool `db:"include_default_system_prompt" json:"include_default_system_prompt"`
}
// GetChatSystemPromptConfig returns both chat system prompt settings in a
// single read to avoid torn reads between separate site-config lookups.
// The include-default fallback preserves the legacy behavior where a
// non-empty custom prompt implied opting out before the explicit toggle
// existed.
func (q *sqlQuerier) GetChatSystemPromptConfig(ctx context.Context) (GetChatSystemPromptConfigRow, error) {
row := q.db.QueryRowContext(ctx, getChatSystemPromptConfig)
var i GetChatSystemPromptConfigRow
err := row.Scan(&i.ChatSystemPrompt, &i.IncludeDefaultSystemPrompt)
return i, err
}
const getChatTitleGenerationModelOverride = `-- name: GetChatTitleGenerationModelOverride :one
SELECT
COALESCE((SELECT value FROM site_configs WHERE key = 'agents_chat_title_generation_model_override'), '') :: text AS model_config_id
`
func (q *sqlQuerier) GetChatTitleGenerationModelOverride(ctx context.Context) (string, error) {
row := q.db.QueryRowContext(ctx, getChatTitleGenerationModelOverride)
var model_config_id string
err := row.Scan(&model_config_id)
return model_config_id, err
}
const getChatWorkspaceTTL = `-- name: GetChatWorkspaceTTL :one
SELECT
COALESCE(
(SELECT value FROM site_configs WHERE key = 'agents_workspace_ttl'),
'0s'
)::text AS workspace_ttl
`
// Returns the global TTL for chat workspaces as a Go duration string.
// Returns "0s" (disabled) when no value has been configured.
func (q *sqlQuerier) GetChatWorkspaceTTL(ctx context.Context) (string, error) {
row := q.db.QueryRowContext(ctx, getChatWorkspaceTTL)
var workspace_ttl string
err := row.Scan(&workspace_ttl)
return workspace_ttl, err
}
const getDERPMeshKey = `-- name: GetDERPMeshKey :one
SELECT value FROM site_configs WHERE key = 'derp_mesh_key'
`
func (q *sqlQuerier) GetDERPMeshKey(ctx context.Context) (string, error) {
row := q.db.QueryRowContext(ctx, getDERPMeshKey)
var value string
err := row.Scan(&value)
return value, err
}
const getDefaultProxyConfig = `-- name: GetDefaultProxyConfig :one
SELECT
COALESCE((SELECT value FROM site_configs WHERE key = 'default_proxy_display_name'), 'Default') :: text AS display_name,
COALESCE((SELECT value FROM site_configs WHERE key = 'default_proxy_icon_url'), '/emojis/1f3e1.png') :: text AS icon_url
`
type GetDefaultProxyConfigRow struct {
DisplayName string `db:"display_name" json:"display_name"`
IconURL string `db:"icon_url" json:"icon_url"`
}
func (q *sqlQuerier) GetDefaultProxyConfig(ctx context.Context) (GetDefaultProxyConfigRow, error) {
row := q.db.QueryRowContext(ctx, getDefaultProxyConfig)
var i GetDefaultProxyConfigRow
err := row.Scan(&i.DisplayName, &i.IconURL)
return i, err
}
const getDeploymentID = `-- name: GetDeploymentID :one
SELECT value FROM site_configs WHERE key = 'deployment_id'
`
func (q *sqlQuerier) GetDeploymentID(ctx context.Context) (string, error) {
row := q.db.QueryRowContext(ctx, getDeploymentID)
var value string
err := row.Scan(&value)
return value, err
}
const getHealthSettings = `-- name: GetHealthSettings :one
SELECT
COALESCE((SELECT value FROM site_configs WHERE key = 'health_settings'), '{}') :: text AS health_settings
`
func (q *sqlQuerier) GetHealthSettings(ctx context.Context) (string, error) {
row := q.db.QueryRowContext(ctx, getHealthSettings)
var health_settings string
err := row.Scan(&health_settings)
return health_settings, err
}
const getLastUpdateCheck = `-- name: GetLastUpdateCheck :one
SELECT value FROM site_configs WHERE key = 'last_update_check'
`
func (q *sqlQuerier) GetLastUpdateCheck(ctx context.Context) (string, error) {
row := q.db.QueryRowContext(ctx, getLastUpdateCheck)
var value string
err := row.Scan(&value)
return value, err
}
const getLogoURL = `-- name: GetLogoURL :one
SELECT value FROM site_configs WHERE key = 'logo_url'
`
func (q *sqlQuerier) GetLogoURL(ctx context.Context) (string, error) {
row := q.db.QueryRowContext(ctx, getLogoURL)
var value string
err := row.Scan(&value)
return value, err
}
const getNotificationsSettings = `-- name: GetNotificationsSettings :one
SELECT
COALESCE((SELECT value FROM site_configs WHERE key = 'notifications_settings'), '{}') :: text AS notifications_settings
`
func (q *sqlQuerier) GetNotificationsSettings(ctx context.Context) (string, error) {
row := q.db.QueryRowContext(ctx, getNotificationsSettings)
var notifications_settings string
err := row.Scan(&notifications_settings)
return notifications_settings, err
}
const getOAuth2DCREnabled = `-- name: GetOAuth2DCREnabled :one
SELECT COALESCE(
(SELECT value = 'true' FROM site_configs WHERE key = 'oauth2_dcr_enabled'),
false
)::bool
`
func (q *sqlQuerier) GetOAuth2DCREnabled(ctx context.Context) (bool, error) {
row := q.db.QueryRowContext(ctx, getOAuth2DCREnabled)
var column_1 bool
err := row.Scan(&column_1)
return column_1, err
}
const getOAuth2GithubDefaultEligible = `-- name: GetOAuth2GithubDefaultEligible :one
SELECT
CASE
WHEN value = 'true' THEN TRUE
ELSE FALSE
END
FROM site_configs
WHERE key = 'oauth2_github_default_eligible'
`
func (q *sqlQuerier) GetOAuth2GithubDefaultEligible(ctx context.Context) (bool, error) {
row := q.db.QueryRowContext(ctx, getOAuth2GithubDefaultEligible)
var column_1 bool
err := row.Scan(&column_1)
return column_1, err
}
const getPrebuildsSettings = `-- name: GetPrebuildsSettings :one
SELECT
COALESCE((SELECT value FROM site_configs WHERE key = 'prebuilds_settings'), '{}') :: text AS prebuilds_settings
`
func (q *sqlQuerier) GetPrebuildsSettings(ctx context.Context) (string, error) {
row := q.db.QueryRowContext(ctx, getPrebuildsSettings)
var prebuilds_settings string
err := row.Scan(&prebuilds_settings)
return prebuilds_settings, err
}
const getRuntimeConfig = `-- name: GetRuntimeConfig :one
SELECT value FROM site_configs WHERE site_configs.key = $1
`
func (q *sqlQuerier) GetRuntimeConfig(ctx context.Context, key string) (string, error) {
row := q.db.QueryRowContext(ctx, getRuntimeConfig, key)
var value string
err := row.Scan(&value)
return value, err
}
const getWebpushVAPIDKeys = `-- name: GetWebpushVAPIDKeys :one
SELECT
COALESCE((SELECT value FROM site_configs WHERE key = 'webpush_vapid_public_key'), '') :: text AS vapid_public_key,
COALESCE((SELECT value FROM site_configs WHERE key = 'webpush_vapid_private_key'), '') :: text AS vapid_private_key
`
type GetWebpushVAPIDKeysRow struct {
VapidPublicKey string `db:"vapid_public_key" json:"vapid_public_key"`
VapidPrivateKey string `db:"vapid_private_key" json:"vapid_private_key"`
}
func (q *sqlQuerier) GetWebpushVAPIDKeys(ctx context.Context) (GetWebpushVAPIDKeysRow, error) {
row := q.db.QueryRowContext(ctx, getWebpushVAPIDKeys)
var i GetWebpushVAPIDKeysRow
err := row.Scan(&i.VapidPublicKey, &i.VapidPrivateKey)
return i, err
}
const insertDERPMeshKey = `-- name: InsertDERPMeshKey :exec
INSERT INTO site_configs (key, value) VALUES ('derp_mesh_key', $1)
`
func (q *sqlQuerier) InsertDERPMeshKey(ctx context.Context, value string) error {
_, err := q.db.ExecContext(ctx, insertDERPMeshKey, value)
return err
}
const insertDeploymentID = `-- name: InsertDeploymentID :exec
INSERT INTO site_configs (key, value) VALUES ('deployment_id', $1)
`
func (q *sqlQuerier) InsertDeploymentID(ctx context.Context, value string) error {
_, err := q.db.ExecContext(ctx, insertDeploymentID, value)
return err
}
const upsertAnnouncementBanners = `-- name: UpsertAnnouncementBanners :exec
INSERT INTO site_configs (key, value) VALUES ('announcement_banners', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'announcement_banners'
`
func (q *sqlQuerier) UpsertAnnouncementBanners(ctx context.Context, value string) error {
_, err := q.db.ExecContext(ctx, upsertAnnouncementBanners, value)
return err
}
const upsertApplicationName = `-- name: UpsertApplicationName :exec
INSERT INTO site_configs (key, value) VALUES ('application_name', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'application_name'
`
func (q *sqlQuerier) UpsertApplicationName(ctx context.Context, value string) error {
_, err := q.db.ExecContext(ctx, upsertApplicationName, value)
return err
}
const upsertChatAdvisorConfig = `-- name: UpsertChatAdvisorConfig :exec
INSERT INTO site_configs (key, value) VALUES ('agents_advisor_config', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'agents_advisor_config'
`
// UpsertChatAdvisorConfig stores the deployment-wide runtime configuration
// for the experimental chat advisor. Callers marshal codersdk.AdvisorConfig
// to JSON before invoking this query.
func (q *sqlQuerier) UpsertChatAdvisorConfig(ctx context.Context, value string) error {
_, err := q.db.ExecContext(ctx, upsertChatAdvisorConfig, value)
return err
}
const upsertChatAutoArchiveDays = `-- name: UpsertChatAutoArchiveDays :exec
INSERT INTO site_configs (key, value)
VALUES ('agents_chat_auto_archive_days', CAST($1 AS integer)::text)
ON CONFLICT (key) DO UPDATE SET value = CAST($1 AS integer)::text
WHERE site_configs.key = 'agents_chat_auto_archive_days'
`
func (q *sqlQuerier) UpsertChatAutoArchiveDays(ctx context.Context, autoArchiveDays int32) error {
_, err := q.db.ExecContext(ctx, upsertChatAutoArchiveDays, autoArchiveDays)
return err
}
const upsertChatCompactionModelOverride = `-- name: UpsertChatCompactionModelOverride :exec
INSERT INTO site_configs (key, value) VALUES ('agents_chat_compaction_model_override', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'agents_chat_compaction_model_override'
`
func (q *sqlQuerier) UpsertChatCompactionModelOverride(ctx context.Context, value string) error {
_, err := q.db.ExecContext(ctx, upsertChatCompactionModelOverride, value)
return err
}
const upsertChatComputerUseProvider = `-- name: UpsertChatComputerUseProvider :exec
INSERT INTO site_configs (key, value) VALUES ('agents_computer_use_provider', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'agents_computer_use_provider'
`
func (q *sqlQuerier) UpsertChatComputerUseProvider(ctx context.Context, provider string) error {
_, err := q.db.ExecContext(ctx, upsertChatComputerUseProvider, provider)
return err
}
const upsertChatDebugLoggingAllowUsers = `-- name: UpsertChatDebugLoggingAllowUsers :exec
INSERT INTO site_configs (key, value)
VALUES (
'agents_chat_debug_logging_allow_users',
CASE
WHEN $1::bool THEN 'true'
ELSE 'false'
END
)
ON CONFLICT (key) DO UPDATE
SET value = CASE
WHEN $1::bool THEN 'true'
ELSE 'false'
END
WHERE site_configs.key = 'agents_chat_debug_logging_allow_users'
`
// UpsertChatDebugLoggingAllowUsers updates the runtime admin setting that
// allows users to opt into chat debug logging.
func (q *sqlQuerier) UpsertChatDebugLoggingAllowUsers(ctx context.Context, allowUsers bool) error {
_, err := q.db.ExecContext(ctx, upsertChatDebugLoggingAllowUsers, allowUsers)
return err
}
const upsertChatDebugRetentionDays = `-- name: UpsertChatDebugRetentionDays :exec
INSERT INTO site_configs (key, value)
VALUES ('agents_chat_debug_retention_days', CAST($1 AS integer)::text)
ON CONFLICT (key) DO UPDATE SET value = CAST($1 AS integer)::text
WHERE site_configs.key = 'agents_chat_debug_retention_days'
`
func (q *sqlQuerier) UpsertChatDebugRetentionDays(ctx context.Context, debugRetentionDays int32) error {
_, err := q.db.ExecContext(ctx, upsertChatDebugRetentionDays, debugRetentionDays)
return err
}
const upsertChatDesktopEnabled = `-- name: UpsertChatDesktopEnabled :exec
INSERT INTO site_configs (key, value)
VALUES (
'agents_desktop_enabled',
CASE
WHEN $1::bool THEN 'true'
ELSE 'false'
END
)
ON CONFLICT (key) DO UPDATE
SET value = CASE
WHEN $1::bool THEN 'true'
ELSE 'false'
END
WHERE site_configs.key = 'agents_desktop_enabled'
`
func (q *sqlQuerier) UpsertChatDesktopEnabled(ctx context.Context, enableDesktop bool) error {
_, err := q.db.ExecContext(ctx, upsertChatDesktopEnabled, enableDesktop)
return err
}
const upsertChatExploreModelOverride = `-- name: UpsertChatExploreModelOverride :exec
INSERT INTO site_configs (key, value) VALUES ('agents_chat_explore_model_override', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'agents_chat_explore_model_override'
`
func (q *sqlQuerier) UpsertChatExploreModelOverride(ctx context.Context, value string) error {
_, err := q.db.ExecContext(ctx, upsertChatExploreModelOverride, value)
return err
}
const upsertChatGeneralModelOverride = `-- name: UpsertChatGeneralModelOverride :exec
INSERT INTO site_configs (key, value) VALUES ('agents_chat_general_model_override', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'agents_chat_general_model_override'
`
func (q *sqlQuerier) UpsertChatGeneralModelOverride(ctx context.Context, value string) error {
_, err := q.db.ExecContext(ctx, upsertChatGeneralModelOverride, value)
return err
}
const upsertChatIncludeDefaultSystemPrompt = `-- name: UpsertChatIncludeDefaultSystemPrompt :exec
INSERT INTO site_configs (key, value)
VALUES (
'agents_chat_include_default_system_prompt',
CASE
WHEN $1::bool THEN 'true'
ELSE 'false'
END
)
ON CONFLICT (key) DO UPDATE
SET value = CASE
WHEN $1::bool THEN 'true'
ELSE 'false'
END
WHERE site_configs.key = 'agents_chat_include_default_system_prompt'
`
func (q *sqlQuerier) UpsertChatIncludeDefaultSystemPrompt(ctx context.Context, includeDefaultSystemPrompt bool) error {
_, err := q.db.ExecContext(ctx, upsertChatIncludeDefaultSystemPrompt, includeDefaultSystemPrompt)
return err
}
const upsertChatPersonalModelOverridesEnabled = `-- name: UpsertChatPersonalModelOverridesEnabled :exec
INSERT INTO site_configs (key, value)
VALUES (
'agents_chat_personal_model_overrides_enabled',
CASE
WHEN $1::bool THEN 'true'
ELSE 'false'
END
)
ON CONFLICT (key) DO UPDATE
SET value = CASE
WHEN $1::bool THEN 'true'
ELSE 'false'
END
WHERE site_configs.key = 'agents_chat_personal_model_overrides_enabled'
`
// UpsertChatPersonalModelOverridesEnabled updates whether users may configure
// personal chat model overrides.
func (q *sqlQuerier) UpsertChatPersonalModelOverridesEnabled(ctx context.Context, enabled bool) error {
_, err := q.db.ExecContext(ctx, upsertChatPersonalModelOverridesEnabled, enabled)
return err
}
const upsertChatPlanModeInstructions = `-- name: UpsertChatPlanModeInstructions :exec
INSERT INTO site_configs (key, value) VALUES ('agents_chat_plan_mode_instructions', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'agents_chat_plan_mode_instructions'
`
func (q *sqlQuerier) UpsertChatPlanModeInstructions(ctx context.Context, value string) error {
_, err := q.db.ExecContext(ctx, upsertChatPlanModeInstructions, value)
return err
}
const upsertChatRetentionDays = `-- name: UpsertChatRetentionDays :exec
INSERT INTO site_configs (key, value)
VALUES ('agents_chat_retention_days', CAST($1 AS integer)::text)
ON CONFLICT (key) DO UPDATE SET value = CAST($1 AS integer)::text
WHERE site_configs.key = 'agents_chat_retention_days'
`
func (q *sqlQuerier) UpsertChatRetentionDays(ctx context.Context, retentionDays int32) error {
_, err := q.db.ExecContext(ctx, upsertChatRetentionDays, retentionDays)
return err
}
const upsertChatSystemPrompt = `-- name: UpsertChatSystemPrompt :exec
INSERT INTO site_configs (key, value) VALUES ('agents_chat_system_prompt', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'agents_chat_system_prompt'
`
func (q *sqlQuerier) UpsertChatSystemPrompt(ctx context.Context, value string) error {
_, err := q.db.ExecContext(ctx, upsertChatSystemPrompt, value)
return err
}
const upsertChatTitleGenerationModelOverride = `-- name: UpsertChatTitleGenerationModelOverride :exec
INSERT INTO site_configs (key, value) VALUES ('agents_chat_title_generation_model_override', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'agents_chat_title_generation_model_override'
`
func (q *sqlQuerier) UpsertChatTitleGenerationModelOverride(ctx context.Context, value string) error {
_, err := q.db.ExecContext(ctx, upsertChatTitleGenerationModelOverride, value)
return err
}
const upsertChatWorkspaceTTL = `-- name: UpsertChatWorkspaceTTL :exec
INSERT INTO site_configs (key, value)
VALUES ('agents_workspace_ttl', $1::text)
ON CONFLICT (key) DO UPDATE
SET value = $1::text
WHERE site_configs.key = 'agents_workspace_ttl'
`
func (q *sqlQuerier) UpsertChatWorkspaceTTL(ctx context.Context, workspaceTtl string) error {
_, err := q.db.ExecContext(ctx, upsertChatWorkspaceTTL, workspaceTtl)
return err
}
const upsertDefaultProxy = `-- name: UpsertDefaultProxy :exec
INSERT INTO site_configs (key, value)
VALUES
('default_proxy_display_name', $1 :: text),
('default_proxy_icon_url', $2 :: text)
ON CONFLICT
(key)
DO UPDATE SET value = EXCLUDED.value WHERE site_configs.key = EXCLUDED.key
`
type UpsertDefaultProxyParams struct {
DisplayName string `db:"display_name" json:"display_name"`
IconURL string `db:"icon_url" json:"icon_url"`
}
// The default proxy is implied and not actually stored in the database.
// So we need to store it's configuration here for display purposes.
// The functional values are immutable and controlled implicitly.
func (q *sqlQuerier) UpsertDefaultProxy(ctx context.Context, arg UpsertDefaultProxyParams) error {
_, err := q.db.ExecContext(ctx, upsertDefaultProxy, arg.DisplayName, arg.IconURL)
return err
}
const upsertHealthSettings = `-- name: UpsertHealthSettings :exec
INSERT INTO site_configs (key, value) VALUES ('health_settings', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'health_settings'
`
func (q *sqlQuerier) UpsertHealthSettings(ctx context.Context, value string) error {
_, err := q.db.ExecContext(ctx, upsertHealthSettings, value)
return err
}
const upsertLastUpdateCheck = `-- name: UpsertLastUpdateCheck :exec
INSERT INTO site_configs (key, value) VALUES ('last_update_check', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'last_update_check'
`
func (q *sqlQuerier) UpsertLastUpdateCheck(ctx context.Context, value string) error {
_, err := q.db.ExecContext(ctx, upsertLastUpdateCheck, value)
return err
}
const upsertLogoURL = `-- name: UpsertLogoURL :exec
INSERT INTO site_configs (key, value) VALUES ('logo_url', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'logo_url'
`
func (q *sqlQuerier) UpsertLogoURL(ctx context.Context, value string) error {
_, err := q.db.ExecContext(ctx, upsertLogoURL, value)
return err
}
const upsertNotificationsSettings = `-- name: UpsertNotificationsSettings :exec
INSERT INTO site_configs (key, value) VALUES ('notifications_settings', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'notifications_settings'
`
func (q *sqlQuerier) UpsertNotificationsSettings(ctx context.Context, value string) error {
_, err := q.db.ExecContext(ctx, upsertNotificationsSettings, value)
return err
}
const upsertOAuth2DCREnabled = `-- name: UpsertOAuth2DCREnabled :exec
INSERT INTO site_configs (key, value)
VALUES (
'oauth2_dcr_enabled',
CASE
WHEN $1::bool THEN 'true'
ELSE 'false'
END
)
ON CONFLICT (key) DO UPDATE
SET value = CASE
WHEN $1::bool THEN 'true'
ELSE 'false'
END
WHERE site_configs.key = 'oauth2_dcr_enabled'
`
func (q *sqlQuerier) UpsertOAuth2DCREnabled(ctx context.Context, enabled bool) error {
_, err := q.db.ExecContext(ctx, upsertOAuth2DCREnabled, enabled)
return err
}
const upsertOAuth2GithubDefaultEligible = `-- name: UpsertOAuth2GithubDefaultEligible :exec
INSERT INTO site_configs (key, value)
VALUES (
'oauth2_github_default_eligible',
CASE
WHEN $1::bool THEN 'true'
ELSE 'false'
END
)
ON CONFLICT (key) DO UPDATE
SET value = CASE
WHEN $1::bool THEN 'true'
ELSE 'false'
END
WHERE site_configs.key = 'oauth2_github_default_eligible'
`
func (q *sqlQuerier) UpsertOAuth2GithubDefaultEligible(ctx context.Context, eligible bool) error {
_, err := q.db.ExecContext(ctx, upsertOAuth2GithubDefaultEligible, eligible)
return err
}
const upsertPrebuildsSettings = `-- name: UpsertPrebuildsSettings :exec
INSERT INTO site_configs (key, value) VALUES ('prebuilds_settings', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'prebuilds_settings'
`
func (q *sqlQuerier) UpsertPrebuildsSettings(ctx context.Context, value string) error {
_, err := q.db.ExecContext(ctx, upsertPrebuildsSettings, value)
return err
}
const upsertRuntimeConfig = `-- name: UpsertRuntimeConfig :exec
INSERT INTO site_configs (key, value) VALUES ($1, $2)
ON CONFLICT (key) DO UPDATE SET value = $2 WHERE site_configs.key = $1
`
type UpsertRuntimeConfigParams struct {
Key string `db:"key" json:"key"`
Value string `db:"value" json:"value"`
}
func (q *sqlQuerier) UpsertRuntimeConfig(ctx context.Context, arg UpsertRuntimeConfigParams) error {
_, err := q.db.ExecContext(ctx, upsertRuntimeConfig, arg.Key, arg.Value)
return err
}
const upsertWebpushVAPIDKeys = `-- name: UpsertWebpushVAPIDKeys :exec
INSERT INTO site_configs (key, value)
VALUES
('webpush_vapid_public_key', $1 :: text),
('webpush_vapid_private_key', $2 :: text)
ON CONFLICT (key)
DO UPDATE SET value = EXCLUDED.value WHERE site_configs.key = EXCLUDED.key
`
type UpsertWebpushVAPIDKeysParams struct {
VapidPublicKey string `db:"vapid_public_key" json:"vapid_public_key"`
VapidPrivateKey string `db:"vapid_private_key" json:"vapid_private_key"`
}
func (q *sqlQuerier) UpsertWebpushVAPIDKeys(ctx context.Context, arg UpsertWebpushVAPIDKeysParams) error {
_, err := q.db.ExecContext(ctx, upsertWebpushVAPIDKeys, arg.VapidPublicKey, arg.VapidPrivateKey)
return err
}
const cleanTailnetCoordinators = `-- name: CleanTailnetCoordinators :exec
DELETE
FROM tailnet_coordinators
WHERE heartbeat_at < now() - INTERVAL '24 HOURS'
`
func (q *sqlQuerier) CleanTailnetCoordinators(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, cleanTailnetCoordinators)
return err
}
const cleanTailnetLostPeers = `-- name: CleanTailnetLostPeers :exec
DELETE
FROM tailnet_peers
WHERE updated_at < now() - INTERVAL '24 HOURS' AND status = 'lost'::tailnet_status
`
func (q *sqlQuerier) CleanTailnetLostPeers(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, cleanTailnetLostPeers)
return err
}
const cleanTailnetTunnels = `-- name: CleanTailnetTunnels :exec
DELETE FROM tailnet_tunnels
WHERE updated_at < now() - INTERVAL '24 HOURS' AND
NOT EXISTS (
SELECT 1 FROM tailnet_peers
WHERE id = tailnet_tunnels.src_id AND coordinator_id = tailnet_tunnels.coordinator_id
)
`
func (q *sqlQuerier) CleanTailnetTunnels(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, cleanTailnetTunnels)
return err
}
const deleteAllTailnetTunnels = `-- name: DeleteAllTailnetTunnels :many
DELETE
FROM tailnet_tunnels
WHERE coordinator_id = $1 and src_id = $2
RETURNING src_id, dst_id
`
type DeleteAllTailnetTunnelsParams struct {
CoordinatorID uuid.UUID `db:"coordinator_id" json:"coordinator_id"`
SrcID uuid.UUID `db:"src_id" json:"src_id"`
}
type DeleteAllTailnetTunnelsRow struct {
SrcID uuid.UUID `db:"src_id" json:"src_id"`
DstID uuid.UUID `db:"dst_id" json:"dst_id"`
}
func (q *sqlQuerier) DeleteAllTailnetTunnels(ctx context.Context, arg DeleteAllTailnetTunnelsParams) ([]DeleteAllTailnetTunnelsRow, error) {
rows, err := q.db.QueryContext(ctx, deleteAllTailnetTunnels, arg.CoordinatorID, arg.SrcID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []DeleteAllTailnetTunnelsRow
for rows.Next() {
var i DeleteAllTailnetTunnelsRow
if err := rows.Scan(&i.SrcID, &i.DstID); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const deleteTailnetPeer = `-- name: DeleteTailnetPeer :one
DELETE
FROM tailnet_peers
WHERE id = $1 and coordinator_id = $2
RETURNING id, coordinator_id
`
type DeleteTailnetPeerParams struct {
ID uuid.UUID `db:"id" json:"id"`
CoordinatorID uuid.UUID `db:"coordinator_id" json:"coordinator_id"`
}
type DeleteTailnetPeerRow struct {
ID uuid.UUID `db:"id" json:"id"`
CoordinatorID uuid.UUID `db:"coordinator_id" json:"coordinator_id"`
}
func (q *sqlQuerier) DeleteTailnetPeer(ctx context.Context, arg DeleteTailnetPeerParams) (DeleteTailnetPeerRow, error) {
row := q.db.QueryRowContext(ctx, deleteTailnetPeer, arg.ID, arg.CoordinatorID)
var i DeleteTailnetPeerRow
err := row.Scan(&i.ID, &i.CoordinatorID)
return i, err
}
const deleteTailnetTunnel = `-- name: DeleteTailnetTunnel :one
DELETE
FROM tailnet_tunnels
WHERE coordinator_id = $1 and src_id = $2 and dst_id = $3
RETURNING coordinator_id, src_id, dst_id
`
type DeleteTailnetTunnelParams struct {
CoordinatorID uuid.UUID `db:"coordinator_id" json:"coordinator_id"`
SrcID uuid.UUID `db:"src_id" json:"src_id"`
DstID uuid.UUID `db:"dst_id" json:"dst_id"`
}
type DeleteTailnetTunnelRow struct {
CoordinatorID uuid.UUID `db:"coordinator_id" json:"coordinator_id"`
SrcID uuid.UUID `db:"src_id" json:"src_id"`
DstID uuid.UUID `db:"dst_id" json:"dst_id"`
}
func (q *sqlQuerier) DeleteTailnetTunnel(ctx context.Context, arg DeleteTailnetTunnelParams) (DeleteTailnetTunnelRow, error) {
row := q.db.QueryRowContext(ctx, deleteTailnetTunnel, arg.CoordinatorID, arg.SrcID, arg.DstID)
var i DeleteTailnetTunnelRow
err := row.Scan(&i.CoordinatorID, &i.SrcID, &i.DstID)
return i, err
}
const getAllTailnetCoordinators = `-- name: GetAllTailnetCoordinators :many
SELECT id, heartbeat_at FROM tailnet_coordinators
`
// For PG Coordinator HTMLDebug
func (q *sqlQuerier) GetAllTailnetCoordinators(ctx context.Context) ([]TailnetCoordinator, error) {
rows, err := q.db.QueryContext(ctx, getAllTailnetCoordinators)
if err != nil {
return nil, err
}
defer rows.Close()
var items []TailnetCoordinator
for rows.Next() {
var i TailnetCoordinator
if err := rows.Scan(&i.ID, &i.HeartbeatAt); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getAllTailnetPeers = `-- name: GetAllTailnetPeers :many
SELECT id, coordinator_id, updated_at, node, status FROM tailnet_peers
`
func (q *sqlQuerier) GetAllTailnetPeers(ctx context.Context) ([]TailnetPeer, error) {
rows, err := q.db.QueryContext(ctx, getAllTailnetPeers)
if err != nil {
return nil, err
}
defer rows.Close()
var items []TailnetPeer
for rows.Next() {
var i TailnetPeer
if err := rows.Scan(
&i.ID,
&i.CoordinatorID,
&i.UpdatedAt,
&i.Node,
&i.Status,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getAllTailnetTunnels = `-- name: GetAllTailnetTunnels :many
SELECT coordinator_id, src_id, dst_id, updated_at FROM tailnet_tunnels
`
func (q *sqlQuerier) GetAllTailnetTunnels(ctx context.Context) ([]TailnetTunnel, error) {
rows, err := q.db.QueryContext(ctx, getAllTailnetTunnels)
if err != nil {
return nil, err
}
defer rows.Close()
var items []TailnetTunnel
for rows.Next() {
var i TailnetTunnel
if err := rows.Scan(
&i.CoordinatorID,
&i.SrcID,
&i.DstID,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getTailnetPeers = `-- name: GetTailnetPeers :many
SELECT id, coordinator_id, updated_at, node, status FROM tailnet_peers WHERE id = $1
`
func (q *sqlQuerier) GetTailnetPeers(ctx context.Context, id uuid.UUID) ([]TailnetPeer, error) {
rows, err := q.db.QueryContext(ctx, getTailnetPeers, id)
if err != nil {
return nil, err
}
defer rows.Close()
var items []TailnetPeer
for rows.Next() {
var i TailnetPeer
if err := rows.Scan(
&i.ID,
&i.CoordinatorID,
&i.UpdatedAt,
&i.Node,
&i.Status,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getTailnetTunnelPeerBindingsBatch = `-- name: GetTailnetTunnelPeerBindingsBatch :many
SELECT tp.id AS peer_id, tp.coordinator_id, tp.updated_at, tp.node, tp.status,
tunnels.lookup_id
FROM (
SELECT dst_id AS peer_id, src_id AS lookup_id
FROM tailnet_tunnels WHERE src_id = ANY($1 :: uuid[])
UNION
SELECT src_id AS peer_id, dst_id AS lookup_id
FROM tailnet_tunnels WHERE dst_id = ANY($1 :: uuid[])
) tunnels
INNER JOIN tailnet_peers tp ON tp.id = tunnels.peer_id
`
type GetTailnetTunnelPeerBindingsBatchRow struct {
PeerID uuid.UUID `db:"peer_id" json:"peer_id"`
CoordinatorID uuid.UUID `db:"coordinator_id" json:"coordinator_id"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Node []byte `db:"node" json:"node"`
Status TailnetStatus `db:"status" json:"status"`
LookupID uuid.UUID `db:"lookup_id" json:"lookup_id"`
}
func (q *sqlQuerier) GetTailnetTunnelPeerBindingsBatch(ctx context.Context, ids []uuid.UUID) ([]GetTailnetTunnelPeerBindingsBatchRow, error) {
rows, err := q.db.QueryContext(ctx, getTailnetTunnelPeerBindingsBatch, pq.Array(ids))
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetTailnetTunnelPeerBindingsBatchRow
for rows.Next() {
var i GetTailnetTunnelPeerBindingsBatchRow
if err := rows.Scan(
&i.PeerID,
&i.CoordinatorID,
&i.UpdatedAt,
&i.Node,
&i.Status,
&i.LookupID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getTailnetTunnelPeerIDsBatch = `-- name: GetTailnetTunnelPeerIDsBatch :many
SELECT src_id AS lookup_id, dst_id AS peer_id, coordinator_id, updated_at
FROM tailnet_tunnels WHERE src_id = ANY($1 :: uuid[])
UNION ALL
SELECT dst_id AS lookup_id, src_id AS peer_id, coordinator_id, updated_at
FROM tailnet_tunnels WHERE dst_id = ANY($1 :: uuid[])
`
type GetTailnetTunnelPeerIDsBatchRow struct {
LookupID uuid.UUID `db:"lookup_id" json:"lookup_id"`
PeerID uuid.UUID `db:"peer_id" json:"peer_id"`
CoordinatorID uuid.UUID `db:"coordinator_id" json:"coordinator_id"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
func (q *sqlQuerier) GetTailnetTunnelPeerIDsBatch(ctx context.Context, ids []uuid.UUID) ([]GetTailnetTunnelPeerIDsBatchRow, error) {
rows, err := q.db.QueryContext(ctx, getTailnetTunnelPeerIDsBatch, pq.Array(ids))
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetTailnetTunnelPeerIDsBatchRow
for rows.Next() {
var i GetTailnetTunnelPeerIDsBatchRow
if err := rows.Scan(
&i.LookupID,
&i.PeerID,
&i.CoordinatorID,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateTailnetPeerStatusByCoordinator = `-- name: UpdateTailnetPeerStatusByCoordinator :many
UPDATE
tailnet_peers
SET
status = $2
WHERE
coordinator_id = $1
RETURNING id
`
type UpdateTailnetPeerStatusByCoordinatorParams struct {
CoordinatorID uuid.UUID `db:"coordinator_id" json:"coordinator_id"`
Status TailnetStatus `db:"status" json:"status"`
}
func (q *sqlQuerier) UpdateTailnetPeerStatusByCoordinator(ctx context.Context, arg UpdateTailnetPeerStatusByCoordinatorParams) ([]uuid.UUID, error) {
rows, err := q.db.QueryContext(ctx, updateTailnetPeerStatusByCoordinator, arg.CoordinatorID, arg.Status)
if err != nil {
return nil, err
}
defer rows.Close()
var items []uuid.UUID
for rows.Next() {
var id uuid.UUID
if err := rows.Scan(&id); err != nil {
return nil, err
}
items = append(items, id)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const upsertTailnetCoordinator = `-- name: UpsertTailnetCoordinator :one
INSERT INTO
tailnet_coordinators (
id,
heartbeat_at
)
VALUES
($1, now() at time zone 'utc')
ON CONFLICT (id)
DO UPDATE SET
id = $1,
heartbeat_at = now() at time zone 'utc'
RETURNING id, heartbeat_at
`
func (q *sqlQuerier) UpsertTailnetCoordinator(ctx context.Context, id uuid.UUID) (TailnetCoordinator, error) {
row := q.db.QueryRowContext(ctx, upsertTailnetCoordinator, id)
var i TailnetCoordinator
err := row.Scan(&i.ID, &i.HeartbeatAt)
return i, err
}
const upsertTailnetPeer = `-- name: UpsertTailnetPeer :one
INSERT INTO
tailnet_peers (
id,
coordinator_id,
node,
status,
updated_at
)
VALUES
($1, $2, $3, $4, now() at time zone 'utc')
ON CONFLICT (id, coordinator_id)
DO UPDATE SET
id = $1,
coordinator_id = $2,
node = $3,
status = $4,
updated_at = now() at time zone 'utc'
RETURNING id, coordinator_id, updated_at, node, status
`
type UpsertTailnetPeerParams struct {
ID uuid.UUID `db:"id" json:"id"`
CoordinatorID uuid.UUID `db:"coordinator_id" json:"coordinator_id"`
Node []byte `db:"node" json:"node"`
Status TailnetStatus `db:"status" json:"status"`
}
func (q *sqlQuerier) UpsertTailnetPeer(ctx context.Context, arg UpsertTailnetPeerParams) (TailnetPeer, error) {
row := q.db.QueryRowContext(ctx, upsertTailnetPeer,
arg.ID,
arg.CoordinatorID,
arg.Node,
arg.Status,
)
var i TailnetPeer
err := row.Scan(
&i.ID,
&i.CoordinatorID,
&i.UpdatedAt,
&i.Node,
&i.Status,
)
return i, err
}
const upsertTailnetTunnel = `-- name: UpsertTailnetTunnel :one
INSERT INTO
tailnet_tunnels (
coordinator_id,
src_id,
dst_id,
updated_at
)
VALUES
($1, $2, $3, now() at time zone 'utc')
ON CONFLICT (coordinator_id, src_id, dst_id)
DO UPDATE SET
coordinator_id = $1,
src_id = $2,
dst_id = $3,
updated_at = now() at time zone 'utc'
RETURNING coordinator_id, src_id, dst_id, updated_at
`
type UpsertTailnetTunnelParams struct {
CoordinatorID uuid.UUID `db:"coordinator_id" json:"coordinator_id"`
SrcID uuid.UUID `db:"src_id" json:"src_id"`
DstID uuid.UUID `db:"dst_id" json:"dst_id"`
}
func (q *sqlQuerier) UpsertTailnetTunnel(ctx context.Context, arg UpsertTailnetTunnelParams) (TailnetTunnel, error) {
row := q.db.QueryRowContext(ctx, upsertTailnetTunnel, arg.CoordinatorID, arg.SrcID, arg.DstID)
var i TailnetTunnel
err := row.Scan(
&i.CoordinatorID,
&i.SrcID,
&i.DstID,
&i.UpdatedAt,
)
return i, err
}
const deleteTask = `-- name: DeleteTask :one
WITH deleted_task AS (
UPDATE tasks
SET
deleted_at = $1::timestamptz
WHERE
id = $2::uuid
AND deleted_at IS NULL
RETURNING id
), deleted_snapshot AS (
DELETE FROM task_snapshots
WHERE task_id = $2::uuid
)
SELECT id FROM deleted_task
`
type DeleteTaskParams struct {
DeletedAt time.Time `db:"deleted_at" json:"deleted_at"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) DeleteTask(ctx context.Context, arg DeleteTaskParams) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, deleteTask, arg.DeletedAt, arg.ID)
var id uuid.UUID
err := row.Scan(&id)
return id, err
}
const getTaskByID = `-- name: GetTaskByID :one
SELECT id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, display_name, workspace_group_acl, workspace_user_acl, status, status_debug, workspace_build_number, workspace_agent_id, workspace_app_id, workspace_agent_lifecycle_state, workspace_app_health, owner_username, owner_name, owner_avatar_url FROM tasks_with_status WHERE id = $1::uuid
`
func (q *sqlQuerier) GetTaskByID(ctx context.Context, id uuid.UUID) (Task, error) {
row := q.db.QueryRowContext(ctx, getTaskByID, id)
var i Task
err := row.Scan(
&i.ID,
&i.OrganizationID,
&i.OwnerID,
&i.Name,
&i.WorkspaceID,
&i.TemplateVersionID,
&i.TemplateParameters,
&i.Prompt,
&i.CreatedAt,
&i.DeletedAt,
&i.DisplayName,
&i.WorkspaceGroupACL,
&i.WorkspaceUserACL,
&i.Status,
&i.StatusDebug,
&i.WorkspaceBuildNumber,
&i.WorkspaceAgentID,
&i.WorkspaceAppID,
&i.WorkspaceAgentLifecycleState,
&i.WorkspaceAppHealth,
&i.OwnerUsername,
&i.OwnerName,
&i.OwnerAvatarUrl,
)
return i, err
}
const getTaskByOwnerIDAndName = `-- name: GetTaskByOwnerIDAndName :one
SELECT id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, display_name, workspace_group_acl, workspace_user_acl, status, status_debug, workspace_build_number, workspace_agent_id, workspace_app_id, workspace_agent_lifecycle_state, workspace_app_health, owner_username, owner_name, owner_avatar_url FROM tasks_with_status
WHERE
owner_id = $1::uuid
AND deleted_at IS NULL
AND LOWER(name) = LOWER($2::text)
`
type GetTaskByOwnerIDAndNameParams struct {
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
Name string `db:"name" json:"name"`
}
func (q *sqlQuerier) GetTaskByOwnerIDAndName(ctx context.Context, arg GetTaskByOwnerIDAndNameParams) (Task, error) {
row := q.db.QueryRowContext(ctx, getTaskByOwnerIDAndName, arg.OwnerID, arg.Name)
var i Task
err := row.Scan(
&i.ID,
&i.OrganizationID,
&i.OwnerID,
&i.Name,
&i.WorkspaceID,
&i.TemplateVersionID,
&i.TemplateParameters,
&i.Prompt,
&i.CreatedAt,
&i.DeletedAt,
&i.DisplayName,
&i.WorkspaceGroupACL,
&i.WorkspaceUserACL,
&i.Status,
&i.StatusDebug,
&i.WorkspaceBuildNumber,
&i.WorkspaceAgentID,
&i.WorkspaceAppID,
&i.WorkspaceAgentLifecycleState,
&i.WorkspaceAppHealth,
&i.OwnerUsername,
&i.OwnerName,
&i.OwnerAvatarUrl,
)
return i, err
}
const getTaskByWorkspaceID = `-- name: GetTaskByWorkspaceID :one
SELECT id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, display_name, workspace_group_acl, workspace_user_acl, status, status_debug, workspace_build_number, workspace_agent_id, workspace_app_id, workspace_agent_lifecycle_state, workspace_app_health, owner_username, owner_name, owner_avatar_url FROM tasks_with_status WHERE workspace_id = $1::uuid
`
func (q *sqlQuerier) GetTaskByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (Task, error) {
row := q.db.QueryRowContext(ctx, getTaskByWorkspaceID, workspaceID)
var i Task
err := row.Scan(
&i.ID,
&i.OrganizationID,
&i.OwnerID,
&i.Name,
&i.WorkspaceID,
&i.TemplateVersionID,
&i.TemplateParameters,
&i.Prompt,
&i.CreatedAt,
&i.DeletedAt,
&i.DisplayName,
&i.WorkspaceGroupACL,
&i.WorkspaceUserACL,
&i.Status,
&i.StatusDebug,
&i.WorkspaceBuildNumber,
&i.WorkspaceAgentID,
&i.WorkspaceAppID,
&i.WorkspaceAgentLifecycleState,
&i.WorkspaceAppHealth,
&i.OwnerUsername,
&i.OwnerName,
&i.OwnerAvatarUrl,
)
return i, err
}
const getTaskSnapshot = `-- name: GetTaskSnapshot :one
SELECT
task_id, log_snapshot, log_snapshot_created_at
FROM
task_snapshots
WHERE
task_id = $1
`
func (q *sqlQuerier) GetTaskSnapshot(ctx context.Context, taskID uuid.UUID) (TaskSnapshot, error) {
row := q.db.QueryRowContext(ctx, getTaskSnapshot, taskID)
var i TaskSnapshot
err := row.Scan(&i.TaskID, &i.LogSnapshot, &i.LogSnapshotCreatedAt)
return i, err
}
const getTelemetryTaskEvents = `-- name: GetTelemetryTaskEvents :many
WITH task_app_ids AS (
SELECT task_id, workspace_app_id
FROM task_workspace_apps
),
task_status_timeline AS (
-- All app statuses across every historical app for each task,
-- plus synthetic "boundary" rows at each stop/start build transition.
-- This allows us to correctly take gaps due to pause/resume into account.
SELECT tai.task_id, was.created_at, was.state::text AS state
FROM workspace_app_statuses was
JOIN task_app_ids tai ON tai.workspace_app_id = was.app_id
UNION ALL
SELECT t.id AS task_id, wb.created_at, '_boundary' AS state
FROM tasks t
JOIN workspace_builds wb ON wb.workspace_id = t.workspace_id
WHERE t.deleted_at IS NULL
AND t.workspace_id IS NOT NULL
AND wb.build_number > 1
),
task_event_data AS (
SELECT
t.id AS task_id,
t.workspace_id,
twa.workspace_app_id,
-- Latest stop build.
stop_build.created_at AS stop_build_created_at,
stop_build.reason AS stop_build_reason,
-- Latest start build (task_resume only).
start_build.created_at AS start_build_created_at,
start_build.reason AS start_build_reason,
start_build.build_number AS start_build_number,
-- Last "working" app status (for idle duration).
lws.created_at AS last_working_status_at,
-- First app status after resume (for resume-to-status duration).
-- Only populated for workspaces in an active phase (started more
-- recently than stopped).
fsar.created_at AS first_status_after_resume_at,
-- Cumulative time spent in "working" state.
active_dur.total_working_ms AS active_duration_ms
FROM tasks t
LEFT JOIN LATERAL (
SELECT task_app.workspace_app_id
FROM task_workspace_apps task_app
WHERE task_app.task_id = t.id
ORDER BY task_app.workspace_build_number DESC
LIMIT 1
) twa ON TRUE
LEFT JOIN LATERAL (
SELECT wb.created_at, wb.reason, wb.build_number
FROM workspace_builds wb
WHERE wb.workspace_id = t.workspace_id
AND wb.transition = 'stop'
ORDER BY wb.build_number DESC
LIMIT 1
) stop_build ON TRUE
LEFT JOIN LATERAL (
SELECT wb.created_at, wb.reason, wb.build_number
FROM workspace_builds wb
WHERE wb.workspace_id = t.workspace_id
AND wb.transition = 'start'
ORDER BY wb.build_number DESC
LIMIT 1
) start_build ON TRUE
LEFT JOIN LATERAL (
SELECT tst.created_at
FROM task_status_timeline tst
WHERE tst.task_id = t.id
AND tst.state = 'working'
-- Only consider status before the latest pause so that
-- post-resume statuses don't mask pre-pause idle time.
AND (stop_build.created_at IS NULL
OR tst.created_at <= stop_build.created_at)
ORDER BY tst.created_at DESC
LIMIT 1
) lws ON TRUE
LEFT JOIN LATERAL (
SELECT was.created_at
FROM workspace_app_statuses was
WHERE was.app_id = twa.workspace_app_id
AND was.created_at > start_build.created_at
ORDER BY was.created_at ASC
LIMIT 1
) fsar ON twa.workspace_app_id IS NOT NULL
AND start_build.created_at IS NOT NULL
AND (stop_build.created_at IS NULL
OR start_build.created_at > stop_build.created_at)
-- Active duration: cumulative time spent in "working" state across all
-- historical app IDs for this task. Uses LEAD() to convert point-in-time
-- statuses into intervals, then sums intervals where state='working'. For
-- the last status, falls back to stop_build time (if paused) or @now (if
-- still running).
LEFT JOIN LATERAL (
SELECT COALESCE(
SUM(EXTRACT(EPOCH FROM (interval_end - interval_start)) * 1000)::bigint,
0
)::bigint AS total_working_ms
FROM (
SELECT
tst.created_at AS interval_start,
COALESCE(
LEAD(tst.created_at) OVER (ORDER BY tst.created_at ASC, CASE WHEN tst.state = '_boundary' THEN 1 ELSE 0 END ASC),
CASE WHEN stop_build.created_at IS NOT NULL
AND (start_build.created_at IS NULL
OR stop_build.created_at > start_build.created_at)
THEN stop_build.created_at
ELSE $1::timestamptz
END
) AS interval_end,
tst.state
FROM task_status_timeline tst
WHERE tst.task_id = t.id
) intervals
WHERE intervals.state = 'working'
) active_dur ON TRUE
WHERE t.deleted_at IS NULL
AND t.workspace_id IS NOT NULL
AND EXISTS (
SELECT 1 FROM workspace_builds wb
WHERE wb.workspace_id = t.workspace_id
AND wb.created_at > $2
)
)
SELECT task_id, workspace_id, workspace_app_id, stop_build_created_at, stop_build_reason, start_build_created_at, start_build_reason, start_build_number, last_working_status_at, first_status_after_resume_at, active_duration_ms FROM task_event_data
ORDER BY task_id
`
type GetTelemetryTaskEventsParams struct {
Now time.Time `db:"now" json:"now"`
CreatedAfter time.Time `db:"created_after" json:"created_after"`
}
type GetTelemetryTaskEventsRow struct {
TaskID uuid.UUID `db:"task_id" json:"task_id"`
WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"`
WorkspaceAppID uuid.NullUUID `db:"workspace_app_id" json:"workspace_app_id"`
StopBuildCreatedAt sql.NullTime `db:"stop_build_created_at" json:"stop_build_created_at"`
StopBuildReason NullBuildReason `db:"stop_build_reason" json:"stop_build_reason"`
StartBuildCreatedAt sql.NullTime `db:"start_build_created_at" json:"start_build_created_at"`
StartBuildReason NullBuildReason `db:"start_build_reason" json:"start_build_reason"`
StartBuildNumber sql.NullInt32 `db:"start_build_number" json:"start_build_number"`
LastWorkingStatusAt sql.NullTime `db:"last_working_status_at" json:"last_working_status_at"`
FirstStatusAfterResumeAt sql.NullTime `db:"first_status_after_resume_at" json:"first_status_after_resume_at"`
ActiveDurationMs int64 `db:"active_duration_ms" json:"active_duration_ms"`
}
// Returns all data needed to build task lifecycle events for telemetry
// in a single round-trip. For each task whose workspace is in the
// given set, fetches:
// - the latest workspace app binding (task_workspace_apps)
// - the most recent stop and start builds (workspace_builds)
// - the last "working" app status (workspace_app_statuses)
// - the first app status after resume, for active workspaces
//
// Assumptions:
// - 1:1 relationship between tasks and workspaces. All builds on the
// workspace are considered task-related.
// - Idle duration approximation: If the agent reports "working", does
// work, then reports "done", we miss that working time.
// - lws and active_dur join across all historical app IDs for the task,
// because each resume cycle provisions a new app ID. This ensures
// pre-pause statuses contribute to idle duration and active duration.
func (q *sqlQuerier) GetTelemetryTaskEvents(ctx context.Context, arg GetTelemetryTaskEventsParams) ([]GetTelemetryTaskEventsRow, error) {
rows, err := q.db.QueryContext(ctx, getTelemetryTaskEvents, arg.Now, arg.CreatedAfter)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetTelemetryTaskEventsRow
for rows.Next() {
var i GetTelemetryTaskEventsRow
if err := rows.Scan(
&i.TaskID,
&i.WorkspaceID,
&i.WorkspaceAppID,
&i.StopBuildCreatedAt,
&i.StopBuildReason,
&i.StartBuildCreatedAt,
&i.StartBuildReason,
&i.StartBuildNumber,
&i.LastWorkingStatusAt,
&i.FirstStatusAfterResumeAt,
&i.ActiveDurationMs,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertTask = `-- name: InsertTask :one
INSERT INTO tasks
(id, organization_id, owner_id, name, display_name, workspace_id, template_version_id, template_parameters, prompt, created_at)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, display_name
`
type InsertTaskParams struct {
ID uuid.UUID `db:"id" json:"id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
Name string `db:"name" json:"name"`
DisplayName string `db:"display_name" json:"display_name"`
WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"`
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
TemplateParameters json.RawMessage `db:"template_parameters" json:"template_parameters"`
Prompt string `db:"prompt" json:"prompt"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
func (q *sqlQuerier) InsertTask(ctx context.Context, arg InsertTaskParams) (TaskTable, error) {
row := q.db.QueryRowContext(ctx, insertTask,
arg.ID,
arg.OrganizationID,
arg.OwnerID,
arg.Name,
arg.DisplayName,
arg.WorkspaceID,
arg.TemplateVersionID,
arg.TemplateParameters,
arg.Prompt,
arg.CreatedAt,
)
var i TaskTable
err := row.Scan(
&i.ID,
&i.OrganizationID,
&i.OwnerID,
&i.Name,
&i.WorkspaceID,
&i.TemplateVersionID,
&i.TemplateParameters,
&i.Prompt,
&i.CreatedAt,
&i.DeletedAt,
&i.DisplayName,
)
return i, err
}
const listTasks = `-- name: ListTasks :many
SELECT id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, display_name, workspace_group_acl, workspace_user_acl, status, status_debug, workspace_build_number, workspace_agent_id, workspace_app_id, workspace_agent_lifecycle_state, workspace_app_health, owner_username, owner_name, owner_avatar_url FROM tasks_with_status tws
WHERE tws.deleted_at IS NULL
AND CASE WHEN $1::UUID != '00000000-0000-0000-0000-000000000000' THEN tws.owner_id = $1::UUID ELSE TRUE END
AND CASE WHEN $2::UUID != '00000000-0000-0000-0000-000000000000' THEN tws.organization_id = $2::UUID ELSE TRUE END
AND CASE WHEN $3::text != '' THEN tws.status = $3::task_status ELSE TRUE END
ORDER BY tws.created_at DESC
`
type ListTasksParams struct {
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
Status string `db:"status" json:"status"`
}
func (q *sqlQuerier) ListTasks(ctx context.Context, arg ListTasksParams) ([]Task, error) {
rows, err := q.db.QueryContext(ctx, listTasks, arg.OwnerID, arg.OrganizationID, arg.Status)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Task
for rows.Next() {
var i Task
if err := rows.Scan(
&i.ID,
&i.OrganizationID,
&i.OwnerID,
&i.Name,
&i.WorkspaceID,
&i.TemplateVersionID,
&i.TemplateParameters,
&i.Prompt,
&i.CreatedAt,
&i.DeletedAt,
&i.DisplayName,
&i.WorkspaceGroupACL,
&i.WorkspaceUserACL,
&i.Status,
&i.StatusDebug,
&i.WorkspaceBuildNumber,
&i.WorkspaceAgentID,
&i.WorkspaceAppID,
&i.WorkspaceAgentLifecycleState,
&i.WorkspaceAppHealth,
&i.OwnerUsername,
&i.OwnerName,
&i.OwnerAvatarUrl,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateTaskPrompt = `-- name: UpdateTaskPrompt :one
UPDATE
tasks
SET
prompt = $1::text
WHERE
id = $2::uuid
AND deleted_at IS NULL
RETURNING id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, display_name
`
type UpdateTaskPromptParams struct {
Prompt string `db:"prompt" json:"prompt"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateTaskPrompt(ctx context.Context, arg UpdateTaskPromptParams) (TaskTable, error) {
row := q.db.QueryRowContext(ctx, updateTaskPrompt, arg.Prompt, arg.ID)
var i TaskTable
err := row.Scan(
&i.ID,
&i.OrganizationID,
&i.OwnerID,
&i.Name,
&i.WorkspaceID,
&i.TemplateVersionID,
&i.TemplateParameters,
&i.Prompt,
&i.CreatedAt,
&i.DeletedAt,
&i.DisplayName,
)
return i, err
}
const updateTaskWorkspaceID = `-- name: UpdateTaskWorkspaceID :one
UPDATE
tasks
SET
workspace_id = $2
FROM
workspaces w
JOIN
template_versions tv
ON
tv.template_id = w.template_id
WHERE
tasks.id = $1
AND tasks.workspace_id IS NULL
AND w.id = $2
AND tv.id = tasks.template_version_id
RETURNING
tasks.id, tasks.organization_id, tasks.owner_id, tasks.name, tasks.workspace_id, tasks.template_version_id, tasks.template_parameters, tasks.prompt, tasks.created_at, tasks.deleted_at, tasks.display_name
`
type UpdateTaskWorkspaceIDParams struct {
ID uuid.UUID `db:"id" json:"id"`
WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"`
}
func (q *sqlQuerier) UpdateTaskWorkspaceID(ctx context.Context, arg UpdateTaskWorkspaceIDParams) (TaskTable, error) {
row := q.db.QueryRowContext(ctx, updateTaskWorkspaceID, arg.ID, arg.WorkspaceID)
var i TaskTable
err := row.Scan(
&i.ID,
&i.OrganizationID,
&i.OwnerID,
&i.Name,
&i.WorkspaceID,
&i.TemplateVersionID,
&i.TemplateParameters,
&i.Prompt,
&i.CreatedAt,
&i.DeletedAt,
&i.DisplayName,
)
return i, err
}
const upsertTaskSnapshot = `-- name: UpsertTaskSnapshot :exec
INSERT INTO
task_snapshots (task_id, log_snapshot, log_snapshot_created_at)
VALUES
($1, $2, $3)
ON CONFLICT
(task_id)
DO UPDATE SET
log_snapshot = EXCLUDED.log_snapshot,
log_snapshot_created_at = EXCLUDED.log_snapshot_created_at
`
type UpsertTaskSnapshotParams struct {
TaskID uuid.UUID `db:"task_id" json:"task_id"`
LogSnapshot json.RawMessage `db:"log_snapshot" json:"log_snapshot"`
LogSnapshotCreatedAt time.Time `db:"log_snapshot_created_at" json:"log_snapshot_created_at"`
}
func (q *sqlQuerier) UpsertTaskSnapshot(ctx context.Context, arg UpsertTaskSnapshotParams) error {
_, err := q.db.ExecContext(ctx, upsertTaskSnapshot, arg.TaskID, arg.LogSnapshot, arg.LogSnapshotCreatedAt)
return err
}
const upsertTaskWorkspaceApp = `-- name: UpsertTaskWorkspaceApp :one
INSERT INTO task_workspace_apps
(task_id, workspace_build_number, workspace_agent_id, workspace_app_id)
VALUES
($1, $2, $3, $4)
ON CONFLICT (task_id, workspace_build_number)
DO UPDATE SET
workspace_agent_id = EXCLUDED.workspace_agent_id,
workspace_app_id = EXCLUDED.workspace_app_id
RETURNING task_id, workspace_agent_id, workspace_app_id, workspace_build_number
`
type UpsertTaskWorkspaceAppParams struct {
TaskID uuid.UUID `db:"task_id" json:"task_id"`
WorkspaceBuildNumber int32 `db:"workspace_build_number" json:"workspace_build_number"`
WorkspaceAgentID uuid.NullUUID `db:"workspace_agent_id" json:"workspace_agent_id"`
WorkspaceAppID uuid.NullUUID `db:"workspace_app_id" json:"workspace_app_id"`
}
func (q *sqlQuerier) UpsertTaskWorkspaceApp(ctx context.Context, arg UpsertTaskWorkspaceAppParams) (TaskWorkspaceApp, error) {
row := q.db.QueryRowContext(ctx, upsertTaskWorkspaceApp,
arg.TaskID,
arg.WorkspaceBuildNumber,
arg.WorkspaceAgentID,
arg.WorkspaceAppID,
)
var i TaskWorkspaceApp
err := row.Scan(
&i.TaskID,
&i.WorkspaceAgentID,
&i.WorkspaceAppID,
&i.WorkspaceBuildNumber,
)
return i, err
}
const getTelemetryItem = `-- name: GetTelemetryItem :one
SELECT key, value, created_at, updated_at FROM telemetry_items WHERE key = $1
`
func (q *sqlQuerier) GetTelemetryItem(ctx context.Context, key string) (TelemetryItem, error) {
row := q.db.QueryRowContext(ctx, getTelemetryItem, key)
var i TelemetryItem
err := row.Scan(
&i.Key,
&i.Value,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getTelemetryItems = `-- name: GetTelemetryItems :many
SELECT key, value, created_at, updated_at FROM telemetry_items
`
func (q *sqlQuerier) GetTelemetryItems(ctx context.Context) ([]TelemetryItem, error) {
rows, err := q.db.QueryContext(ctx, getTelemetryItems)
if err != nil {
return nil, err
}
defer rows.Close()
var items []TelemetryItem
for rows.Next() {
var i TelemetryItem
if err := rows.Scan(
&i.Key,
&i.Value,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertTelemetryItemIfNotExists = `-- name: InsertTelemetryItemIfNotExists :exec
INSERT INTO telemetry_items (key, value)
VALUES ($1, $2)
ON CONFLICT (key) DO NOTHING
`
type InsertTelemetryItemIfNotExistsParams struct {
Key string `db:"key" json:"key"`
Value string `db:"value" json:"value"`
}
func (q *sqlQuerier) InsertTelemetryItemIfNotExists(ctx context.Context, arg InsertTelemetryItemIfNotExistsParams) error {
_, err := q.db.ExecContext(ctx, insertTelemetryItemIfNotExists, arg.Key, arg.Value)
return err
}
const upsertTelemetryItem = `-- name: UpsertTelemetryItem :exec
INSERT INTO telemetry_items (key, value)
VALUES ($1, $2)
ON CONFLICT (key) DO UPDATE SET value = $2, updated_at = NOW() WHERE telemetry_items.key = $1
`
type UpsertTelemetryItemParams struct {
Key string `db:"key" json:"key"`
Value string `db:"value" json:"value"`
}
func (q *sqlQuerier) UpsertTelemetryItem(ctx context.Context, arg UpsertTelemetryItemParams) error {
_, err := q.db.ExecContext(ctx, upsertTelemetryItem, arg.Key, arg.Value)
return err
}
const deleteOldTelemetryLocks = `-- name: DeleteOldTelemetryLocks :exec
DELETE FROM
telemetry_locks
WHERE
period_ending_at < $1::timestamptz
`
// Deletes old telemetry locks from the telemetry_locks table.
func (q *sqlQuerier) DeleteOldTelemetryLocks(ctx context.Context, periodEndingAtBefore time.Time) error {
_, err := q.db.ExecContext(ctx, deleteOldTelemetryLocks, periodEndingAtBefore)
return err
}
const insertTelemetryLock = `-- name: InsertTelemetryLock :exec
INSERT INTO
telemetry_locks (event_type, period_ending_at)
VALUES
($1, $2)
`
type InsertTelemetryLockParams struct {
EventType string `db:"event_type" json:"event_type"`
PeriodEndingAt time.Time `db:"period_ending_at" json:"period_ending_at"`
}
// Inserts a new lock row into the telemetry_locks table. Replicas should call
// this function prior to attempting to generate or publish a heartbeat event to
// the telemetry service.
// If the query returns a duplicate primary key error, the replica should not
// attempt to generate or publish the event to the telemetry service.
func (q *sqlQuerier) InsertTelemetryLock(ctx context.Context, arg InsertTelemetryLockParams) error {
_, err := q.db.ExecContext(ctx, insertTelemetryLock, arg.EventType, arg.PeriodEndingAt)
return err
}
const getTemplateAverageBuildTime = `-- name: GetTemplateAverageBuildTime :one
WITH build_times AS (
SELECT
EXTRACT(EPOCH FROM (pj.completed_at - pj.started_at))::FLOAT AS exec_time_sec,
workspace_builds.transition
FROM
workspace_builds
JOIN template_versions ON
workspace_builds.template_version_id = template_versions.id
JOIN provisioner_jobs pj ON
workspace_builds.job_id = pj.id
WHERE
template_versions.template_id = $1 AND
(pj.completed_at IS NOT NULL) AND (pj.started_at IS NOT NULL) AND
(pj.canceled_at IS NULL) AND
((pj.error IS NULL) OR (pj.error = ''))
ORDER BY
workspace_builds.created_at DESC
LIMIT 100
)
SELECT
-- Postgres offers no clear way to DRY this short of a function or other
-- complexities.
coalesce((PERCENTILE_DISC(0.5) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'start')), -1)::FLOAT AS start_50,
coalesce((PERCENTILE_DISC(0.5) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'stop')), -1)::FLOAT AS stop_50,
coalesce((PERCENTILE_DISC(0.5) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'delete')), -1)::FLOAT AS delete_50,
coalesce((PERCENTILE_DISC(0.95) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'start')), -1)::FLOAT AS start_95,
coalesce((PERCENTILE_DISC(0.95) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'stop')), -1)::FLOAT AS stop_95,
coalesce((PERCENTILE_DISC(0.95) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'delete')), -1)::FLOAT AS delete_95
FROM build_times
`
type GetTemplateAverageBuildTimeRow struct {
Start50 float64 `db:"start_50" json:"start_50"`
Stop50 float64 `db:"stop_50" json:"stop_50"`
Delete50 float64 `db:"delete_50" json:"delete_50"`
Start95 float64 `db:"start_95" json:"start_95"`
Stop95 float64 `db:"stop_95" json:"stop_95"`
Delete95 float64 `db:"delete_95" json:"delete_95"`
}
func (q *sqlQuerier) GetTemplateAverageBuildTime(ctx context.Context, templateID uuid.NullUUID) (GetTemplateAverageBuildTimeRow, error) {
row := q.db.QueryRowContext(ctx, getTemplateAverageBuildTime, templateID)
var i GetTemplateAverageBuildTimeRow
err := row.Scan(
&i.Start50,
&i.Stop50,
&i.Delete50,
&i.Start95,
&i.Stop95,
&i.Delete95,
)
return i, err
}
const getTemplateByID = `-- name: GetTemplateByID :one
SELECT
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, time_til_autostop_notify, agents_allowed, created_by_avatar_url, created_by_username, created_by_name, organization_name, organization_display_name, organization_icon
FROM
template_with_names
WHERE
id = $1
LIMIT
1
`
func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Template, error) {
row := q.db.QueryRowContext(ctx, getTemplateByID, id)
var i Template
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.OrganizationID,
&i.Deleted,
&i.Name,
&i.Provisioner,
&i.ActiveVersionID,
&i.Description,
&i.DefaultTTL,
&i.CreatedBy,
&i.Icon,
&i.UserACL,
&i.GroupACL,
&i.DisplayName,
&i.AllowUserCancelWorkspaceJobs,
&i.AllowUserAutostart,
&i.AllowUserAutostop,
&i.FailureTTL,
&i.TimeTilDormant,
&i.TimeTilDormantAutoDelete,
&i.AutostopRequirementDaysOfWeek,
&i.AutostopRequirementWeeks,
&i.AutostartBlockDaysOfWeek,
&i.RequireActiveVersion,
&i.Deprecated,
&i.ActivityBump,
&i.MaxPortSharingLevel,
&i.UseClassicParameterFlow,
&i.CorsBehavior,
&i.DisableModuleCache,
&i.TimeTilAutostopNotify,
&i.AgentsAllowed,
&i.CreatedByAvatarURL,
&i.CreatedByUsername,
&i.CreatedByName,
&i.OrganizationName,
&i.OrganizationDisplayName,
&i.OrganizationIcon,
)
return i, err
}
const getTemplateByOrganizationAndName = `-- name: GetTemplateByOrganizationAndName :one
SELECT
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, time_til_autostop_notify, agents_allowed, created_by_avatar_url, created_by_username, created_by_name, organization_name, organization_display_name, organization_icon
FROM
template_with_names AS templates
WHERE
organization_id = $1
AND deleted = $2
AND LOWER("name") = LOWER($3)
LIMIT
1
`
type GetTemplateByOrganizationAndNameParams struct {
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
Deleted bool `db:"deleted" json:"deleted"`
Name string `db:"name" json:"name"`
}
func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg GetTemplateByOrganizationAndNameParams) (Template, error) {
row := q.db.QueryRowContext(ctx, getTemplateByOrganizationAndName, arg.OrganizationID, arg.Deleted, arg.Name)
var i Template
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.OrganizationID,
&i.Deleted,
&i.Name,
&i.Provisioner,
&i.ActiveVersionID,
&i.Description,
&i.DefaultTTL,
&i.CreatedBy,
&i.Icon,
&i.UserACL,
&i.GroupACL,
&i.DisplayName,
&i.AllowUserCancelWorkspaceJobs,
&i.AllowUserAutostart,
&i.AllowUserAutostop,
&i.FailureTTL,
&i.TimeTilDormant,
&i.TimeTilDormantAutoDelete,
&i.AutostopRequirementDaysOfWeek,
&i.AutostopRequirementWeeks,
&i.AutostartBlockDaysOfWeek,
&i.RequireActiveVersion,
&i.Deprecated,
&i.ActivityBump,
&i.MaxPortSharingLevel,
&i.UseClassicParameterFlow,
&i.CorsBehavior,
&i.DisableModuleCache,
&i.TimeTilAutostopNotify,
&i.AgentsAllowed,
&i.CreatedByAvatarURL,
&i.CreatedByUsername,
&i.CreatedByName,
&i.OrganizationName,
&i.OrganizationDisplayName,
&i.OrganizationIcon,
)
return i, err
}
const getTemplates = `-- name: GetTemplates :many
SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, time_til_autostop_notify, agents_allowed, created_by_avatar_url, created_by_username, created_by_name, organization_name, organization_display_name, organization_icon FROM template_with_names AS templates
ORDER BY (name, id) ASC
`
func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
rows, err := q.db.QueryContext(ctx, getTemplates)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Template
for rows.Next() {
var i Template
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.OrganizationID,
&i.Deleted,
&i.Name,
&i.Provisioner,
&i.ActiveVersionID,
&i.Description,
&i.DefaultTTL,
&i.CreatedBy,
&i.Icon,
&i.UserACL,
&i.GroupACL,
&i.DisplayName,
&i.AllowUserCancelWorkspaceJobs,
&i.AllowUserAutostart,
&i.AllowUserAutostop,
&i.FailureTTL,
&i.TimeTilDormant,
&i.TimeTilDormantAutoDelete,
&i.AutostopRequirementDaysOfWeek,
&i.AutostopRequirementWeeks,
&i.AutostartBlockDaysOfWeek,
&i.RequireActiveVersion,
&i.Deprecated,
&i.ActivityBump,
&i.MaxPortSharingLevel,
&i.UseClassicParameterFlow,
&i.CorsBehavior,
&i.DisableModuleCache,
&i.TimeTilAutostopNotify,
&i.AgentsAllowed,
&i.CreatedByAvatarURL,
&i.CreatedByUsername,
&i.CreatedByName,
&i.OrganizationName,
&i.OrganizationDisplayName,
&i.OrganizationIcon,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getTemplatesWithFilter = `-- name: GetTemplatesWithFilter :many
SELECT
t.id, t.created_at, t.updated_at, t.organization_id, t.deleted, t.name, t.provisioner, t.active_version_id, t.description, t.default_ttl, t.created_by, t.icon, t.user_acl, t.group_acl, t.display_name, t.allow_user_cancel_workspace_jobs, t.allow_user_autostart, t.allow_user_autostop, t.failure_ttl, t.time_til_dormant, t.time_til_dormant_autodelete, t.autostop_requirement_days_of_week, t.autostop_requirement_weeks, t.autostart_block_days_of_week, t.require_active_version, t.deprecated, t.activity_bump, t.max_port_sharing_level, t.use_classic_parameter_flow, t.cors_behavior, t.disable_module_cache, t.time_til_autostop_notify, t.agents_allowed, t.created_by_avatar_url, t.created_by_username, t.created_by_name, t.organization_name, t.organization_display_name, t.organization_icon
FROM
template_with_names AS t
LEFT JOIN
template_versions tv ON t.active_version_id = tv.id
WHERE
-- Optionally include deleted templates
t.deleted = $1
-- Filter by organization_id
AND CASE
WHEN $2 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
t.organization_id = $2
ELSE true
END
-- Filter by exact name
AND CASE
WHEN $3 :: text != '' THEN
LOWER(t.name) = LOWER($3)
ELSE true
END
-- Filter by exact display name
AND CASE
WHEN $4 :: text != '' THEN
LOWER(t.display_name) = LOWER($4)
ELSE true
END
-- Filter by name, matching on substring
AND CASE
WHEN $5 :: text != '' THEN
lower(t.name) ILIKE '%' || lower($5) || '%'
ELSE true
END
-- Filter by display_name, matching on substring (fallback to name if display_name is empty)
AND CASE
WHEN $6 :: text != '' THEN
CASE
WHEN t.display_name IS NOT NULL AND t.display_name != '' THEN
lower(t.display_name) ILIKE '%' || lower($6) || '%'
ELSE
-- Remove spaces if present since 't.name' cannot have any spaces
lower(t.name) ILIKE '%' || REPLACE(lower($6), ' ', '') || '%'
END
ELSE true
END
-- Filter by ids
AND CASE
WHEN array_length($7 :: uuid[], 1) > 0 THEN
t.id = ANY($7)
ELSE true
END
-- Filter by deprecated
AND CASE
WHEN $8 :: boolean IS NOT NULL THEN
CASE
WHEN $8 :: boolean THEN
t.deprecated != ''
ELSE
t.deprecated = ''
END
ELSE true
END
-- Filter by has_ai_task in latest version
AND CASE
WHEN $9 :: boolean IS NOT NULL THEN
tv.has_ai_task = $9 :: boolean
ELSE true
END
-- Filter by agents_allowed
AND CASE
WHEN $10 :: boolean IS NOT NULL THEN
t.agents_allowed = $10 :: boolean
ELSE true
END
-- Filter by author_id
AND CASE
WHEN $11 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
t.created_by = $11
ELSE true
END
-- Filter by author_username
AND CASE
WHEN $12 :: text != '' THEN
t.created_by = (SELECT id FROM users WHERE lower(users.username) = lower($12) AND deleted = false)
ELSE true
END
-- Filter by has_external_agent in latest version
AND CASE
WHEN $13 :: boolean IS NOT NULL THEN
tv.has_external_agent = $13 :: boolean
ELSE true
END
-- Authorize Filter clause will be injected below in GetAuthorizedTemplates
-- @authorize_filter
ORDER BY (t.name, t.id) ASC
`
type GetTemplatesWithFilterParams struct {
Deleted bool `db:"deleted" json:"deleted"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
ExactName string `db:"exact_name" json:"exact_name"`
ExactDisplayName string `db:"exact_display_name" json:"exact_display_name"`
FuzzyName string `db:"fuzzy_name" json:"fuzzy_name"`
FuzzyDisplayName string `db:"fuzzy_display_name" json:"fuzzy_display_name"`
IDs []uuid.UUID `db:"ids" json:"ids"`
Deprecated sql.NullBool `db:"deprecated" json:"deprecated"`
HasAITask sql.NullBool `db:"has_ai_task" json:"has_ai_task"`
AgentsAllowed sql.NullBool `db:"agents_allowed" json:"agents_allowed"`
AuthorID uuid.UUID `db:"author_id" json:"author_id"`
AuthorUsername string `db:"author_username" json:"author_username"`
HasExternalAgent sql.NullBool `db:"has_external_agent" json:"has_external_agent"`
}
func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplatesWithFilterParams) ([]Template, error) {
rows, err := q.db.QueryContext(ctx, getTemplatesWithFilter,
arg.Deleted,
arg.OrganizationID,
arg.ExactName,
arg.ExactDisplayName,
arg.FuzzyName,
arg.FuzzyDisplayName,
pq.Array(arg.IDs),
arg.Deprecated,
arg.HasAITask,
arg.AgentsAllowed,
arg.AuthorID,
arg.AuthorUsername,
arg.HasExternalAgent,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Template
for rows.Next() {
var i Template
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.OrganizationID,
&i.Deleted,
&i.Name,
&i.Provisioner,
&i.ActiveVersionID,
&i.Description,
&i.DefaultTTL,
&i.CreatedBy,
&i.Icon,
&i.UserACL,
&i.GroupACL,
&i.DisplayName,
&i.AllowUserCancelWorkspaceJobs,
&i.AllowUserAutostart,
&i.AllowUserAutostop,
&i.FailureTTL,
&i.TimeTilDormant,
&i.TimeTilDormantAutoDelete,
&i.AutostopRequirementDaysOfWeek,
&i.AutostopRequirementWeeks,
&i.AutostartBlockDaysOfWeek,
&i.RequireActiveVersion,
&i.Deprecated,
&i.ActivityBump,
&i.MaxPortSharingLevel,
&i.UseClassicParameterFlow,
&i.CorsBehavior,
&i.DisableModuleCache,
&i.TimeTilAutostopNotify,
&i.AgentsAllowed,
&i.CreatedByAvatarURL,
&i.CreatedByUsername,
&i.CreatedByName,
&i.OrganizationName,
&i.OrganizationDisplayName,
&i.OrganizationIcon,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertTemplate = `-- name: InsertTemplate :exec
INSERT INTO
templates (
id,
created_at,
updated_at,
organization_id,
"name",
provisioner,
active_version_id,
description,
created_by,
icon,
user_acl,
group_acl,
display_name,
allow_user_cancel_workspace_jobs,
max_port_sharing_level,
use_classic_parameter_flow,
cors_behavior,
agents_allowed
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
`
type InsertTemplateParams struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
Name string `db:"name" json:"name"`
Provisioner ProvisionerType `db:"provisioner" json:"provisioner"`
ActiveVersionID uuid.UUID `db:"active_version_id" json:"active_version_id"`
Description string `db:"description" json:"description"`
CreatedBy uuid.UUID `db:"created_by" json:"created_by"`
Icon string `db:"icon" json:"icon"`
UserACL TemplateACL `db:"user_acl" json:"user_acl"`
GroupACL TemplateACL `db:"group_acl" json:"group_acl"`
DisplayName string `db:"display_name" json:"display_name"`
AllowUserCancelWorkspaceJobs bool `db:"allow_user_cancel_workspace_jobs" json:"allow_user_cancel_workspace_jobs"`
MaxPortSharingLevel AppSharingLevel `db:"max_port_sharing_level" json:"max_port_sharing_level"`
UseClassicParameterFlow bool `db:"use_classic_parameter_flow" json:"use_classic_parameter_flow"`
CorsBehavior CorsBehavior `db:"cors_behavior" json:"cors_behavior"`
AgentsAllowed bool `db:"agents_allowed" json:"agents_allowed"`
}
func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParams) error {
_, err := q.db.ExecContext(ctx, insertTemplate,
arg.ID,
arg.CreatedAt,
arg.UpdatedAt,
arg.OrganizationID,
arg.Name,
arg.Provisioner,
arg.ActiveVersionID,
arg.Description,
arg.CreatedBy,
arg.Icon,
arg.UserACL,
arg.GroupACL,
arg.DisplayName,
arg.AllowUserCancelWorkspaceJobs,
arg.MaxPortSharingLevel,
arg.UseClassicParameterFlow,
arg.CorsBehavior,
arg.AgentsAllowed,
)
return err
}
const updateTemplateACLByID = `-- name: UpdateTemplateACLByID :exec
UPDATE
templates
SET
group_acl = $1,
user_acl = $2
WHERE
id = $3
`
type UpdateTemplateACLByIDParams struct {
GroupACL TemplateACL `db:"group_acl" json:"group_acl"`
UserACL TemplateACL `db:"user_acl" json:"user_acl"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateTemplateACLByID(ctx context.Context, arg UpdateTemplateACLByIDParams) error {
_, err := q.db.ExecContext(ctx, updateTemplateACLByID, arg.GroupACL, arg.UserACL, arg.ID)
return err
}
const updateTemplateAccessControlByID = `-- name: UpdateTemplateAccessControlByID :exec
UPDATE
templates
SET
require_active_version = $2,
deprecated = $3
WHERE
id = $1
`
type UpdateTemplateAccessControlByIDParams struct {
ID uuid.UUID `db:"id" json:"id"`
RequireActiveVersion bool `db:"require_active_version" json:"require_active_version"`
Deprecated string `db:"deprecated" json:"deprecated"`
}
func (q *sqlQuerier) UpdateTemplateAccessControlByID(ctx context.Context, arg UpdateTemplateAccessControlByIDParams) error {
_, err := q.db.ExecContext(ctx, updateTemplateAccessControlByID, arg.ID, arg.RequireActiveVersion, arg.Deprecated)
return err
}
const updateTemplateActiveVersionByID = `-- name: UpdateTemplateActiveVersionByID :exec
UPDATE
templates
SET
active_version_id = $2,
updated_at = $3
WHERE
id = $1
`
type UpdateTemplateActiveVersionByIDParams struct {
ID uuid.UUID `db:"id" json:"id"`
ActiveVersionID uuid.UUID `db:"active_version_id" json:"active_version_id"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
func (q *sqlQuerier) UpdateTemplateActiveVersionByID(ctx context.Context, arg UpdateTemplateActiveVersionByIDParams) error {
_, err := q.db.ExecContext(ctx, updateTemplateActiveVersionByID, arg.ID, arg.ActiveVersionID, arg.UpdatedAt)
return err
}
const updateTemplateDeletedByID = `-- name: UpdateTemplateDeletedByID :exec
UPDATE
templates
SET
deleted = $2,
updated_at = $3
WHERE
id = $1
`
type UpdateTemplateDeletedByIDParams struct {
ID uuid.UUID `db:"id" json:"id"`
Deleted bool `db:"deleted" json:"deleted"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
func (q *sqlQuerier) UpdateTemplateDeletedByID(ctx context.Context, arg UpdateTemplateDeletedByIDParams) error {
_, err := q.db.ExecContext(ctx, updateTemplateDeletedByID, arg.ID, arg.Deleted, arg.UpdatedAt)
return err
}
const updateTemplateMetaByID = `-- name: UpdateTemplateMetaByID :exec
UPDATE
templates
SET
updated_at = $2,
description = $3,
name = $4,
icon = $5,
display_name = $6,
allow_user_cancel_workspace_jobs = $7,
group_acl = $8,
max_port_sharing_level = $9,
use_classic_parameter_flow = $10,
cors_behavior = $11,
disable_module_cache = $12,
agents_allowed = $13
WHERE
id = $1
`
type UpdateTemplateMetaByIDParams struct {
ID uuid.UUID `db:"id" json:"id"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Description string `db:"description" json:"description"`
Name string `db:"name" json:"name"`
Icon string `db:"icon" json:"icon"`
DisplayName string `db:"display_name" json:"display_name"`
AllowUserCancelWorkspaceJobs bool `db:"allow_user_cancel_workspace_jobs" json:"allow_user_cancel_workspace_jobs"`
GroupACL TemplateACL `db:"group_acl" json:"group_acl"`
MaxPortSharingLevel AppSharingLevel `db:"max_port_sharing_level" json:"max_port_sharing_level"`
UseClassicParameterFlow bool `db:"use_classic_parameter_flow" json:"use_classic_parameter_flow"`
CorsBehavior CorsBehavior `db:"cors_behavior" json:"cors_behavior"`
DisableModuleCache bool `db:"disable_module_cache" json:"disable_module_cache"`
AgentsAllowed bool `db:"agents_allowed" json:"agents_allowed"`
}
func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) error {
_, err := q.db.ExecContext(ctx, updateTemplateMetaByID,
arg.ID,
arg.UpdatedAt,
arg.Description,
arg.Name,
arg.Icon,
arg.DisplayName,
arg.AllowUserCancelWorkspaceJobs,
arg.GroupACL,
arg.MaxPortSharingLevel,
arg.UseClassicParameterFlow,
arg.CorsBehavior,
arg.DisableModuleCache,
arg.AgentsAllowed,
)
return err
}
const updateTemplateScheduleByID = `-- name: UpdateTemplateScheduleByID :exec
UPDATE
templates
SET
updated_at = $2,
allow_user_autostart = $3,
allow_user_autostop = $4,
default_ttl = $5,
activity_bump = $6,
autostop_requirement_days_of_week = $7,
autostop_requirement_weeks = $8,
autostart_block_days_of_week = $9,
failure_ttl = $10,
time_til_dormant = $11,
time_til_dormant_autodelete = $12,
time_til_autostop_notify = $13
WHERE
id = $1
`
type UpdateTemplateScheduleByIDParams struct {
ID uuid.UUID `db:"id" json:"id"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
AllowUserAutostart bool `db:"allow_user_autostart" json:"allow_user_autostart"`
AllowUserAutostop bool `db:"allow_user_autostop" json:"allow_user_autostop"`
DefaultTTL int64 `db:"default_ttl" json:"default_ttl"`
ActivityBump int64 `db:"activity_bump" json:"activity_bump"`
AutostopRequirementDaysOfWeek int16 `db:"autostop_requirement_days_of_week" json:"autostop_requirement_days_of_week"`
AutostopRequirementWeeks int64 `db:"autostop_requirement_weeks" json:"autostop_requirement_weeks"`
AutostartBlockDaysOfWeek int16 `db:"autostart_block_days_of_week" json:"autostart_block_days_of_week"`
FailureTTL int64 `db:"failure_ttl" json:"failure_ttl"`
TimeTilDormant int64 `db:"time_til_dormant" json:"time_til_dormant"`
TimeTilDormantAutoDelete int64 `db:"time_til_dormant_autodelete" json:"time_til_dormant_autodelete"`
TimeTilAutostopNotify int64 `db:"time_til_autostop_notify" json:"time_til_autostop_notify"`
}
func (q *sqlQuerier) UpdateTemplateScheduleByID(ctx context.Context, arg UpdateTemplateScheduleByIDParams) error {
_, err := q.db.ExecContext(ctx, updateTemplateScheduleByID,
arg.ID,
arg.UpdatedAt,
arg.AllowUserAutostart,
arg.AllowUserAutostop,
arg.DefaultTTL,
arg.ActivityBump,
arg.AutostopRequirementDaysOfWeek,
arg.AutostopRequirementWeeks,
arg.AutostartBlockDaysOfWeek,
arg.FailureTTL,
arg.TimeTilDormant,
arg.TimeTilDormantAutoDelete,
arg.TimeTilAutostopNotify,
)
return err
}
const getTemplateVersionParameters = `-- name: GetTemplateVersionParameters :many
SELECT template_version_id, name, description, type, mutable, default_value, icon, options, validation_regex, validation_min, validation_max, validation_error, validation_monotonic, required, display_name, display_order, ephemeral, form_type FROM template_version_parameters WHERE template_version_id = $1 ORDER BY display_order ASC, LOWER(name) ASC
`
func (q *sqlQuerier) GetTemplateVersionParameters(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionParameter, error) {
rows, err := q.db.QueryContext(ctx, getTemplateVersionParameters, templateVersionID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []TemplateVersionParameter
for rows.Next() {
var i TemplateVersionParameter
if err := rows.Scan(
&i.TemplateVersionID,
&i.Name,
&i.Description,
&i.Type,
&i.Mutable,
&i.DefaultValue,
&i.Icon,
&i.Options,
&i.ValidationRegex,
&i.ValidationMin,
&i.ValidationMax,
&i.ValidationError,
&i.ValidationMonotonic,
&i.Required,
&i.DisplayName,
&i.DisplayOrder,
&i.Ephemeral,
&i.FormType,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertTemplateVersionParameter = `-- name: InsertTemplateVersionParameter :one
INSERT INTO
template_version_parameters (
template_version_id,
name,
description,
type,
form_type,
mutable,
default_value,
icon,
options,
validation_regex,
validation_min,
validation_max,
validation_error,
validation_monotonic,
required,
display_name,
display_order,
ephemeral
)
VALUES
(
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
$9,
$10,
$11,
$12,
$13,
$14,
$15,
$16,
$17,
$18
) RETURNING template_version_id, name, description, type, mutable, default_value, icon, options, validation_regex, validation_min, validation_max, validation_error, validation_monotonic, required, display_name, display_order, ephemeral, form_type
`
type InsertTemplateVersionParameterParams struct {
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
Name string `db:"name" json:"name"`
Description string `db:"description" json:"description"`
Type string `db:"type" json:"type"`
FormType ParameterFormType `db:"form_type" json:"form_type"`
Mutable bool `db:"mutable" json:"mutable"`
DefaultValue string `db:"default_value" json:"default_value"`
Icon string `db:"icon" json:"icon"`
Options json.RawMessage `db:"options" json:"options"`
ValidationRegex string `db:"validation_regex" json:"validation_regex"`
ValidationMin sql.NullInt32 `db:"validation_min" json:"validation_min"`
ValidationMax sql.NullInt32 `db:"validation_max" json:"validation_max"`
ValidationError string `db:"validation_error" json:"validation_error"`
ValidationMonotonic string `db:"validation_monotonic" json:"validation_monotonic"`
Required bool `db:"required" json:"required"`
DisplayName string `db:"display_name" json:"display_name"`
DisplayOrder int32 `db:"display_order" json:"display_order"`
Ephemeral bool `db:"ephemeral" json:"ephemeral"`
}
func (q *sqlQuerier) InsertTemplateVersionParameter(ctx context.Context, arg InsertTemplateVersionParameterParams) (TemplateVersionParameter, error) {
row := q.db.QueryRowContext(ctx, insertTemplateVersionParameter,
arg.TemplateVersionID,
arg.Name,
arg.Description,
arg.Type,
arg.FormType,
arg.Mutable,
arg.DefaultValue,
arg.Icon,
arg.Options,
arg.ValidationRegex,
arg.ValidationMin,
arg.ValidationMax,
arg.ValidationError,
arg.ValidationMonotonic,
arg.Required,
arg.DisplayName,
arg.DisplayOrder,
arg.Ephemeral,
)
var i TemplateVersionParameter
err := row.Scan(
&i.TemplateVersionID,
&i.Name,
&i.Description,
&i.Type,
&i.Mutable,
&i.DefaultValue,
&i.Icon,
&i.Options,
&i.ValidationRegex,
&i.ValidationMin,
&i.ValidationMax,
&i.ValidationError,
&i.ValidationMonotonic,
&i.Required,
&i.DisplayName,
&i.DisplayOrder,
&i.Ephemeral,
&i.FormType,
)
return i, err
}
const archiveUnusedTemplateVersions = `-- name: ArchiveUnusedTemplateVersions :many
UPDATE
template_versions
SET
archived = true,
updated_at = $1
FROM
-- Archive all versions that are returned from this query.
(
SELECT
scoped_template_versions.id
FROM
-- Scope an archive to a single template and ignore already archived template versions
(
SELECT
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, source_example_id, has_ai_task, has_external_agent
FROM
template_versions
WHERE
template_versions.template_id = $2 :: uuid
AND
archived = false
AND
-- This allows archiving a specific template version.
CASE
WHEN $3::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
template_versions.id = $3 :: uuid
ELSE
true
END
) AS scoped_template_versions
LEFT JOIN
provisioner_jobs ON scoped_template_versions.job_id = provisioner_jobs.id
LEFT JOIN
templates ON scoped_template_versions.template_id = templates.id
WHERE
-- Actively used template versions (meaning the latest build is using
-- the version) are never archived. A "restart" command on the workspace,
-- even if failed, would use the version. So it cannot be archived until
-- the build is outdated.
NOT EXISTS (
-- Return all "used" versions, where "used" is defined as being
-- used by a latest workspace build.
SELECT template_version_id FROM (
SELECT
DISTINCT ON (workspace_id) template_version_id, transition
FROM
workspace_builds
ORDER BY workspace_id, build_number DESC
) AS used_versions
WHERE
used_versions.transition != 'delete'
AND
scoped_template_versions.id = used_versions.template_version_id
)
-- Also never archive the active template version
AND active_version_id != scoped_template_versions.id
AND CASE
-- Optionally, only archive versions that match a given
-- job status like 'failed'.
WHEN $4 :: provisioner_job_status IS NOT NULL THEN
provisioner_jobs.job_status = $4 :: provisioner_job_status
ELSE
true
END
-- Pending or running jobs should not be archived, as they are "in progress"
AND provisioner_jobs.job_status != 'running'
AND provisioner_jobs.job_status != 'pending'
) AS archived_versions
WHERE
template_versions.id IN (archived_versions.id)
RETURNING template_versions.id
`
type ArchiveUnusedTemplateVersionsParams struct {
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
JobStatus NullProvisionerJobStatus `db:"job_status" json:"job_status"`
}
// Archiving templates is a soft delete action, so is reversible.
// Archiving prevents the version from being used and discovered
// by listing.
// Only unused template versions will be archived, which are any versions not
// referenced by the latest build of a workspace.
func (q *sqlQuerier) ArchiveUnusedTemplateVersions(ctx context.Context, arg ArchiveUnusedTemplateVersionsParams) ([]uuid.UUID, error) {
rows, err := q.db.QueryContext(ctx, archiveUnusedTemplateVersions,
arg.UpdatedAt,
arg.TemplateID,
arg.TemplateVersionID,
arg.JobStatus,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []uuid.UUID
for rows.Next() {
var id uuid.UUID
if err := rows.Scan(&id); err != nil {
return nil, err
}
items = append(items, id)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getPreviousTemplateVersion = `-- name: GetPreviousTemplateVersion :one
SELECT
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, source_example_id, has_ai_task, has_external_agent, created_by_avatar_url, created_by_username, created_by_name
FROM
template_version_with_user AS template_versions
WHERE
created_at < (
SELECT created_at
FROM template_version_with_user AS tv
WHERE tv.organization_id = $1 AND tv.name = $2 AND tv.template_id = $3
)
AND organization_id = $1
AND template_id = $3
ORDER BY created_at DESC
LIMIT 1
`
type GetPreviousTemplateVersionParams struct {
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
Name string `db:"name" json:"name"`
TemplateID uuid.NullUUID `db:"template_id" json:"template_id"`
}
func (q *sqlQuerier) GetPreviousTemplateVersion(ctx context.Context, arg GetPreviousTemplateVersionParams) (TemplateVersion, error) {
row := q.db.QueryRowContext(ctx, getPreviousTemplateVersion, arg.OrganizationID, arg.Name, arg.TemplateID)
var i TemplateVersion
err := row.Scan(
&i.ID,
&i.TemplateID,
&i.OrganizationID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Name,
&i.Readme,
&i.JobID,
&i.CreatedBy,
&i.ExternalAuthProviders,
&i.Message,
&i.Archived,
&i.SourceExampleID,
&i.HasAITask,
&i.HasExternalAgent,
&i.CreatedByAvatarURL,
&i.CreatedByUsername,
&i.CreatedByName,
)
return i, err
}
const getTemplateVersionByID = `-- name: GetTemplateVersionByID :one
SELECT
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, source_example_id, has_ai_task, has_external_agent, created_by_avatar_url, created_by_username, created_by_name
FROM
template_version_with_user AS template_versions
WHERE
id = $1
`
func (q *sqlQuerier) GetTemplateVersionByID(ctx context.Context, id uuid.UUID) (TemplateVersion, error) {
row := q.db.QueryRowContext(ctx, getTemplateVersionByID, id)
var i TemplateVersion
err := row.Scan(
&i.ID,
&i.TemplateID,
&i.OrganizationID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Name,
&i.Readme,
&i.JobID,
&i.CreatedBy,
&i.ExternalAuthProviders,
&i.Message,
&i.Archived,
&i.SourceExampleID,
&i.HasAITask,
&i.HasExternalAgent,
&i.CreatedByAvatarURL,
&i.CreatedByUsername,
&i.CreatedByName,
)
return i, err
}
const getTemplateVersionByJobID = `-- name: GetTemplateVersionByJobID :one
SELECT
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, source_example_id, has_ai_task, has_external_agent, created_by_avatar_url, created_by_username, created_by_name
FROM
template_version_with_user AS template_versions
WHERE
job_id = $1
`
func (q *sqlQuerier) GetTemplateVersionByJobID(ctx context.Context, jobID uuid.UUID) (TemplateVersion, error) {
row := q.db.QueryRowContext(ctx, getTemplateVersionByJobID, jobID)
var i TemplateVersion
err := row.Scan(
&i.ID,
&i.TemplateID,
&i.OrganizationID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Name,
&i.Readme,
&i.JobID,
&i.CreatedBy,
&i.ExternalAuthProviders,
&i.Message,
&i.Archived,
&i.SourceExampleID,
&i.HasAITask,
&i.HasExternalAgent,
&i.CreatedByAvatarURL,
&i.CreatedByUsername,
&i.CreatedByName,
)
return i, err
}
const getTemplateVersionByTemplateIDAndName = `-- name: GetTemplateVersionByTemplateIDAndName :one
SELECT
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, source_example_id, has_ai_task, has_external_agent, created_by_avatar_url, created_by_username, created_by_name
FROM
template_version_with_user AS template_versions
WHERE
template_id = $1
AND "name" = $2
`
type GetTemplateVersionByTemplateIDAndNameParams struct {
TemplateID uuid.NullUUID `db:"template_id" json:"template_id"`
Name string `db:"name" json:"name"`
}
func (q *sqlQuerier) GetTemplateVersionByTemplateIDAndName(ctx context.Context, arg GetTemplateVersionByTemplateIDAndNameParams) (TemplateVersion, error) {
row := q.db.QueryRowContext(ctx, getTemplateVersionByTemplateIDAndName, arg.TemplateID, arg.Name)
var i TemplateVersion
err := row.Scan(
&i.ID,
&i.TemplateID,
&i.OrganizationID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Name,
&i.Readme,
&i.JobID,
&i.CreatedBy,
&i.ExternalAuthProviders,
&i.Message,
&i.Archived,
&i.SourceExampleID,
&i.HasAITask,
&i.HasExternalAgent,
&i.CreatedByAvatarURL,
&i.CreatedByUsername,
&i.CreatedByName,
)
return i, err
}
const getTemplateVersionsByIDs = `-- name: GetTemplateVersionsByIDs :many
SELECT
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, source_example_id, has_ai_task, has_external_agent, created_by_avatar_url, created_by_username, created_by_name
FROM
template_version_with_user AS template_versions
WHERE
id = ANY($1 :: uuid [ ])
`
func (q *sqlQuerier) GetTemplateVersionsByIDs(ctx context.Context, ids []uuid.UUID) ([]TemplateVersion, error) {
rows, err := q.db.QueryContext(ctx, getTemplateVersionsByIDs, pq.Array(ids))
if err != nil {
return nil, err
}
defer rows.Close()
var items []TemplateVersion
for rows.Next() {
var i TemplateVersion
if err := rows.Scan(
&i.ID,
&i.TemplateID,
&i.OrganizationID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Name,
&i.Readme,
&i.JobID,
&i.CreatedBy,
&i.ExternalAuthProviders,
&i.Message,
&i.Archived,
&i.SourceExampleID,
&i.HasAITask,
&i.HasExternalAgent,
&i.CreatedByAvatarURL,
&i.CreatedByUsername,
&i.CreatedByName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getTemplateVersionsByTemplateID = `-- name: GetTemplateVersionsByTemplateID :many
SELECT
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, source_example_id, has_ai_task, has_external_agent, created_by_avatar_url, created_by_username, created_by_name
FROM
template_version_with_user AS template_versions
WHERE
template_id = $1 :: uuid
AND CASE
-- If no filter is provided, default to returning ALL template versions.
-- The called should always provide a filter if they want to omit
-- archived versions.
WHEN $2 :: boolean IS NULL THEN true
ELSE template_versions.archived = $2 :: boolean
END
AND CASE
-- This allows using the last element on a page as effectively a cursor.
-- This is an important option for scripts that need to paginate without
-- duplicating or missing data.
WHEN $3 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN (
-- The pagination cursor is the last ID of the previous page.
-- The query is ordered by the created_at field, so select all
-- rows after the cursor.
(created_at, id) > (
SELECT
created_at, id
FROM
template_versions
WHERE
id = $3
)
)
ELSE true
END
ORDER BY
-- Deterministic and consistent ordering of all rows, even if they share
-- a timestamp. This is to ensure consistent pagination.
(created_at, id) ASC OFFSET $4
LIMIT
-- A null limit means "no limit", so 0 means return all
NULLIF($5 :: int, 0)
`
type GetTemplateVersionsByTemplateIDParams struct {
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
Archived sql.NullBool `db:"archived" json:"archived"`
AfterID uuid.UUID `db:"after_id" json:"after_id"`
OffsetOpt int32 `db:"offset_opt" json:"offset_opt"`
LimitOpt int32 `db:"limit_opt" json:"limit_opt"`
}
func (q *sqlQuerier) GetTemplateVersionsByTemplateID(ctx context.Context, arg GetTemplateVersionsByTemplateIDParams) ([]TemplateVersion, error) {
rows, err := q.db.QueryContext(ctx, getTemplateVersionsByTemplateID,
arg.TemplateID,
arg.Archived,
arg.AfterID,
arg.OffsetOpt,
arg.LimitOpt,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []TemplateVersion
for rows.Next() {
var i TemplateVersion
if err := rows.Scan(
&i.ID,
&i.TemplateID,
&i.OrganizationID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Name,
&i.Readme,
&i.JobID,
&i.CreatedBy,
&i.ExternalAuthProviders,
&i.Message,
&i.Archived,
&i.SourceExampleID,
&i.HasAITask,
&i.HasExternalAgent,
&i.CreatedByAvatarURL,
&i.CreatedByUsername,
&i.CreatedByName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getTemplateVersionsCreatedAfter = `-- name: GetTemplateVersionsCreatedAfter :many
SELECT id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, source_example_id, has_ai_task, has_external_agent, created_by_avatar_url, created_by_username, created_by_name FROM template_version_with_user AS template_versions WHERE created_at > $1
`
func (q *sqlQuerier) GetTemplateVersionsCreatedAfter(ctx context.Context, createdAt time.Time) ([]TemplateVersion, error) {
rows, err := q.db.QueryContext(ctx, getTemplateVersionsCreatedAfter, createdAt)
if err != nil {
return nil, err
}
defer rows.Close()
var items []TemplateVersion
for rows.Next() {
var i TemplateVersion
if err := rows.Scan(
&i.ID,
&i.TemplateID,
&i.OrganizationID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Name,
&i.Readme,
&i.JobID,
&i.CreatedBy,
&i.ExternalAuthProviders,
&i.Message,
&i.Archived,
&i.SourceExampleID,
&i.HasAITask,
&i.HasExternalAgent,
&i.CreatedByAvatarURL,
&i.CreatedByUsername,
&i.CreatedByName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertTemplateVersion = `-- name: InsertTemplateVersion :exec
INSERT INTO
template_versions (
id,
template_id,
organization_id,
created_at,
updated_at,
"name",
message,
readme,
job_id,
created_by,
source_example_id
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
`
type InsertTemplateVersionParams struct {
ID uuid.UUID `db:"id" json:"id"`
TemplateID uuid.NullUUID `db:"template_id" json:"template_id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Name string `db:"name" json:"name"`
Message string `db:"message" json:"message"`
Readme string `db:"readme" json:"readme"`
JobID uuid.UUID `db:"job_id" json:"job_id"`
CreatedBy uuid.UUID `db:"created_by" json:"created_by"`
SourceExampleID sql.NullString `db:"source_example_id" json:"source_example_id"`
}
func (q *sqlQuerier) InsertTemplateVersion(ctx context.Context, arg InsertTemplateVersionParams) error {
_, err := q.db.ExecContext(ctx, insertTemplateVersion,
arg.ID,
arg.TemplateID,
arg.OrganizationID,
arg.CreatedAt,
arg.UpdatedAt,
arg.Name,
arg.Message,
arg.Readme,
arg.JobID,
arg.CreatedBy,
arg.SourceExampleID,
)
return err
}
const unarchiveTemplateVersion = `-- name: UnarchiveTemplateVersion :exec
UPDATE
template_versions
SET
archived = false,
updated_at = $1
WHERE
id = $2
`
type UnarchiveTemplateVersionParams struct {
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
}
// This will always work regardless of the current state of the template version.
func (q *sqlQuerier) UnarchiveTemplateVersion(ctx context.Context, arg UnarchiveTemplateVersionParams) error {
_, err := q.db.ExecContext(ctx, unarchiveTemplateVersion, arg.UpdatedAt, arg.TemplateVersionID)
return err
}
const updateTemplateVersionByID = `-- name: UpdateTemplateVersionByID :exec
UPDATE
template_versions
SET
template_id = $2,
updated_at = $3,
name = $4,
message = $5
WHERE
id = $1
`
type UpdateTemplateVersionByIDParams struct {
ID uuid.UUID `db:"id" json:"id"`
TemplateID uuid.NullUUID `db:"template_id" json:"template_id"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Name string `db:"name" json:"name"`
Message string `db:"message" json:"message"`
}
func (q *sqlQuerier) UpdateTemplateVersionByID(ctx context.Context, arg UpdateTemplateVersionByIDParams) error {
_, err := q.db.ExecContext(ctx, updateTemplateVersionByID,
arg.ID,
arg.TemplateID,
arg.UpdatedAt,
arg.Name,
arg.Message,
)
return err
}
const updateTemplateVersionDescriptionByJobID = `-- name: UpdateTemplateVersionDescriptionByJobID :exec
UPDATE
template_versions
SET
readme = $2,
updated_at = $3
WHERE
job_id = $1
`
type UpdateTemplateVersionDescriptionByJobIDParams struct {
JobID uuid.UUID `db:"job_id" json:"job_id"`
Readme string `db:"readme" json:"readme"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
func (q *sqlQuerier) UpdateTemplateVersionDescriptionByJobID(ctx context.Context, arg UpdateTemplateVersionDescriptionByJobIDParams) error {
_, err := q.db.ExecContext(ctx, updateTemplateVersionDescriptionByJobID, arg.JobID, arg.Readme, arg.UpdatedAt)
return err
}
const updateTemplateVersionExternalAuthProvidersByJobID = `-- name: UpdateTemplateVersionExternalAuthProvidersByJobID :exec
UPDATE
template_versions
SET
external_auth_providers = $2,
updated_at = $3
WHERE
job_id = $1
`
type UpdateTemplateVersionExternalAuthProvidersByJobIDParams struct {
JobID uuid.UUID `db:"job_id" json:"job_id"`
ExternalAuthProviders json.RawMessage `db:"external_auth_providers" json:"external_auth_providers"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
func (q *sqlQuerier) UpdateTemplateVersionExternalAuthProvidersByJobID(ctx context.Context, arg UpdateTemplateVersionExternalAuthProvidersByJobIDParams) error {
_, err := q.db.ExecContext(ctx, updateTemplateVersionExternalAuthProvidersByJobID, arg.JobID, arg.ExternalAuthProviders, arg.UpdatedAt)
return err
}
const updateTemplateVersionFlagsByJobID = `-- name: UpdateTemplateVersionFlagsByJobID :exec
UPDATE
template_versions
SET
has_ai_task = $2,
has_external_agent = $3,
updated_at = $4
WHERE
job_id = $1
`
type UpdateTemplateVersionFlagsByJobIDParams struct {
JobID uuid.UUID `db:"job_id" json:"job_id"`
HasAITask sql.NullBool `db:"has_ai_task" json:"has_ai_task"`
HasExternalAgent sql.NullBool `db:"has_external_agent" json:"has_external_agent"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
func (q *sqlQuerier) UpdateTemplateVersionFlagsByJobID(ctx context.Context, arg UpdateTemplateVersionFlagsByJobIDParams) error {
_, err := q.db.ExecContext(ctx, updateTemplateVersionFlagsByJobID,
arg.JobID,
arg.HasAITask,
arg.HasExternalAgent,
arg.UpdatedAt,
)
return err
}
const getTemplateVersionTerraformValues = `-- name: GetTemplateVersionTerraformValues :one
SELECT
template_version_terraform_values.template_version_id, template_version_terraform_values.updated_at, template_version_terraform_values.cached_plan, template_version_terraform_values.cached_module_files, template_version_terraform_values.provisionerd_version
FROM
template_version_terraform_values
WHERE
template_version_terraform_values.template_version_id = $1
`
func (q *sqlQuerier) GetTemplateVersionTerraformValues(ctx context.Context, templateVersionID uuid.UUID) (TemplateVersionTerraformValue, error) {
row := q.db.QueryRowContext(ctx, getTemplateVersionTerraformValues, templateVersionID)
var i TemplateVersionTerraformValue
err := row.Scan(
&i.TemplateVersionID,
&i.UpdatedAt,
&i.CachedPlan,
&i.CachedModuleFiles,
&i.ProvisionerdVersion,
)
return i, err
}
const hasTemplateVersionsUsingCachedModuleFileInOrg = `-- name: HasTemplateVersionsUsingCachedModuleFileInOrg :one
SELECT EXISTS (
SELECT 1
FROM template_version_terraform_values tvtv
JOIN template_versions tv
ON tv.id = tvtv.template_version_id
WHERE tvtv.cached_module_files = $1::uuid
AND tv.organization_id = $2::uuid
)
`
type HasTemplateVersionsUsingCachedModuleFileInOrgParams struct {
FileID uuid.UUID `db:"file_id" json:"file_id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
}
// Reports whether the given file is referenced as cached module files by any
// template version in the given organization. Used to authorize provisioner
// module-file downloads so a daemon cannot read another organization's cached
// Terraform module source.
func (q *sqlQuerier) HasTemplateVersionsUsingCachedModuleFileInOrg(ctx context.Context, arg HasTemplateVersionsUsingCachedModuleFileInOrgParams) (bool, error) {
row := q.db.QueryRowContext(ctx, hasTemplateVersionsUsingCachedModuleFileInOrg, arg.FileID, arg.OrganizationID)
var exists bool
err := row.Scan(&exists)
return exists, err
}
const insertTemplateVersionTerraformValuesByJobID = `-- name: InsertTemplateVersionTerraformValuesByJobID :exec
INSERT INTO
template_version_terraform_values (
template_version_id,
cached_plan,
cached_module_files,
updated_at,
provisionerd_version
)
VALUES
(
(select id from template_versions where job_id = $1),
$2,
$3,
$4,
$5
)
`
type InsertTemplateVersionTerraformValuesByJobIDParams struct {
JobID uuid.UUID `db:"job_id" json:"job_id"`
CachedPlan json.RawMessage `db:"cached_plan" json:"cached_plan"`
CachedModuleFiles uuid.NullUUID `db:"cached_module_files" json:"cached_module_files"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
ProvisionerdVersion string `db:"provisionerd_version" json:"provisionerd_version"`
}
func (q *sqlQuerier) InsertTemplateVersionTerraformValuesByJobID(ctx context.Context, arg InsertTemplateVersionTerraformValuesByJobIDParams) error {
_, err := q.db.ExecContext(ctx, insertTemplateVersionTerraformValuesByJobID,
arg.JobID,
arg.CachedPlan,
arg.CachedModuleFiles,
arg.UpdatedAt,
arg.ProvisionerdVersion,
)
return err
}
const getTemplateVersionVariables = `-- name: GetTemplateVersionVariables :many
SELECT template_version_id, name, description, type, value, default_value, required, sensitive FROM template_version_variables WHERE template_version_id = $1 ORDER BY name
`
func (q *sqlQuerier) GetTemplateVersionVariables(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionVariable, error) {
rows, err := q.db.QueryContext(ctx, getTemplateVersionVariables, templateVersionID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []TemplateVersionVariable
for rows.Next() {
var i TemplateVersionVariable
if err := rows.Scan(
&i.TemplateVersionID,
&i.Name,
&i.Description,
&i.Type,
&i.Value,
&i.DefaultValue,
&i.Required,
&i.Sensitive,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertTemplateVersionVariable = `-- name: InsertTemplateVersionVariable :one
INSERT INTO
template_version_variables (
template_version_id,
name,
description,
type,
value,
default_value,
required,
sensitive
)
VALUES
(
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8
) RETURNING template_version_id, name, description, type, value, default_value, required, sensitive
`
type InsertTemplateVersionVariableParams struct {
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
Name string `db:"name" json:"name"`
Description string `db:"description" json:"description"`
Type string `db:"type" json:"type"`
Value string `db:"value" json:"value"`
DefaultValue string `db:"default_value" json:"default_value"`
Required bool `db:"required" json:"required"`
Sensitive bool `db:"sensitive" json:"sensitive"`
}
func (q *sqlQuerier) InsertTemplateVersionVariable(ctx context.Context, arg InsertTemplateVersionVariableParams) (TemplateVersionVariable, error) {
row := q.db.QueryRowContext(ctx, insertTemplateVersionVariable,
arg.TemplateVersionID,
arg.Name,
arg.Description,
arg.Type,
arg.Value,
arg.DefaultValue,
arg.Required,
arg.Sensitive,
)
var i TemplateVersionVariable
err := row.Scan(
&i.TemplateVersionID,
&i.Name,
&i.Description,
&i.Type,
&i.Value,
&i.DefaultValue,
&i.Required,
&i.Sensitive,
)
return i, err
}
const getTemplateVersionWorkspaceTags = `-- name: GetTemplateVersionWorkspaceTags :many
SELECT template_version_id, key, value FROM template_version_workspace_tags WHERE template_version_id = $1 ORDER BY LOWER(key) ASC
`
func (q *sqlQuerier) GetTemplateVersionWorkspaceTags(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionWorkspaceTag, error) {
rows, err := q.db.QueryContext(ctx, getTemplateVersionWorkspaceTags, templateVersionID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []TemplateVersionWorkspaceTag
for rows.Next() {
var i TemplateVersionWorkspaceTag
if err := rows.Scan(&i.TemplateVersionID, &i.Key, &i.Value); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertTemplateVersionWorkspaceTag = `-- name: InsertTemplateVersionWorkspaceTag :one
INSERT INTO
template_version_workspace_tags (
template_version_id,
key,
value
)
VALUES
(
$1,
$2,
$3
) RETURNING template_version_id, key, value
`
type InsertTemplateVersionWorkspaceTagParams struct {
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
Key string `db:"key" json:"key"`
Value string `db:"value" json:"value"`
}
func (q *sqlQuerier) InsertTemplateVersionWorkspaceTag(ctx context.Context, arg InsertTemplateVersionWorkspaceTagParams) (TemplateVersionWorkspaceTag, error) {
row := q.db.QueryRowContext(ctx, insertTemplateVersionWorkspaceTag, arg.TemplateVersionID, arg.Key, arg.Value)
var i TemplateVersionWorkspaceTag
err := row.Scan(&i.TemplateVersionID, &i.Key, &i.Value)
return i, err
}
const disableForeignKeysAndTriggers = `-- name: DisableForeignKeysAndTriggers :exec
DO $$
DECLARE
table_record record;
BEGIN
FOR table_record IN
SELECT table_schema, table_name
FROM information_schema.tables
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
AND table_type = 'BASE TABLE'
LOOP
EXECUTE format('ALTER TABLE %I.%I DISABLE TRIGGER ALL',
table_record.table_schema,
table_record.table_name);
END LOOP;
END;
$$
`
// Disable foreign keys and triggers for all tables.
// Deprecated: disable foreign keys was created to aid in migrating off
// of the test-only in-memory database. Do not use this in new code.
func (q *sqlQuerier) DisableForeignKeysAndTriggers(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, disableForeignKeysAndTriggers)
return err
}
const getTotalUsageDCManagedAgentsV1 = `-- name: GetTotalUsageDCManagedAgentsV1 :one
SELECT
-- The first cast is necessary since you can't sum strings, and the second
-- cast is necessary to make sqlc happy.
COALESCE(SUM((usage_data->>'count')::bigint), 0)::bigint AS total_count
FROM
usage_events_daily
WHERE
event_type = 'dc_managed_agents_v1'
-- Parentheses are necessary to avoid sqlc from generating an extra
-- argument.
AND day BETWEEN date_trunc('day', ($1::timestamptz) AT TIME ZONE 'UTC')::date AND date_trunc('day', ($2::timestamptz) AT TIME ZONE 'UTC')::date
`
type GetTotalUsageDCManagedAgentsV1Params struct {
StartDate time.Time `db:"start_date" json:"start_date"`
EndDate time.Time `db:"end_date" json:"end_date"`
}
// Gets the total number of managed agents created between two dates. Uses the
// aggregate table to avoid large scans or a complex index on the usage_events
// table.
//
// This has the trade off that we can't count accurately between two exact
// timestamps. The provided timestamps will be converted to UTC and truncated to
// the events that happened on and between the two dates. Both dates are
// inclusive.
func (q *sqlQuerier) GetTotalUsageDCManagedAgentsV1(ctx context.Context, arg GetTotalUsageDCManagedAgentsV1Params) (int64, error) {
row := q.db.QueryRowContext(ctx, getTotalUsageDCManagedAgentsV1, arg.StartDate, arg.EndDate)
var total_count int64
err := row.Scan(&total_count)
return total_count, err
}
const insertUsageEvent = `-- name: InsertUsageEvent :exec
INSERT INTO
usage_events (
id,
event_type,
event_data,
created_at,
publish_started_at,
published_at,
failure_message
)
VALUES
($1, $2, $3, $4, NULL, NULL, NULL)
ON CONFLICT (id) DO NOTHING
`
type InsertUsageEventParams struct {
ID string `db:"id" json:"id"`
EventType string `db:"event_type" json:"event_type"`
EventData json.RawMessage `db:"event_data" json:"event_data"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
// Duplicate events are ignored intentionally to allow for multiple replicas to
// publish heartbeat events.
func (q *sqlQuerier) InsertUsageEvent(ctx context.Context, arg InsertUsageEventParams) error {
_, err := q.db.ExecContext(ctx, insertUsageEvent,
arg.ID,
arg.EventType,
arg.EventData,
arg.CreatedAt,
)
return err
}
const listUsageEventCreatedAtsByTypeSince = `-- name: ListUsageEventCreatedAtsByTypeSince :many
SELECT created_at
FROM usage_events
WHERE event_type = $1
AND created_at >= $2::timestamptz
`
type ListUsageEventCreatedAtsByTypeSinceParams struct {
EventType string `db:"event_type" json:"event_type"`
Since time.Time `db:"since" json:"since"`
}
// Used by the usage generator to find missing heartbeat buckets.
func (q *sqlQuerier) ListUsageEventCreatedAtsByTypeSince(ctx context.Context, arg ListUsageEventCreatedAtsByTypeSinceParams) ([]time.Time, error) {
rows, err := q.db.QueryContext(ctx, listUsageEventCreatedAtsByTypeSince, arg.EventType, arg.Since)
if err != nil {
return nil, err
}
defer rows.Close()
var items []time.Time
for rows.Next() {
var created_at time.Time
if err := rows.Scan(&created_at); err != nil {
return nil, err
}
items = append(items, created_at)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const selectUsageEventsForPublishing = `-- name: SelectUsageEventsForPublishing :many
WITH usage_events AS (
UPDATE
usage_events
SET
publish_started_at = $1::timestamptz
WHERE
id IN (
SELECT
potential_event.id
FROM
usage_events potential_event
WHERE
-- Do not publish events that have already been published or
-- have permanently failed to publish.
potential_event.published_at IS NULL
-- Do not publish events that are already being published by
-- another replica.
AND (
potential_event.publish_started_at IS NULL
-- If the event has publish_started_at set, it must be older
-- than an hour ago. This is so we can retry publishing
-- events where the replica exited or couldn't update the
-- row.
-- The parentheses around @now::timestamptz are necessary to
-- avoid sqlc from generating an extra argument.
OR potential_event.publish_started_at < ($1::timestamptz) - INTERVAL '1 hour'
)
-- Do not publish events older than 30 days. Tallyman will
-- always permanently reject these events anyways. This is to
-- avoid duplicate events being billed to customers, as
-- Metronome will only deduplicate events within 34 days.
-- Also, the same parentheses thing here as above.
AND potential_event.created_at > ($1::timestamptz) - INTERVAL '30 days'
ORDER BY potential_event.created_at ASC
FOR UPDATE SKIP LOCKED
LIMIT 100
)
RETURNING id, event_type, event_data, created_at, publish_started_at, published_at, failure_message
)
SELECT id, event_type, event_data, created_at, publish_started_at, published_at, failure_message
FROM usage_events
ORDER BY created_at ASC
`
// Note that this selects from the CTE, not the original table. The CTE is named
// the same as the original table to trick sqlc into reusing the existing struct
// for the table.
// The CTE and the reorder is required because UPDATE doesn't guarantee order.
func (q *sqlQuerier) SelectUsageEventsForPublishing(ctx context.Context, now time.Time) ([]UsageEvent, error) {
rows, err := q.db.QueryContext(ctx, selectUsageEventsForPublishing, now)
if err != nil {
return nil, err
}
defer rows.Close()
var items []UsageEvent
for rows.Next() {
var i UsageEvent
if err := rows.Scan(
&i.ID,
&i.EventType,
&i.EventData,
&i.CreatedAt,
&i.PublishStartedAt,
&i.PublishedAt,
&i.FailureMessage,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateUsageEventsPostPublish = `-- name: UpdateUsageEventsPostPublish :exec
UPDATE
usage_events
SET
publish_started_at = NULL,
published_at = CASE WHEN input.set_published_at THEN $1::timestamptz ELSE NULL END,
failure_message = NULLIF(input.failure_message, '')
FROM (
SELECT
UNNEST($2::text[]) AS id,
UNNEST($3::text[]) AS failure_message,
UNNEST($4::boolean[]) AS set_published_at
) input
WHERE
input.id = usage_events.id
-- If the number of ids, failure messages, and set published ats are not the
-- same, do not do anything. Unfortunately you can't really throw from a
-- query without writing a function or doing some jank like dividing by
-- zero, so this is the best we can do.
AND cardinality($2::text[]) = cardinality($3::text[])
AND cardinality($2::text[]) = cardinality($4::boolean[])
`
type UpdateUsageEventsPostPublishParams struct {
Now time.Time `db:"now" json:"now"`
IDs []string `db:"ids" json:"ids"`
FailureMessages []string `db:"failure_messages" json:"failure_messages"`
SetPublishedAts []bool `db:"set_published_ats" json:"set_published_ats"`
}
func (q *sqlQuerier) UpdateUsageEventsPostPublish(ctx context.Context, arg UpdateUsageEventsPostPublishParams) error {
_, err := q.db.ExecContext(ctx, updateUsageEventsPostPublish,
arg.Now,
pq.Array(arg.IDs),
pq.Array(arg.FailureMessages),
pq.Array(arg.SetPublishedAts),
)
return err
}
const usageEventExistsByID = `-- name: UsageEventExistsByID :one
SELECT EXISTS(
SELECT 1 FROM usage_events WHERE id = $1
)::bool
`
func (q *sqlQuerier) UsageEventExistsByID(ctx context.Context, id string) (bool, error) {
row := q.db.QueryRowContext(ctx, usageEventExistsByID, id)
var column_1 bool
err := row.Scan(&column_1)
return column_1, err
}
const deleteUserAIProviderKey = `-- name: DeleteUserAIProviderKey :exec
DELETE FROM
user_ai_provider_keys
WHERE
user_id = $1::uuid
AND ai_provider_id = $2::uuid
`
type DeleteUserAIProviderKeyParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
AIProviderID uuid.UUID `db:"ai_provider_id" json:"ai_provider_id"`
}
func (q *sqlQuerier) DeleteUserAIProviderKey(ctx context.Context, arg DeleteUserAIProviderKeyParams) error {
_, err := q.db.ExecContext(ctx, deleteUserAIProviderKey, arg.UserID, arg.AIProviderID)
return err
}
const deleteUserAIProviderKeysByProviderID = `-- name: DeleteUserAIProviderKeysByProviderID :exec
DELETE FROM
user_ai_provider_keys
WHERE
ai_provider_id = $1::uuid
`
func (q *sqlQuerier) DeleteUserAIProviderKeysByProviderID(ctx context.Context, aiProviderID uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteUserAIProviderKeysByProviderID, aiProviderID)
return err
}
const getUserAIProviderKeyByProviderID = `-- name: GetUserAIProviderKeyByProviderID :one
SELECT
id, user_id, ai_provider_id, api_key, api_key_key_id, created_at, updated_at
FROM
user_ai_provider_keys
WHERE
user_id = $1::uuid
AND ai_provider_id = $2::uuid
`
type GetUserAIProviderKeyByProviderIDParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
AIProviderID uuid.UUID `db:"ai_provider_id" json:"ai_provider_id"`
}
func (q *sqlQuerier) GetUserAIProviderKeyByProviderID(ctx context.Context, arg GetUserAIProviderKeyByProviderIDParams) (UserAIProviderKey, error) {
row := q.db.QueryRowContext(ctx, getUserAIProviderKeyByProviderID, arg.UserID, arg.AIProviderID)
var i UserAIProviderKey
err := row.Scan(
&i.ID,
&i.UserID,
&i.AIProviderID,
&i.APIKey,
&i.ApiKeyKeyID,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getUserAIProviderKeys = `-- name: GetUserAIProviderKeys :many
SELECT
id, user_id, ai_provider_id, api_key, api_key_key_id, created_at, updated_at
FROM
user_ai_provider_keys
ORDER BY
user_id ASC,
ai_provider_id ASC,
created_at ASC,
id ASC
`
// GetUserAIProviderKeys is used by dbcrypt key rotation. Request paths should use
// user-scoped lookups instead of this bulk accessor.
func (q *sqlQuerier) GetUserAIProviderKeys(ctx context.Context) ([]UserAIProviderKey, error) {
rows, err := q.db.QueryContext(ctx, getUserAIProviderKeys)
if err != nil {
return nil, err
}
defer rows.Close()
var items []UserAIProviderKey
for rows.Next() {
var i UserAIProviderKey
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.AIProviderID,
&i.APIKey,
&i.ApiKeyKeyID,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getUserAIProviderKeysByUserID = `-- name: GetUserAIProviderKeysByUserID :many
SELECT
id, user_id, ai_provider_id, api_key, api_key_key_id, created_at, updated_at
FROM
user_ai_provider_keys
WHERE
user_id = $1::uuid
ORDER BY
ai_provider_id ASC,
created_at ASC,
id ASC
`
func (q *sqlQuerier) GetUserAIProviderKeysByUserID(ctx context.Context, userID uuid.UUID) ([]UserAIProviderKey, error) {
rows, err := q.db.QueryContext(ctx, getUserAIProviderKeysByUserID, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []UserAIProviderKey
for rows.Next() {
var i UserAIProviderKey
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.AIProviderID,
&i.APIKey,
&i.ApiKeyKeyID,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateEncryptedUserAIProviderKey = `-- name: UpdateEncryptedUserAIProviderKey :one
UPDATE
user_ai_provider_keys
SET
api_key = $1::text,
api_key_key_id = $2::text,
updated_at = NOW()
WHERE
id = $3::uuid
RETURNING
id, user_id, ai_provider_id, api_key, api_key_key_id, created_at, updated_at
`
type UpdateEncryptedUserAIProviderKeyParams struct {
APIKey string `db:"api_key" json:"api_key"`
ApiKeyKeyID sql.NullString `db:"api_key_key_id" json:"api_key_key_id"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateEncryptedUserAIProviderKey(ctx context.Context, arg UpdateEncryptedUserAIProviderKeyParams) (UserAIProviderKey, error) {
row := q.db.QueryRowContext(ctx, updateEncryptedUserAIProviderKey, arg.APIKey, arg.ApiKeyKeyID, arg.ID)
var i UserAIProviderKey
err := row.Scan(
&i.ID,
&i.UserID,
&i.AIProviderID,
&i.APIKey,
&i.ApiKeyKeyID,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const updateUserAIProviderKey = `-- name: UpdateUserAIProviderKey :one
UPDATE
user_ai_provider_keys
SET
api_key = $1::text,
api_key_key_id = $2::text,
updated_at = NOW()
WHERE
user_id = $3::uuid
AND ai_provider_id = $4::uuid
RETURNING
id, user_id, ai_provider_id, api_key, api_key_key_id, created_at, updated_at
`
type UpdateUserAIProviderKeyParams struct {
APIKey string `db:"api_key" json:"api_key"`
ApiKeyKeyID sql.NullString `db:"api_key_key_id" json:"api_key_key_id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
AIProviderID uuid.UUID `db:"ai_provider_id" json:"ai_provider_id"`
}
func (q *sqlQuerier) UpdateUserAIProviderKey(ctx context.Context, arg UpdateUserAIProviderKeyParams) (UserAIProviderKey, error) {
row := q.db.QueryRowContext(ctx, updateUserAIProviderKey,
arg.APIKey,
arg.ApiKeyKeyID,
arg.UserID,
arg.AIProviderID,
)
var i UserAIProviderKey
err := row.Scan(
&i.ID,
&i.UserID,
&i.AIProviderID,
&i.APIKey,
&i.ApiKeyKeyID,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const upsertUserAIProviderKey = `-- name: UpsertUserAIProviderKey :one
INSERT INTO user_ai_provider_keys (
id,
user_id,
ai_provider_id,
api_key,
api_key_key_id,
created_at,
updated_at
) VALUES (
$1::uuid,
$2::uuid,
$3::uuid,
$4::text,
$5::text,
$6::timestamptz,
$7::timestamptz
)
ON CONFLICT (user_id, ai_provider_id) DO UPDATE
SET
api_key = EXCLUDED.api_key,
api_key_key_id = EXCLUDED.api_key_key_id,
updated_at = EXCLUDED.updated_at
RETURNING
id, user_id, ai_provider_id, api_key, api_key_key_id, created_at, updated_at
`
type UpsertUserAIProviderKeyParams struct {
ID uuid.UUID `db:"id" json:"id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
AIProviderID uuid.UUID `db:"ai_provider_id" json:"ai_provider_id"`
APIKey string `db:"api_key" json:"api_key"`
ApiKeyKeyID sql.NullString `db:"api_key_key_id" json:"api_key_key_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
// UpsertUserAIProviderKey preserves the original id and created_at when the
// user/provider pair already exists. On conflict, callers provide id and
// created_at for the insert path only.
func (q *sqlQuerier) UpsertUserAIProviderKey(ctx context.Context, arg UpsertUserAIProviderKeyParams) (UserAIProviderKey, error) {
row := q.db.QueryRowContext(ctx, upsertUserAIProviderKey,
arg.ID,
arg.UserID,
arg.AIProviderID,
arg.APIKey,
arg.ApiKeyKeyID,
arg.CreatedAt,
arg.UpdatedAt,
)
var i UserAIProviderKey
err := row.Scan(
&i.ID,
&i.UserID,
&i.AIProviderID,
&i.APIKey,
&i.ApiKeyKeyID,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const countOIDCLinkedIDsByIssuer = `-- name: CountOIDCLinkedIDsByIssuer :many
SELECT
(CASE
WHEN user_links.linked_id = '' THEN ''
ELSE split_part(user_links.linked_id, '||', 1)
END)::text AS issuer_prefix,
COUNT(*)::int AS count
FROM
user_links
INNER JOIN
users ON user_links.user_id = users.id
WHERE
user_links.login_type = 'oidc'
AND users.deleted = false
GROUP BY issuer_prefix
`
type CountOIDCLinkedIDsByIssuerRow struct {
IssuerPrefix string `db:"issuer_prefix" json:"issuer_prefix"`
Count int32 `db:"count" json:"count"`
}
// Groups OIDC user links by their issuer prefix (the part before "||" in
// linked_id) and returns a count for each. Empty linked_ids are reported
// with an empty issuer_prefix. Used for analysis before resetting
// mismatched links.
func (q *sqlQuerier) CountOIDCLinkedIDsByIssuer(ctx context.Context) ([]CountOIDCLinkedIDsByIssuerRow, error) {
rows, err := q.db.QueryContext(ctx, countOIDCLinkedIDsByIssuer)
if err != nil {
return nil, err
}
defer rows.Close()
var items []CountOIDCLinkedIDsByIssuerRow
for rows.Next() {
var i CountOIDCLinkedIDsByIssuerRow
if err := rows.Scan(&i.IssuerPrefix, &i.Count); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getUserLinkByLinkedID = `-- name: GetUserLinkByLinkedID :one
SELECT
user_links.user_id, user_links.login_type, user_links.linked_id, user_links.oauth_access_token, user_links.oauth_refresh_token, user_links.oauth_expiry, user_links.oauth_access_token_key_id, user_links.oauth_refresh_token_key_id, user_links.claims
FROM
user_links
INNER JOIN
users ON user_links.user_id = users.id
WHERE
linked_id = $1
AND
deleted = false
`
func (q *sqlQuerier) GetUserLinkByLinkedID(ctx context.Context, linkedID string) (UserLink, error) {
row := q.db.QueryRowContext(ctx, getUserLinkByLinkedID, linkedID)
var i UserLink
err := row.Scan(
&i.UserID,
&i.LoginType,
&i.LinkedID,
&i.OAuthAccessToken,
&i.OAuthRefreshToken,
&i.OAuthExpiry,
&i.OAuthAccessTokenKeyID,
&i.OAuthRefreshTokenKeyID,
&i.Claims,
)
return i, err
}
const getUserLinkByUserIDLoginType = `-- name: GetUserLinkByUserIDLoginType :one
SELECT
user_id, login_type, linked_id, oauth_access_token, oauth_refresh_token, oauth_expiry, oauth_access_token_key_id, oauth_refresh_token_key_id, claims
FROM
user_links
WHERE
user_id = $1 AND login_type = $2
`
type GetUserLinkByUserIDLoginTypeParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
LoginType LoginType `db:"login_type" json:"login_type"`
}
func (q *sqlQuerier) GetUserLinkByUserIDLoginType(ctx context.Context, arg GetUserLinkByUserIDLoginTypeParams) (UserLink, error) {
row := q.db.QueryRowContext(ctx, getUserLinkByUserIDLoginType, arg.UserID, arg.LoginType)
var i UserLink
err := row.Scan(
&i.UserID,
&i.LoginType,
&i.LinkedID,
&i.OAuthAccessToken,
&i.OAuthRefreshToken,
&i.OAuthExpiry,
&i.OAuthAccessTokenKeyID,
&i.OAuthRefreshTokenKeyID,
&i.Claims,
)
return i, err
}
const getUserLinksByUserID = `-- name: GetUserLinksByUserID :many
SELECT user_id, login_type, linked_id, oauth_access_token, oauth_refresh_token, oauth_expiry, oauth_access_token_key_id, oauth_refresh_token_key_id, claims FROM user_links WHERE user_id = $1
`
func (q *sqlQuerier) GetUserLinksByUserID(ctx context.Context, userID uuid.UUID) ([]UserLink, error) {
rows, err := q.db.QueryContext(ctx, getUserLinksByUserID, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []UserLink
for rows.Next() {
var i UserLink
if err := rows.Scan(
&i.UserID,
&i.LoginType,
&i.LinkedID,
&i.OAuthAccessToken,
&i.OAuthRefreshToken,
&i.OAuthExpiry,
&i.OAuthAccessTokenKeyID,
&i.OAuthRefreshTokenKeyID,
&i.Claims,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertUserLink = `-- name: InsertUserLink :one
INSERT INTO
user_links (
user_id,
login_type,
linked_id,
oauth_access_token,
oauth_access_token_key_id,
oauth_refresh_token,
oauth_refresh_token_key_id,
oauth_expiry,
claims
)
VALUES
( $1, $2, $3, $4, $5, $6, $7, $8, $9 ) RETURNING user_id, login_type, linked_id, oauth_access_token, oauth_refresh_token, oauth_expiry, oauth_access_token_key_id, oauth_refresh_token_key_id, claims
`
type InsertUserLinkParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
LoginType LoginType `db:"login_type" json:"login_type"`
LinkedID string `db:"linked_id" json:"linked_id"`
OAuthAccessToken string `db:"oauth_access_token" json:"oauth_access_token"`
OAuthAccessTokenKeyID sql.NullString `db:"oauth_access_token_key_id" json:"oauth_access_token_key_id"`
OAuthRefreshToken string `db:"oauth_refresh_token" json:"oauth_refresh_token"`
OAuthRefreshTokenKeyID sql.NullString `db:"oauth_refresh_token_key_id" json:"oauth_refresh_token_key_id"`
OAuthExpiry time.Time `db:"oauth_expiry" json:"oauth_expiry"`
Claims UserLinkClaims `db:"claims" json:"claims"`
}
func (q *sqlQuerier) InsertUserLink(ctx context.Context, arg InsertUserLinkParams) (UserLink, error) {
row := q.db.QueryRowContext(ctx, insertUserLink,
arg.UserID,
arg.LoginType,
arg.LinkedID,
arg.OAuthAccessToken,
arg.OAuthAccessTokenKeyID,
arg.OAuthRefreshToken,
arg.OAuthRefreshTokenKeyID,
arg.OAuthExpiry,
arg.Claims,
)
var i UserLink
err := row.Scan(
&i.UserID,
&i.LoginType,
&i.LinkedID,
&i.OAuthAccessToken,
&i.OAuthRefreshToken,
&i.OAuthExpiry,
&i.OAuthAccessTokenKeyID,
&i.OAuthRefreshTokenKeyID,
&i.Claims,
)
return i, err
}
const oIDCClaimFieldValues = `-- name: OIDCClaimFieldValues :many
SELECT
-- DISTINCT to remove duplicates
DISTINCT jsonb_array_elements_text(CASE
-- When the type is an array, filter out any non-string elements.
-- This is to keep the return type consistent.
WHEN jsonb_typeof(claims->'merged_claims'->$1::text) = 'array' THEN
(
SELECT
jsonb_agg(element)
FROM
jsonb_array_elements(claims->'merged_claims'->$1::text) AS element
WHERE
-- Filtering out non-string elements
jsonb_typeof(element) = 'string'
)
-- Some IDPs return a single string instead of an array of strings.
WHEN jsonb_typeof(claims->'merged_claims'->$1::text) = 'string' THEN
jsonb_build_array(claims->'merged_claims'->$1::text)
END)
FROM
user_links
WHERE
-- IDP sync only supports string and array (of string) types
jsonb_typeof(claims->'merged_claims'->$1::text) = ANY(ARRAY['string', 'array'])
AND login_type = 'oidc'
AND CASE
WHEN $2 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
user_links.user_id = ANY(SELECT organization_members.user_id FROM organization_members WHERE organization_id = $2)
ELSE true
END
`
type OIDCClaimFieldValuesParams struct {
ClaimField string `db:"claim_field" json:"claim_field"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
}
func (q *sqlQuerier) OIDCClaimFieldValues(ctx context.Context, arg OIDCClaimFieldValuesParams) ([]string, error) {
rows, err := q.db.QueryContext(ctx, oIDCClaimFieldValues, arg.ClaimField, arg.OrganizationID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []string
for rows.Next() {
var jsonb_array_elements_text string
if err := rows.Scan(&jsonb_array_elements_text); err != nil {
return nil, err
}
items = append(items, jsonb_array_elements_text)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const oIDCClaimFields = `-- name: OIDCClaimFields :many
SELECT
DISTINCT jsonb_object_keys(claims->'merged_claims')
FROM
user_links
WHERE
-- Only return rows where the top level key exists
claims ? 'merged_claims' AND
-- 'null' is the default value for the id_token_claims field
-- jsonb 'null' is not the same as SQL NULL. Strip these out.
jsonb_typeof(claims->'merged_claims') != 'null' AND
login_type = 'oidc'
AND CASE WHEN $1 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
user_links.user_id = ANY(SELECT organization_members.user_id FROM organization_members WHERE organization_id = $1)
ELSE true
END
`
// OIDCClaimFields returns a list of distinct keys in the the merged_claims fields.
// This query is used to generate the list of available sync fields for idp sync settings.
func (q *sqlQuerier) OIDCClaimFields(ctx context.Context, organizationID uuid.UUID) ([]string, error) {
rows, err := q.db.QueryContext(ctx, oIDCClaimFields, organizationID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []string
for rows.Next() {
var jsonb_object_keys string
if err := rows.Scan(&jsonb_object_keys); err != nil {
return nil, err
}
items = append(items, jsonb_object_keys)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const unlinkOIDCUsersByIssuerMismatch = `-- name: UnlinkOIDCUsersByIssuerMismatch :execrows
UPDATE user_links
SET linked_id = ''
FROM users
WHERE user_links.user_id = users.id
AND user_links.login_type = 'oidc'
AND user_links.linked_id != ''
AND NOT starts_with(user_links.linked_id, $1)
AND users.deleted = false
`
// Resets linked_id to ” for OIDC links where the linked_id is non-empty
// and does not begin with the expected issuer prefix. This allows users to
// re-authenticate under a new OIDC provider.
func (q *sqlQuerier) UnlinkOIDCUsersByIssuerMismatch(ctx context.Context, expectedPrefix string) (int64, error) {
result, err := q.db.ExecContext(ctx, unlinkOIDCUsersByIssuerMismatch, expectedPrefix)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const updateUserLink = `-- name: UpdateUserLink :one
UPDATE
user_links
SET
oauth_access_token = $1,
oauth_access_token_key_id = $2,
oauth_refresh_token = $3,
oauth_refresh_token_key_id = $4,
oauth_expiry = $5,
claims = $6
WHERE
user_id = $7 AND login_type = $8 RETURNING user_id, login_type, linked_id, oauth_access_token, oauth_refresh_token, oauth_expiry, oauth_access_token_key_id, oauth_refresh_token_key_id, claims
`
type UpdateUserLinkParams struct {
OAuthAccessToken string `db:"oauth_access_token" json:"oauth_access_token"`
OAuthAccessTokenKeyID sql.NullString `db:"oauth_access_token_key_id" json:"oauth_access_token_key_id"`
OAuthRefreshToken string `db:"oauth_refresh_token" json:"oauth_refresh_token"`
OAuthRefreshTokenKeyID sql.NullString `db:"oauth_refresh_token_key_id" json:"oauth_refresh_token_key_id"`
OAuthExpiry time.Time `db:"oauth_expiry" json:"oauth_expiry"`
Claims UserLinkClaims `db:"claims" json:"claims"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
LoginType LoginType `db:"login_type" json:"login_type"`
}
func (q *sqlQuerier) UpdateUserLink(ctx context.Context, arg UpdateUserLinkParams) (UserLink, error) {
row := q.db.QueryRowContext(ctx, updateUserLink,
arg.OAuthAccessToken,
arg.OAuthAccessTokenKeyID,
arg.OAuthRefreshToken,
arg.OAuthRefreshTokenKeyID,
arg.OAuthExpiry,
arg.Claims,
arg.UserID,
arg.LoginType,
)
var i UserLink
err := row.Scan(
&i.UserID,
&i.LoginType,
&i.LinkedID,
&i.OAuthAccessToken,
&i.OAuthRefreshToken,
&i.OAuthExpiry,
&i.OAuthAccessTokenKeyID,
&i.OAuthRefreshTokenKeyID,
&i.Claims,
)
return i, err
}
const updateUserLinkedID = `-- name: UpdateUserLinkedID :one
UPDATE
user_links
SET
linked_id = $1
WHERE
user_id = $2 AND login_type = $3 AND linked_id = '' RETURNING user_id, login_type, linked_id, oauth_access_token, oauth_refresh_token, oauth_expiry, oauth_access_token_key_id, oauth_refresh_token_key_id, claims
`
type UpdateUserLinkedIDParams struct {
LinkedID string `db:"linked_id" json:"linked_id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
LoginType LoginType `db:"login_type" json:"login_type"`
}
// Backfills linked_id for legacy user_links that were created before
// linked_id tracking was added. Only updates when linked_id is empty
// to avoid overwriting a valid binding.
func (q *sqlQuerier) UpdateUserLinkedID(ctx context.Context, arg UpdateUserLinkedIDParams) (UserLink, error) {
row := q.db.QueryRowContext(ctx, updateUserLinkedID, arg.LinkedID, arg.UserID, arg.LoginType)
var i UserLink
err := row.Scan(
&i.UserID,
&i.LoginType,
&i.LinkedID,
&i.OAuthAccessToken,
&i.OAuthRefreshToken,
&i.OAuthExpiry,
&i.OAuthAccessTokenKeyID,
&i.OAuthRefreshTokenKeyID,
&i.Claims,
)
return i, err
}
const createUserSecret = `-- name: CreateUserSecret :one
INSERT INTO user_secrets (
id,
user_id,
name,
description,
value,
value_key_id,
env_name,
file_path,
enabled
) VALUES (
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
$9
) RETURNING id, user_id, name, description, value, env_name, file_path, created_at, updated_at, value_key_id, enabled
`
type CreateUserSecretParams struct {
ID uuid.UUID `db:"id" json:"id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
Name string `db:"name" json:"name"`
Description string `db:"description" json:"description"`
Value string `db:"value" json:"value"`
ValueKeyID sql.NullString `db:"value_key_id" json:"value_key_id"`
EnvName string `db:"env_name" json:"env_name"`
FilePath string `db:"file_path" json:"file_path"`
Enabled bool `db:"enabled" json:"enabled"`
}
func (q *sqlQuerier) CreateUserSecret(ctx context.Context, arg CreateUserSecretParams) (UserSecret, error) {
row := q.db.QueryRowContext(ctx, createUserSecret,
arg.ID,
arg.UserID,
arg.Name,
arg.Description,
arg.Value,
arg.ValueKeyID,
arg.EnvName,
arg.FilePath,
arg.Enabled,
)
var i UserSecret
err := row.Scan(
&i.ID,
&i.UserID,
&i.Name,
&i.Description,
&i.Value,
&i.EnvName,
&i.FilePath,
&i.CreatedAt,
&i.UpdatedAt,
&i.ValueKeyID,
&i.Enabled,
)
return i, err
}
const deleteUserSecretByUserIDAndName = `-- name: DeleteUserSecretByUserIDAndName :one
DELETE FROM user_secrets
WHERE user_id = $1 AND name = $2
RETURNING id, user_id, name, description, value, env_name, file_path, created_at, updated_at, value_key_id, enabled
`
type DeleteUserSecretByUserIDAndNameParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
Name string `db:"name" json:"name"`
}
func (q *sqlQuerier) DeleteUserSecretByUserIDAndName(ctx context.Context, arg DeleteUserSecretByUserIDAndNameParams) (UserSecret, error) {
row := q.db.QueryRowContext(ctx, deleteUserSecretByUserIDAndName, arg.UserID, arg.Name)
var i UserSecret
err := row.Scan(
&i.ID,
&i.UserID,
&i.Name,
&i.Description,
&i.Value,
&i.EnvName,
&i.FilePath,
&i.CreatedAt,
&i.UpdatedAt,
&i.ValueKeyID,
&i.Enabled,
)
return i, err
}
const getUserSecretByID = `-- name: GetUserSecretByID :one
SELECT id, user_id, name, description, value, env_name, file_path, created_at, updated_at, value_key_id, enabled
FROM user_secrets
WHERE id = $1
`
func (q *sqlQuerier) GetUserSecretByID(ctx context.Context, id uuid.UUID) (UserSecret, error) {
row := q.db.QueryRowContext(ctx, getUserSecretByID, id)
var i UserSecret
err := row.Scan(
&i.ID,
&i.UserID,
&i.Name,
&i.Description,
&i.Value,
&i.EnvName,
&i.FilePath,
&i.CreatedAt,
&i.UpdatedAt,
&i.ValueKeyID,
&i.Enabled,
)
return i, err
}
const getUserSecretByUserIDAndName = `-- name: GetUserSecretByUserIDAndName :one
SELECT id, user_id, name, description, value, env_name, file_path, created_at, updated_at, value_key_id, enabled
FROM user_secrets
WHERE user_id = $1 AND name = $2
`
type GetUserSecretByUserIDAndNameParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
Name string `db:"name" json:"name"`
}
func (q *sqlQuerier) GetUserSecretByUserIDAndName(ctx context.Context, arg GetUserSecretByUserIDAndNameParams) (UserSecret, error) {
row := q.db.QueryRowContext(ctx, getUserSecretByUserIDAndName, arg.UserID, arg.Name)
var i UserSecret
err := row.Scan(
&i.ID,
&i.UserID,
&i.Name,
&i.Description,
&i.Value,
&i.EnvName,
&i.FilePath,
&i.CreatedAt,
&i.UpdatedAt,
&i.ValueKeyID,
&i.Enabled,
)
return i, err
}
const getUserSecretsTelemetrySummary = `-- name: GetUserSecretsTelemetrySummary :one
WITH active_users AS (
SELECT id AS user_id
FROM users
WHERE deleted = false
AND is_system = false
AND status = 'active'::user_status
),
per_user AS (
SELECT au.user_id, COUNT(us.id)::bigint AS n
FROM active_users au
LEFT JOIN user_secrets us ON us.user_id = au.user_id
GROUP BY au.user_id
),
secrets_filtered AS (
SELECT us.env_name, us.file_path
FROM user_secrets us
JOIN active_users au ON au.user_id = us.user_id
)
SELECT
COUNT(*) FILTER (WHERE n > 0)::bigint AS users_with_secrets,
(SELECT COUNT(*) FROM secrets_filtered)::bigint AS total_secrets,
(SELECT COUNT(*) FROM secrets_filtered WHERE env_name != '' AND file_path = '' )::bigint AS env_name_only,
(SELECT COUNT(*) FROM secrets_filtered WHERE env_name = '' AND file_path != '')::bigint AS file_path_only,
(SELECT COUNT(*) FROM secrets_filtered WHERE env_name != '' AND file_path != '')::bigint AS both,
(SELECT COUNT(*) FROM secrets_filtered WHERE env_name = '' AND file_path = '' )::bigint AS neither,
COALESCE(MAX(n), 0)::bigint AS secrets_per_user_max,
COALESCE(percentile_disc(0.25) WITHIN GROUP (ORDER BY n), 0)::bigint AS secrets_per_user_p25,
COALESCE(percentile_disc(0.50) WITHIN GROUP (ORDER BY n), 0)::bigint AS secrets_per_user_p50,
COALESCE(percentile_disc(0.75) WITHIN GROUP (ORDER BY n), 0)::bigint AS secrets_per_user_p75,
COALESCE(percentile_disc(0.90) WITHIN GROUP (ORDER BY n), 0)::bigint AS secrets_per_user_p90
FROM per_user
`
type GetUserSecretsTelemetrySummaryRow struct {
UsersWithSecrets int64 `db:"users_with_secrets" json:"users_with_secrets"`
TotalSecrets int64 `db:"total_secrets" json:"total_secrets"`
EnvNameOnly int64 `db:"env_name_only" json:"env_name_only"`
FilePathOnly int64 `db:"file_path_only" json:"file_path_only"`
Both int64 `db:"both" json:"both"`
Neither int64 `db:"neither" json:"neither"`
SecretsPerUserMax int64 `db:"secrets_per_user_max" json:"secrets_per_user_max"`
SecretsPerUserP25 int64 `db:"secrets_per_user_p25" json:"secrets_per_user_p25"`
SecretsPerUserP50 int64 `db:"secrets_per_user_p50" json:"secrets_per_user_p50"`
SecretsPerUserP75 int64 `db:"secrets_per_user_p75" json:"secrets_per_user_p75"`
SecretsPerUserP90 int64 `db:"secrets_per_user_p90" json:"secrets_per_user_p90"`
}
// Returns deployment-wide aggregates for the telemetry snapshot.
//
// The denominator for both user-level counts and the per-user
// distribution is active non-system users. Specifically:
//
// - deleted = false: Coder soft-deletes by flipping users.deleted
// rather than removing rows. The delete_deleted_user_resources()
// trigger now removes their user_secrets, but soft-deleted users
// are still excluded here so they don't dilute the percentile
// distribution as zero-secret entries.
// - status = 'active': dormant users (no recent activity) and
// suspended users (explicitly disabled) cannot use secrets, so
// they shouldn't dilute the percentile distribution as
// zero-secret entries.
// - is_system = false: internal subjects like the prebuilds user
// never use secrets in the normal flow.
//
// Status transitions move users in and out of this denominator, so a
// snapshot's UsersWithSecrets can drop without any secret being
// deleted.
//
// The percentile distribution is computed across all active non-system
// users, including those with zero secrets, so the percentiles reflect
// deployment-wide adoption rather than only the power-user subset.
// percentile_disc returns an actual integer count from the underlying
// values rather than interpolating between rows.
func (q *sqlQuerier) GetUserSecretsTelemetrySummary(ctx context.Context) (GetUserSecretsTelemetrySummaryRow, error) {
row := q.db.QueryRowContext(ctx, getUserSecretsTelemetrySummary)
var i GetUserSecretsTelemetrySummaryRow
err := row.Scan(
&i.UsersWithSecrets,
&i.TotalSecrets,
&i.EnvNameOnly,
&i.FilePathOnly,
&i.Both,
&i.Neither,
&i.SecretsPerUserMax,
&i.SecretsPerUserP25,
&i.SecretsPerUserP50,
&i.SecretsPerUserP75,
&i.SecretsPerUserP90,
)
return i, err
}
const listUserSecrets = `-- name: ListUserSecrets :many
SELECT
id, user_id, name, description,
env_name, file_path, enabled,
created_at, updated_at
FROM user_secrets
WHERE user_id = $1
ORDER BY name ASC
`
type ListUserSecretsRow struct {
ID uuid.UUID `db:"id" json:"id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
Name string `db:"name" json:"name"`
Description string `db:"description" json:"description"`
EnvName string `db:"env_name" json:"env_name"`
FilePath string `db:"file_path" json:"file_path"`
Enabled bool `db:"enabled" json:"enabled"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
// Returns metadata only (no value or value_key_id) for the
// REST API list and get endpoints.
func (q *sqlQuerier) ListUserSecrets(ctx context.Context, userID uuid.UUID) ([]ListUserSecretsRow, error) {
rows, err := q.db.QueryContext(ctx, listUserSecrets, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListUserSecretsRow
for rows.Next() {
var i ListUserSecretsRow
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.Name,
&i.Description,
&i.EnvName,
&i.FilePath,
&i.Enabled,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listUserSecretsWithValues = `-- name: ListUserSecretsWithValues :many
SELECT id, user_id, name, description, value, env_name, file_path, created_at, updated_at, value_key_id, enabled
FROM user_secrets
WHERE user_id = $1
ORDER BY name ASC
`
// Returns all columns including the secret value. Used by the
// provisioner (build-time injection) and the agent manifest
// (runtime injection).
func (q *sqlQuerier) ListUserSecretsWithValues(ctx context.Context, userID uuid.UUID) ([]UserSecret, error) {
rows, err := q.db.QueryContext(ctx, listUserSecretsWithValues, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []UserSecret
for rows.Next() {
var i UserSecret
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.Name,
&i.Description,
&i.Value,
&i.EnvName,
&i.FilePath,
&i.CreatedAt,
&i.UpdatedAt,
&i.ValueKeyID,
&i.Enabled,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateUserSecretByUserIDAndName = `-- name: UpdateUserSecretByUserIDAndName :one
UPDATE user_secrets
SET
value = CASE WHEN $1::bool THEN $2 ELSE value END,
value_key_id = CASE WHEN $1::bool THEN $3 ELSE value_key_id END,
description = CASE WHEN $4::bool THEN $5 ELSE description END,
env_name = CASE WHEN $6::bool THEN $7 ELSE env_name END,
file_path = CASE WHEN $8::bool THEN $9 ELSE file_path END,
enabled = CASE WHEN $10::bool THEN $11 ELSE enabled END,
updated_at = CURRENT_TIMESTAMP
WHERE user_id = $12 AND name = $13
RETURNING id, user_id, name, description, value, env_name, file_path, created_at, updated_at, value_key_id, enabled
`
type UpdateUserSecretByUserIDAndNameParams struct {
UpdateValue bool `db:"update_value" json:"update_value"`
Value string `db:"value" json:"value"`
ValueKeyID sql.NullString `db:"value_key_id" json:"value_key_id"`
UpdateDescription bool `db:"update_description" json:"update_description"`
Description string `db:"description" json:"description"`
UpdateEnvName bool `db:"update_env_name" json:"update_env_name"`
EnvName string `db:"env_name" json:"env_name"`
UpdateFilePath bool `db:"update_file_path" json:"update_file_path"`
FilePath string `db:"file_path" json:"file_path"`
UpdateEnabled bool `db:"update_enabled" json:"update_enabled"`
Enabled bool `db:"enabled" json:"enabled"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
Name string `db:"name" json:"name"`
}
func (q *sqlQuerier) UpdateUserSecretByUserIDAndName(ctx context.Context, arg UpdateUserSecretByUserIDAndNameParams) (UserSecret, error) {
row := q.db.QueryRowContext(ctx, updateUserSecretByUserIDAndName,
arg.UpdateValue,
arg.Value,
arg.ValueKeyID,
arg.UpdateDescription,
arg.Description,
arg.UpdateEnvName,
arg.EnvName,
arg.UpdateFilePath,
arg.FilePath,
arg.UpdateEnabled,
arg.Enabled,
arg.UserID,
arg.Name,
)
var i UserSecret
err := row.Scan(
&i.ID,
&i.UserID,
&i.Name,
&i.Description,
&i.Value,
&i.EnvName,
&i.FilePath,
&i.CreatedAt,
&i.UpdatedAt,
&i.ValueKeyID,
&i.Enabled,
)
return i, err
}
const deleteUserSkillByUserIDAndName = `-- name: DeleteUserSkillByUserIDAndName :one
DELETE FROM user_skills
WHERE user_id = $1 AND name = $2
RETURNING id, user_id, name, description, content, created_at, updated_at
`
type DeleteUserSkillByUserIDAndNameParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
Name string `db:"name" json:"name"`
}
func (q *sqlQuerier) DeleteUserSkillByUserIDAndName(ctx context.Context, arg DeleteUserSkillByUserIDAndNameParams) (UserSkill, error) {
row := q.db.QueryRowContext(ctx, deleteUserSkillByUserIDAndName, arg.UserID, arg.Name)
var i UserSkill
err := row.Scan(
&i.ID,
&i.UserID,
&i.Name,
&i.Description,
&i.Content,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getUserSkillByUserIDAndName = `-- name: GetUserSkillByUserIDAndName :one
SELECT id, user_id, name, description, content, created_at, updated_at
FROM user_skills
WHERE user_id = $1 AND name = $2
`
type GetUserSkillByUserIDAndNameParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
Name string `db:"name" json:"name"`
}
func (q *sqlQuerier) GetUserSkillByUserIDAndName(ctx context.Context, arg GetUserSkillByUserIDAndNameParams) (UserSkill, error) {
row := q.db.QueryRowContext(ctx, getUserSkillByUserIDAndName, arg.UserID, arg.Name)
var i UserSkill
err := row.Scan(
&i.ID,
&i.UserID,
&i.Name,
&i.Description,
&i.Content,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const insertUserSkill = `-- name: InsertUserSkill :one
INSERT INTO user_skills (id, user_id, name, description, content)
VALUES ($1::uuid, $2::uuid, $3::text, $4::text, $5::text)
RETURNING id, user_id, name, description, content, created_at, updated_at
`
type InsertUserSkillParams struct {
ID uuid.UUID `db:"id" json:"id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
Name string `db:"name" json:"name"`
Description string `db:"description" json:"description"`
Content string `db:"content" json:"content"`
}
func (q *sqlQuerier) InsertUserSkill(ctx context.Context, arg InsertUserSkillParams) (UserSkill, error) {
row := q.db.QueryRowContext(ctx, insertUserSkill,
arg.ID,
arg.UserID,
arg.Name,
arg.Description,
arg.Content,
)
var i UserSkill
err := row.Scan(
&i.ID,
&i.UserID,
&i.Name,
&i.Description,
&i.Content,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const listUserSkillMetadataByUserID = `-- name: ListUserSkillMetadataByUserID :many
SELECT
id, user_id, name, description, created_at, updated_at
FROM user_skills
WHERE user_id = $1
ORDER BY name ASC
`
type ListUserSkillMetadataByUserIDRow struct {
ID uuid.UUID `db:"id" json:"id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
Name string `db:"name" json:"name"`
Description string `db:"description" json:"description"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
func (q *sqlQuerier) ListUserSkillMetadataByUserID(ctx context.Context, userID uuid.UUID) ([]ListUserSkillMetadataByUserIDRow, error) {
rows, err := q.db.QueryContext(ctx, listUserSkillMetadataByUserID, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListUserSkillMetadataByUserIDRow
for rows.Next() {
var i ListUserSkillMetadataByUserIDRow
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.Name,
&i.Description,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateUserSkillByUserIDAndName = `-- name: UpdateUserSkillByUserIDAndName :one
UPDATE user_skills
SET
description = $1,
content = $2,
updated_at = now()
WHERE user_id = $3 AND name = $4
RETURNING id, user_id, name, description, content, created_at, updated_at
`
type UpdateUserSkillByUserIDAndNameParams struct {
Description string `db:"description" json:"description"`
Content string `db:"content" json:"content"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
Name string `db:"name" json:"name"`
}
func (q *sqlQuerier) UpdateUserSkillByUserIDAndName(ctx context.Context, arg UpdateUserSkillByUserIDAndNameParams) (UserSkill, error) {
row := q.db.QueryRowContext(ctx, updateUserSkillByUserIDAndName,
arg.Description,
arg.Content,
arg.UserID,
arg.Name,
)
var i UserSkill
err := row.Scan(
&i.ID,
&i.UserID,
&i.Name,
&i.Description,
&i.Content,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const allUserIDs = `-- name: AllUserIDs :many
SELECT DISTINCT id FROM USERS
WHERE CASE WHEN $1::bool THEN TRUE ELSE is_system = false END
`
// AllUserIDs returns all UserIDs regardless of user status or deletion.
func (q *sqlQuerier) AllUserIDs(ctx context.Context, includeSystem bool) ([]uuid.UUID, error) {
rows, err := q.db.QueryContext(ctx, allUserIDs, includeSystem)
if err != nil {
return nil, err
}
defer rows.Close()
var items []uuid.UUID
for rows.Next() {
var id uuid.UUID
if err := rows.Scan(&id); err != nil {
return nil, err
}
items = append(items, id)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const deleteUserChatCompactionThreshold = `-- name: DeleteUserChatCompactionThreshold :exec
DELETE FROM user_configs WHERE user_id = $1 AND key = $2
`
type DeleteUserChatCompactionThresholdParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
Key string `db:"key" json:"key"`
}
func (q *sqlQuerier) DeleteUserChatCompactionThreshold(ctx context.Context, arg DeleteUserChatCompactionThresholdParams) error {
_, err := q.db.ExecContext(ctx, deleteUserChatCompactionThreshold, arg.UserID, arg.Key)
return err
}
const getActiveUserCount = `-- name: GetActiveUserCount :one
SELECT
COUNT(*)
FROM
users
WHERE
status = 'active'::user_status AND deleted = false
AND is_service_account = false
AND CASE WHEN $1::bool THEN TRUE ELSE is_system = false END
`
func (q *sqlQuerier) GetActiveUserCount(ctx context.Context, includeSystem bool) (int64, error) {
row := q.db.QueryRowContext(ctx, getActiveUserCount, includeSystem)
var count int64
err := row.Scan(&count)
return count, err
}
const getActiveUsersAuthorizationRoles = `-- name: GetActiveUsersAuthorizationRoles :many
WITH org_roles AS (
SELECT
organization_members.user_id,
-- The roles are returned as a flat array, org scoped and site side.
-- Concatenating the organization id scopes the organization roles.
array_agg(org_role || ':' || organization_members.organization_id::text) AS roles
FROM
organization_members
JOIN organizations ON organizations.id = organization_members.organization_id,
-- All org members get an implied organization-member role for
-- their orgs. Memberships of service accounts are aggregated here
-- too, but their rows never survive the join against the outer
-- WHERE, so the organization-service-account case does not apply.
--
-- organizations.default_org_member_roles applies to every member
-- but is not materialized on membership rows, so it is unioned in
-- here.
unnest(
array_cat(
array_append(organization_members.roles, 'organization-member'),
organizations.default_org_member_roles
)
) AS org_role
GROUP BY
organization_members.user_id
),
user_groups AS (
SELECT
group_members.user_id,
array_agg(group_members.group_id :: text) AS groups
FROM
group_members
GROUP BY
group_members.user_id
)
SELECT
users.id,
array_cat(
-- All users are members
array_append(users.rbac_roles, 'member'),
-- Users with no org memberships have no org_roles row.
coalesce(org_roles.roles, ARRAY[]::text[])
) :: text[] AS roles,
coalesce(user_groups.groups, ARRAY[]::text[]) :: text[] AS groups
FROM
users
LEFT JOIN org_roles ON org_roles.user_id = users.id
LEFT JOIN user_groups ON user_groups.user_id = users.id
WHERE
users.status = 'active'::user_status
AND users.deleted = false
AND users.is_system = false
AND users.is_service_account = false
`
type GetActiveUsersAuthorizationRolesRow struct {
ID uuid.UUID `db:"id" json:"id"`
Roles []string `db:"roles" json:"roles"`
Groups []string `db:"groups" json:"groups"`
}
// Returns the authorization roles (site and org-scoped, including implied
// member roles and organization default roles) and the group memberships
// for every active, non-deleted user who is neither a system user nor a
// service account, matching the GetActiveUserCount population.
// Must stay semantically in sync with GetAuthorizationUserRoles;
// TestGetActiveUsersAuthorizationRolesParity enforces this.
func (q *sqlQuerier) GetActiveUsersAuthorizationRoles(ctx context.Context) ([]GetActiveUsersAuthorizationRolesRow, error) {
rows, err := q.db.QueryContext(ctx, getActiveUsersAuthorizationRoles)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetActiveUsersAuthorizationRolesRow
for rows.Next() {
var i GetActiveUsersAuthorizationRolesRow
if err := rows.Scan(&i.ID, pq.Array(&i.Roles), pq.Array(&i.Groups)); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getAuthorizationUserRoles = `-- name: GetAuthorizationUserRoles :one
SELECT
-- username and email are returned just to help for logging purposes
-- status is used to enforce 'suspended' users, as all roles are ignored
-- when suspended.
id, username, status, email,
-- All user roles, including their org roles.
array_cat(
-- All users are members
array_append(users.rbac_roles, 'member'),
(
SELECT
-- The roles are returned as a flat array, org scoped and site side.
-- Concatenating the organization id scopes the organization roles.
array_agg(org_roles || ':' || organization_members.organization_id::text)
FROM
organization_members
JOIN organizations ON organizations.id = organization_members.organization_id,
-- All org members get an implied role for their orgs. Most members
-- get organization-member, but service accounts will get
-- organization-service-account instead. They're largely the same,
-- but having them be distinct means we can allow configuring
-- service-accounts to have slightly broader permissions, such as
-- for workspace sharing.
--
-- organizations.default_org_member_roles is unioned in so changes
-- to org defaults propagate to every member on the next request.
unnest(
array_cat(
array_append(
roles,
CASE WHEN users.is_service_account THEN
'organization-service-account'
ELSE
'organization-member'
END
),
organizations.default_org_member_roles
)
) AS org_roles
WHERE
user_id = users.id
)
) :: text[] AS roles,
-- All groups the user is in.
(
SELECT
array_agg(
group_members.group_id :: text
)
FROM
group_members
WHERE
user_id = users.id
) :: text[] AS groups
FROM
users
WHERE
users.id = $1
`
type GetAuthorizationUserRolesRow struct {
ID uuid.UUID `db:"id" json:"id"`
Username string `db:"username" json:"username"`
Status UserStatus `db:"status" json:"status"`
Email string `db:"email" json:"email"`
Roles []string `db:"roles" json:"roles"`
Groups []string `db:"groups" json:"groups"`
}
// This function returns roles for authorization purposes. Implied member roles
// are included.
// Must stay semantically in sync with GetActiveUsersAuthorizationRoles
// (implied member roles, org default roles, groups);
// TestGetActiveUsersAuthorizationRolesParity enforces this.
func (q *sqlQuerier) GetAuthorizationUserRoles(ctx context.Context, userID uuid.UUID) (GetAuthorizationUserRolesRow, error) {
row := q.db.QueryRowContext(ctx, getAuthorizationUserRoles, userID)
var i GetAuthorizationUserRolesRow
err := row.Scan(
&i.ID,
&i.Username,
&i.Status,
&i.Email,
pq.Array(&i.Roles),
pq.Array(&i.Groups),
)
return i, err
}
const getUserAgentChatSendShortcut = `-- name: GetUserAgentChatSendShortcut :one
SELECT
value AS agent_chat_send_shortcut
FROM
user_configs
WHERE
user_id = $1
AND key = 'preference_agent_chat_send_shortcut'
`
func (q *sqlQuerier) GetUserAgentChatSendShortcut(ctx context.Context, userID uuid.UUID) (string, error) {
row := q.db.QueryRowContext(ctx, getUserAgentChatSendShortcut, userID)
var agent_chat_send_shortcut string
err := row.Scan(&agent_chat_send_shortcut)
return agent_chat_send_shortcut, err
}
const getUserAppearanceSettings = `-- name: GetUserAppearanceSettings :one
SELECT
COALESCE(MAX(value) FILTER (WHERE key = 'theme_preference'), '')::text AS theme_preference,
COALESCE(MAX(value) FILTER (WHERE key = 'theme_mode'), '')::text AS theme_mode,
COALESCE(MAX(value) FILTER (WHERE key = 'theme_light'), '')::text AS theme_light,
COALESCE(MAX(value) FILTER (WHERE key = 'theme_dark'), '')::text AS theme_dark,
COALESCE(MAX(value) FILTER (WHERE key = 'terminal_font'), '')::text AS terminal_font
FROM
user_configs
WHERE
user_id = $1
AND key IN (
'theme_preference',
'theme_mode',
'theme_light',
'theme_dark',
'terminal_font'
)
`
type GetUserAppearanceSettingsRow struct {
ThemePreference string `db:"theme_preference" json:"theme_preference"`
ThemeMode string `db:"theme_mode" json:"theme_mode"`
ThemeLight string `db:"theme_light" json:"theme_light"`
ThemeDark string `db:"theme_dark" json:"theme_dark"`
TerminalFont string `db:"terminal_font" json:"terminal_font"`
}
func (q *sqlQuerier) GetUserAppearanceSettings(ctx context.Context, userID uuid.UUID) (GetUserAppearanceSettingsRow, error) {
row := q.db.QueryRowContext(ctx, getUserAppearanceSettings, userID)
var i GetUserAppearanceSettingsRow
err := row.Scan(
&i.ThemePreference,
&i.ThemeMode,
&i.ThemeLight,
&i.ThemeDark,
&i.TerminalFont,
)
return i, err
}
const getUserByEmailOrUsername = `-- name: GetUserByEmailOrUsername :one
SELECT
id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, is_service_account, chat_spend_limit_micros
FROM
users
WHERE
(LOWER(username) = LOWER($1) OR ($2 != '' AND LOWER(email) = LOWER($2))) AND
deleted = false
LIMIT
1
`
type GetUserByEmailOrUsernameParams struct {
Username string `db:"username" json:"username"`
Email interface{} `db:"email" json:"email"`
}
func (q *sqlQuerier) GetUserByEmailOrUsername(ctx context.Context, arg GetUserByEmailOrUsernameParams) (User, error) {
row := q.db.QueryRowContext(ctx, getUserByEmailOrUsername, arg.Username, arg.Email)
var i User
err := row.Scan(
&i.ID,
&i.Email,
&i.Username,
&i.HashedPassword,
&i.CreatedAt,
&i.UpdatedAt,
&i.Status,
&i.RBACRoles,
&i.LoginType,
&i.AvatarURL,
&i.Deleted,
&i.LastSeenAt,
&i.QuietHoursSchedule,
&i.Name,
&i.GithubComUserID,
&i.HashedOneTimePasscode,
&i.OneTimePasscodeExpiresAt,
&i.IsSystem,
&i.IsServiceAccount,
&i.ChatSpendLimitMicros,
)
return i, err
}
const getUserByID = `-- name: GetUserByID :one
SELECT
id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, is_service_account, chat_spend_limit_micros
FROM
users
WHERE
id = $1
LIMIT
1
`
func (q *sqlQuerier) GetUserByID(ctx context.Context, id uuid.UUID) (User, error) {
row := q.db.QueryRowContext(ctx, getUserByID, id)
var i User
err := row.Scan(
&i.ID,
&i.Email,
&i.Username,
&i.HashedPassword,
&i.CreatedAt,
&i.UpdatedAt,
&i.Status,
&i.RBACRoles,
&i.LoginType,
&i.AvatarURL,
&i.Deleted,
&i.LastSeenAt,
&i.QuietHoursSchedule,
&i.Name,
&i.GithubComUserID,
&i.HashedOneTimePasscode,
&i.OneTimePasscodeExpiresAt,
&i.IsSystem,
&i.IsServiceAccount,
&i.ChatSpendLimitMicros,
)
return i, err
}
const getUserChatCompactionThreshold = `-- name: GetUserChatCompactionThreshold :one
SELECT value AS threshold_percent FROM user_configs
WHERE user_id = $1 AND key = $2
`
type GetUserChatCompactionThresholdParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
Key string `db:"key" json:"key"`
}
func (q *sqlQuerier) GetUserChatCompactionThreshold(ctx context.Context, arg GetUserChatCompactionThresholdParams) (string, error) {
row := q.db.QueryRowContext(ctx, getUserChatCompactionThreshold, arg.UserID, arg.Key)
var threshold_percent string
err := row.Scan(&threshold_percent)
return threshold_percent, err
}
const getUserChatCustomPrompt = `-- name: GetUserChatCustomPrompt :one
SELECT
value as chat_custom_prompt
FROM
user_configs
WHERE
user_id = $1
AND key = 'chat_custom_prompt'
`
func (q *sqlQuerier) GetUserChatCustomPrompt(ctx context.Context, userID uuid.UUID) (string, error) {
row := q.db.QueryRowContext(ctx, getUserChatCustomPrompt, userID)
var chat_custom_prompt string
err := row.Scan(&chat_custom_prompt)
return chat_custom_prompt, err
}
const getUserChatDebugLoggingEnabled = `-- name: GetUserChatDebugLoggingEnabled :one
SELECT
COALESCE((
SELECT value = 'true'
FROM user_configs
WHERE user_id = $1
AND key = 'chat_debug_logging_enabled'
), false) :: boolean AS debug_logging_enabled
`
func (q *sqlQuerier) GetUserChatDebugLoggingEnabled(ctx context.Context, userID uuid.UUID) (bool, error) {
row := q.db.QueryRowContext(ctx, getUserChatDebugLoggingEnabled, userID)
var debug_logging_enabled bool
err := row.Scan(&debug_logging_enabled)
return debug_logging_enabled, err
}
const getUserChatPersonalModelOverride = `-- name: GetUserChatPersonalModelOverride :one
SELECT value AS personal_model_override FROM user_configs
WHERE user_id = $1
AND key = $2
`
type GetUserChatPersonalModelOverrideParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
Key string `db:"key" json:"key"`
}
func (q *sqlQuerier) GetUserChatPersonalModelOverride(ctx context.Context, arg GetUserChatPersonalModelOverrideParams) (string, error) {
row := q.db.QueryRowContext(ctx, getUserChatPersonalModelOverride, arg.UserID, arg.Key)
var personal_model_override string
err := row.Scan(&personal_model_override)
return personal_model_override, err
}
const getUserCodeDiffDisplayMode = `-- name: GetUserCodeDiffDisplayMode :one
SELECT
value AS code_diff_display_mode
FROM
user_configs
WHERE
user_id = $1
AND key = 'preference_code_diff_display_mode'
`
func (q *sqlQuerier) GetUserCodeDiffDisplayMode(ctx context.Context, userID uuid.UUID) (string, error) {
row := q.db.QueryRowContext(ctx, getUserCodeDiffDisplayMode, userID)
var code_diff_display_mode string
err := row.Scan(&code_diff_display_mode)
return code_diff_display_mode, err
}
const getUserCount = `-- name: GetUserCount :one
SELECT
COUNT(*)
FROM
users
WHERE
deleted = false
AND CASE WHEN $1::bool THEN TRUE ELSE is_system = false END
`
func (q *sqlQuerier) GetUserCount(ctx context.Context, includeSystem bool) (int64, error) {
row := q.db.QueryRowContext(ctx, getUserCount, includeSystem)
var count int64
err := row.Scan(&count)
return count, err
}
const getUserForChatSyntheticAPIKeyByID = `-- name: GetUserForChatSyntheticAPIKeyByID :one
SELECT id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, is_service_account, chat_spend_limit_micros
FROM users
WHERE id = $1::uuid
`
func (q *sqlQuerier) GetUserForChatSyntheticAPIKeyByID(ctx context.Context, id uuid.UUID) (User, error) {
row := q.db.QueryRowContext(ctx, getUserForChatSyntheticAPIKeyByID, id)
var i User
err := row.Scan(
&i.ID,
&i.Email,
&i.Username,
&i.HashedPassword,
&i.CreatedAt,
&i.UpdatedAt,
&i.Status,
&i.RBACRoles,
&i.LoginType,
&i.AvatarURL,
&i.Deleted,
&i.LastSeenAt,
&i.QuietHoursSchedule,
&i.Name,
&i.GithubComUserID,
&i.HashedOneTimePasscode,
&i.OneTimePasscodeExpiresAt,
&i.IsSystem,
&i.IsServiceAccount,
&i.ChatSpendLimitMicros,
)
return i, err
}
const getUserShellToolDisplayMode = `-- name: GetUserShellToolDisplayMode :one
SELECT
value AS shell_tool_display_mode
FROM
user_configs
WHERE
user_id = $1
AND key = 'preference_shell_tool_display_mode'
`
func (q *sqlQuerier) GetUserShellToolDisplayMode(ctx context.Context, userID uuid.UUID) (string, error) {
row := q.db.QueryRowContext(ctx, getUserShellToolDisplayMode, userID)
var shell_tool_display_mode string
err := row.Scan(&shell_tool_display_mode)
return shell_tool_display_mode, err
}
const getUserTaskNotificationAlertDismissed = `-- name: GetUserTaskNotificationAlertDismissed :one
SELECT
value::boolean as task_notification_alert_dismissed
FROM
user_configs
WHERE
user_id = $1
AND key = 'preference_task_notification_alert_dismissed'
`
func (q *sqlQuerier) GetUserTaskNotificationAlertDismissed(ctx context.Context, userID uuid.UUID) (bool, error) {
row := q.db.QueryRowContext(ctx, getUserTaskNotificationAlertDismissed, userID)
var task_notification_alert_dismissed bool
err := row.Scan(&task_notification_alert_dismissed)
return task_notification_alert_dismissed, err
}
const getUserThinkingDisplayMode = `-- name: GetUserThinkingDisplayMode :one
SELECT
value AS thinking_display_mode
FROM
user_configs
WHERE
user_id = $1
AND key = 'preference_thinking_display_mode'
`
func (q *sqlQuerier) GetUserThinkingDisplayMode(ctx context.Context, userID uuid.UUID) (string, error) {
row := q.db.QueryRowContext(ctx, getUserThinkingDisplayMode, userID)
var thinking_display_mode string
err := row.Scan(&thinking_display_mode)
return thinking_display_mode, err
}
const getUsers = `-- name: GetUsers :many
SELECT
id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, is_service_account, chat_spend_limit_micros, COUNT(*) OVER() AS count
FROM
users
WHERE
users.deleted = false
AND CASE
-- This allows using the last element on a page as effectively a cursor.
-- This is an important option for scripts that need to paginate without
-- duplicating or missing data.
WHEN $1 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN (
-- The pagination cursor is the last ID of the previous page.
-- The query is ordered by the username field, so select all
-- rows after the cursor.
(LOWER(username)) > (
SELECT
LOWER(username)
FROM
users
WHERE
id = $1
)
)
ELSE true
END
-- Start filters
-- Filter by email, username, or name (display name)
AND CASE
WHEN $2 :: text != '' THEN (
email ILIKE concat('%', $2, '%')
OR username ILIKE concat('%', $2, '%')
OR name ILIKE concat('%', $2, '%')
)
ELSE true
END
-- Filter by name (display name)
AND CASE
WHEN $3 :: text != '' THEN
name ILIKE concat('%', $3, '%')
ELSE true
END
-- Filter by exact username
AND CASE
WHEN $4 :: text != '' THEN
lower(username) = lower($4)
ELSE true
END
-- Filter by exact email
AND CASE
WHEN $5 :: text != '' THEN
lower(email) = lower($5)
ELSE true
END
-- Filter by status
AND CASE
-- @status needs to be a text because it can be empty, If it was
-- user_status enum, it would not.
WHEN cardinality($6 :: user_status[]) > 0 THEN
status = ANY($6 :: user_status[])
ELSE true
END
-- Filter by rbac_roles
AND CASE
-- @rbac_role allows filtering by rbac roles. If 'member' is included, show everyone, as
-- everyone is a member.
WHEN cardinality($7 :: text[]) > 0 AND 'member' != ANY($7 :: text[]) THEN
rbac_roles && $7 :: text[]
ELSE true
END
-- Filter by last_seen
AND CASE
WHEN $8 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
last_seen_at <= $8
ELSE true
END
AND CASE
WHEN $9 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
last_seen_at >= $9
ELSE true
END
-- Filter by created_at
AND CASE
WHEN $10 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
created_at <= $10
ELSE true
END
AND CASE
WHEN $11 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
created_at >= $11
ELSE true
END
-- Filter by system type
AND CASE
WHEN $12::bool THEN TRUE
ELSE is_system = false
END
-- Filter by github.com user ID
AND CASE
WHEN $13 :: bigint != 0 THEN
github_com_user_id = $13
ELSE true
END
-- Filter by login_type
AND CASE
WHEN cardinality($14 :: login_type[]) > 0 THEN
login_type = ANY($14 :: login_type[])
ELSE true
END
-- Filter by service account.
AND CASE
WHEN $15 :: boolean IS NOT NULL THEN
is_service_account = $15 :: boolean
ELSE true
END
-- End of filters
-- Authorize Filter clause will be injected below in GetAuthorizedUsers
-- @authorize_filter
ORDER BY
-- Deterministic and consistent ordering of all users. This is to ensure consistent pagination.
LOWER(username) ASC OFFSET $16
LIMIT
-- A null limit means "no limit", so 0 means return all
NULLIF($17 :: int, 0)
`
type GetUsersParams struct {
AfterID uuid.UUID `db:"after_id" json:"after_id"`
Search string `db:"search" json:"search"`
Name string `db:"name" json:"name"`
ExactUsername string `db:"exact_username" json:"exact_username"`
ExactEmail string `db:"exact_email" json:"exact_email"`
Status []UserStatus `db:"status" json:"status"`
RbacRole []string `db:"rbac_role" json:"rbac_role"`
LastSeenBefore time.Time `db:"last_seen_before" json:"last_seen_before"`
LastSeenAfter time.Time `db:"last_seen_after" json:"last_seen_after"`
CreatedBefore time.Time `db:"created_before" json:"created_before"`
CreatedAfter time.Time `db:"created_after" json:"created_after"`
IncludeSystem bool `db:"include_system" json:"include_system"`
GithubComUserID int64 `db:"github_com_user_id" json:"github_com_user_id"`
LoginType []LoginType `db:"login_type" json:"login_type"`
IsServiceAccount sql.NullBool `db:"is_service_account" json:"is_service_account"`
OffsetOpt int32 `db:"offset_opt" json:"offset_opt"`
LimitOpt int32 `db:"limit_opt" json:"limit_opt"`
}
type GetUsersRow struct {
ID uuid.UUID `db:"id" json:"id"`
Email string `db:"email" json:"email"`
Username string `db:"username" json:"username"`
HashedPassword []byte `db:"hashed_password" json:"hashed_password"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Status UserStatus `db:"status" json:"status"`
RBACRoles pq.StringArray `db:"rbac_roles" json:"rbac_roles"`
LoginType LoginType `db:"login_type" json:"login_type"`
AvatarURL string `db:"avatar_url" json:"avatar_url"`
Deleted bool `db:"deleted" json:"deleted"`
LastSeenAt time.Time `db:"last_seen_at" json:"last_seen_at"`
QuietHoursSchedule string `db:"quiet_hours_schedule" json:"quiet_hours_schedule"`
Name string `db:"name" json:"name"`
GithubComUserID sql.NullInt64 `db:"github_com_user_id" json:"github_com_user_id"`
HashedOneTimePasscode []byte `db:"hashed_one_time_passcode" json:"hashed_one_time_passcode"`
OneTimePasscodeExpiresAt sql.NullTime `db:"one_time_passcode_expires_at" json:"one_time_passcode_expires_at"`
IsSystem bool `db:"is_system" json:"is_system"`
IsServiceAccount bool `db:"is_service_account" json:"is_service_account"`
ChatSpendLimitMicros sql.NullInt64 `db:"chat_spend_limit_micros" json:"chat_spend_limit_micros"`
Count int64 `db:"count" json:"count"`
}
// This will never return deleted users.
func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUsersRow, error) {
rows, err := q.db.QueryContext(ctx, getUsers,
arg.AfterID,
arg.Search,
arg.Name,
arg.ExactUsername,
arg.ExactEmail,
pq.Array(arg.Status),
pq.Array(arg.RbacRole),
arg.LastSeenBefore,
arg.LastSeenAfter,
arg.CreatedBefore,
arg.CreatedAfter,
arg.IncludeSystem,
arg.GithubComUserID,
pq.Array(arg.LoginType),
arg.IsServiceAccount,
arg.OffsetOpt,
arg.LimitOpt,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetUsersRow
for rows.Next() {
var i GetUsersRow
if err := rows.Scan(
&i.ID,
&i.Email,
&i.Username,
&i.HashedPassword,
&i.CreatedAt,
&i.UpdatedAt,
&i.Status,
&i.RBACRoles,
&i.LoginType,
&i.AvatarURL,
&i.Deleted,
&i.LastSeenAt,
&i.QuietHoursSchedule,
&i.Name,
&i.GithubComUserID,
&i.HashedOneTimePasscode,
&i.OneTimePasscodeExpiresAt,
&i.IsSystem,
&i.IsServiceAccount,
&i.ChatSpendLimitMicros,
&i.Count,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getUsersByIDs = `-- name: GetUsersByIDs :many
SELECT id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, is_service_account, chat_spend_limit_micros FROM users WHERE id = ANY($1 :: uuid [ ])
`
// This shouldn't check for deleted, because it's frequently used
// to look up references to actions. eg. a user could build a workspace
// for another user, then be deleted... we still want them to appear!
func (q *sqlQuerier) GetUsersByIDs(ctx context.Context, ids []uuid.UUID) ([]User, error) {
rows, err := q.db.QueryContext(ctx, getUsersByIDs, pq.Array(ids))
if err != nil {
return nil, err
}
defer rows.Close()
var items []User
for rows.Next() {
var i User
if err := rows.Scan(
&i.ID,
&i.Email,
&i.Username,
&i.HashedPassword,
&i.CreatedAt,
&i.UpdatedAt,
&i.Status,
&i.RBACRoles,
&i.LoginType,
&i.AvatarURL,
&i.Deleted,
&i.LastSeenAt,
&i.QuietHoursSchedule,
&i.Name,
&i.GithubComUserID,
&i.HashedOneTimePasscode,
&i.OneTimePasscodeExpiresAt,
&i.IsSystem,
&i.IsServiceAccount,
&i.ChatSpendLimitMicros,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertUser = `-- name: InsertUser :one
INSERT INTO
users (
id,
email,
username,
name,
hashed_password,
created_at,
updated_at,
rbac_roles,
login_type,
status,
is_service_account
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9,
-- if the status passed in is empty, fallback to dormant, which is what
-- we were doing before.
COALESCE(NULLIF($10::text, '')::user_status, 'dormant'::user_status),
$11::bool
) RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, is_service_account, chat_spend_limit_micros
`
type InsertUserParams struct {
ID uuid.UUID `db:"id" json:"id"`
Email string `db:"email" json:"email"`
Username string `db:"username" json:"username"`
Name string `db:"name" json:"name"`
HashedPassword []byte `db:"hashed_password" json:"hashed_password"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
RBACRoles pq.StringArray `db:"rbac_roles" json:"rbac_roles"`
LoginType LoginType `db:"login_type" json:"login_type"`
Status string `db:"status" json:"status"`
IsServiceAccount bool `db:"is_service_account" json:"is_service_account"`
}
func (q *sqlQuerier) InsertUser(ctx context.Context, arg InsertUserParams) (User, error) {
row := q.db.QueryRowContext(ctx, insertUser,
arg.ID,
arg.Email,
arg.Username,
arg.Name,
arg.HashedPassword,
arg.CreatedAt,
arg.UpdatedAt,
arg.RBACRoles,
arg.LoginType,
arg.Status,
arg.IsServiceAccount,
)
var i User
err := row.Scan(
&i.ID,
&i.Email,
&i.Username,
&i.HashedPassword,
&i.CreatedAt,
&i.UpdatedAt,
&i.Status,
&i.RBACRoles,
&i.LoginType,
&i.AvatarURL,
&i.Deleted,
&i.LastSeenAt,
&i.QuietHoursSchedule,
&i.Name,
&i.GithubComUserID,
&i.HashedOneTimePasscode,
&i.OneTimePasscodeExpiresAt,
&i.IsSystem,
&i.IsServiceAccount,
&i.ChatSpendLimitMicros,
)
return i, err
}
const listUserChatCompactionThresholds = `-- name: ListUserChatCompactionThresholds :many
SELECT user_id, key, value FROM user_configs
WHERE user_id = $1
AND key LIKE 'chat\_compaction\_threshold\_pct:%'
ORDER BY key
`
func (q *sqlQuerier) ListUserChatCompactionThresholds(ctx context.Context, userID uuid.UUID) ([]UserConfig, error) {
rows, err := q.db.QueryContext(ctx, listUserChatCompactionThresholds, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []UserConfig
for rows.Next() {
var i UserConfig
if err := rows.Scan(&i.UserID, &i.Key, &i.Value); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listUserChatPersonalModelOverrides = `-- name: ListUserChatPersonalModelOverrides :many
SELECT key, value FROM user_configs
WHERE user_id = $1
AND key LIKE 'chat\_personal\_model\_override:%'
ORDER BY key
`
type ListUserChatPersonalModelOverridesRow struct {
Key string `db:"key" json:"key"`
Value string `db:"value" json:"value"`
}
func (q *sqlQuerier) ListUserChatPersonalModelOverrides(ctx context.Context, userID uuid.UUID) ([]ListUserChatPersonalModelOverridesRow, error) {
rows, err := q.db.QueryContext(ctx, listUserChatPersonalModelOverrides, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListUserChatPersonalModelOverridesRow
for rows.Next() {
var i ListUserChatPersonalModelOverridesRow
if err := rows.Scan(&i.Key, &i.Value); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateInactiveUsersToDormant = `-- name: UpdateInactiveUsersToDormant :many
UPDATE
users
SET
status = 'dormant'::user_status,
updated_at = $1
WHERE
last_seen_at < $2 :: timestamp
AND status = 'active'::user_status
AND NOT is_system
RETURNING id, email, username, last_seen_at
`
type UpdateInactiveUsersToDormantParams struct {
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
LastSeenAfter time.Time `db:"last_seen_after" json:"last_seen_after"`
}
type UpdateInactiveUsersToDormantRow struct {
ID uuid.UUID `db:"id" json:"id"`
Email string `db:"email" json:"email"`
Username string `db:"username" json:"username"`
LastSeenAt time.Time `db:"last_seen_at" json:"last_seen_at"`
}
func (q *sqlQuerier) UpdateInactiveUsersToDormant(ctx context.Context, arg UpdateInactiveUsersToDormantParams) ([]UpdateInactiveUsersToDormantRow, error) {
rows, err := q.db.QueryContext(ctx, updateInactiveUsersToDormant, arg.UpdatedAt, arg.LastSeenAfter)
if err != nil {
return nil, err
}
defer rows.Close()
var items []UpdateInactiveUsersToDormantRow
for rows.Next() {
var i UpdateInactiveUsersToDormantRow
if err := rows.Scan(
&i.ID,
&i.Email,
&i.Username,
&i.LastSeenAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateUserAgentChatSendShortcut = `-- name: UpdateUserAgentChatSendShortcut :one
INSERT INTO
user_configs (user_id, key, value)
VALUES
($1, 'preference_agent_chat_send_shortcut', $2::text)
ON CONFLICT
ON CONSTRAINT user_configs_pkey
DO UPDATE
SET
value = $2
WHERE user_configs.user_id = $1
AND user_configs.key = 'preference_agent_chat_send_shortcut'
RETURNING value AS agent_chat_send_shortcut
`
type UpdateUserAgentChatSendShortcutParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
AgentChatSendShortcut string `db:"agent_chat_send_shortcut" json:"agent_chat_send_shortcut"`
}
func (q *sqlQuerier) UpdateUserAgentChatSendShortcut(ctx context.Context, arg UpdateUserAgentChatSendShortcutParams) (string, error) {
row := q.db.QueryRowContext(ctx, updateUserAgentChatSendShortcut, arg.UserID, arg.AgentChatSendShortcut)
var agent_chat_send_shortcut string
err := row.Scan(&agent_chat_send_shortcut)
return agent_chat_send_shortcut, err
}
const updateUserChatCompactionThreshold = `-- name: UpdateUserChatCompactionThreshold :one
INSERT INTO user_configs (user_id, key, value)
VALUES ($1, $2, ($3::int)::text)
ON CONFLICT ON CONSTRAINT user_configs_pkey
DO UPDATE SET value = ($3::int)::text
RETURNING user_id, key, value
`
type UpdateUserChatCompactionThresholdParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
Key string `db:"key" json:"key"`
ThresholdPercent int32 `db:"threshold_percent" json:"threshold_percent"`
}
func (q *sqlQuerier) UpdateUserChatCompactionThreshold(ctx context.Context, arg UpdateUserChatCompactionThresholdParams) (UserConfig, error) {
row := q.db.QueryRowContext(ctx, updateUserChatCompactionThreshold, arg.UserID, arg.Key, arg.ThresholdPercent)
var i UserConfig
err := row.Scan(&i.UserID, &i.Key, &i.Value)
return i, err
}
const updateUserChatCustomPrompt = `-- name: UpdateUserChatCustomPrompt :one
INSERT INTO
user_configs (user_id, key, value)
VALUES
($1, 'chat_custom_prompt', $2)
ON CONFLICT
ON CONSTRAINT user_configs_pkey
DO UPDATE
SET
value = $2
WHERE user_configs.user_id = $1
AND user_configs.key = 'chat_custom_prompt'
RETURNING user_id, key, value
`
type UpdateUserChatCustomPromptParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
ChatCustomPrompt string `db:"chat_custom_prompt" json:"chat_custom_prompt"`
}
func (q *sqlQuerier) UpdateUserChatCustomPrompt(ctx context.Context, arg UpdateUserChatCustomPromptParams) (UserConfig, error) {
row := q.db.QueryRowContext(ctx, updateUserChatCustomPrompt, arg.UserID, arg.ChatCustomPrompt)
var i UserConfig
err := row.Scan(&i.UserID, &i.Key, &i.Value)
return i, err
}
const updateUserCodeDiffDisplayMode = `-- name: UpdateUserCodeDiffDisplayMode :one
INSERT INTO
user_configs (user_id, key, value)
VALUES
($1, 'preference_code_diff_display_mode', $2::text)
ON CONFLICT
ON CONSTRAINT user_configs_pkey
DO UPDATE
SET
value = $2
WHERE user_configs.user_id = $1
AND user_configs.key = 'preference_code_diff_display_mode'
RETURNING value AS code_diff_display_mode
`
type UpdateUserCodeDiffDisplayModeParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
CodeDiffDisplayMode string `db:"code_diff_display_mode" json:"code_diff_display_mode"`
}
func (q *sqlQuerier) UpdateUserCodeDiffDisplayMode(ctx context.Context, arg UpdateUserCodeDiffDisplayModeParams) (string, error) {
row := q.db.QueryRowContext(ctx, updateUserCodeDiffDisplayMode, arg.UserID, arg.CodeDiffDisplayMode)
var code_diff_display_mode string
err := row.Scan(&code_diff_display_mode)
return code_diff_display_mode, err
}
const updateUserDeletedByID = `-- name: UpdateUserDeletedByID :exec
UPDATE
users
SET
deleted = true
WHERE
id = $1
`
func (q *sqlQuerier) UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, updateUserDeletedByID, id)
return err
}
const updateUserGithubComUserID = `-- name: UpdateUserGithubComUserID :exec
UPDATE
users
SET
github_com_user_id = $2
WHERE
id = $1
`
type UpdateUserGithubComUserIDParams struct {
ID uuid.UUID `db:"id" json:"id"`
GithubComUserID sql.NullInt64 `db:"github_com_user_id" json:"github_com_user_id"`
}
func (q *sqlQuerier) UpdateUserGithubComUserID(ctx context.Context, arg UpdateUserGithubComUserIDParams) error {
_, err := q.db.ExecContext(ctx, updateUserGithubComUserID, arg.ID, arg.GithubComUserID)
return err
}
const updateUserHashedOneTimePasscode = `-- name: UpdateUserHashedOneTimePasscode :exec
UPDATE
users
SET
hashed_one_time_passcode = $2,
one_time_passcode_expires_at = $3
WHERE
id = $1
`
type UpdateUserHashedOneTimePasscodeParams struct {
ID uuid.UUID `db:"id" json:"id"`
HashedOneTimePasscode []byte `db:"hashed_one_time_passcode" json:"hashed_one_time_passcode"`
OneTimePasscodeExpiresAt sql.NullTime `db:"one_time_passcode_expires_at" json:"one_time_passcode_expires_at"`
}
func (q *sqlQuerier) UpdateUserHashedOneTimePasscode(ctx context.Context, arg UpdateUserHashedOneTimePasscodeParams) error {
_, err := q.db.ExecContext(ctx, updateUserHashedOneTimePasscode, arg.ID, arg.HashedOneTimePasscode, arg.OneTimePasscodeExpiresAt)
return err
}
const updateUserHashedPassword = `-- name: UpdateUserHashedPassword :exec
UPDATE
users
SET
hashed_password = $2,
hashed_one_time_passcode = NULL,
one_time_passcode_expires_at = NULL
WHERE
id = $1
`
type UpdateUserHashedPasswordParams struct {
ID uuid.UUID `db:"id" json:"id"`
HashedPassword []byte `db:"hashed_password" json:"hashed_password"`
}
func (q *sqlQuerier) UpdateUserHashedPassword(ctx context.Context, arg UpdateUserHashedPasswordParams) error {
_, err := q.db.ExecContext(ctx, updateUserHashedPassword, arg.ID, arg.HashedPassword)
return err
}
const updateUserLastSeenAt = `-- name: UpdateUserLastSeenAt :one
UPDATE
users
SET
last_seen_at = $2,
updated_at = $3
WHERE
id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, is_service_account, chat_spend_limit_micros
`
type UpdateUserLastSeenAtParams struct {
ID uuid.UUID `db:"id" json:"id"`
LastSeenAt time.Time `db:"last_seen_at" json:"last_seen_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
func (q *sqlQuerier) UpdateUserLastSeenAt(ctx context.Context, arg UpdateUserLastSeenAtParams) (User, error) {
row := q.db.QueryRowContext(ctx, updateUserLastSeenAt, arg.ID, arg.LastSeenAt, arg.UpdatedAt)
var i User
err := row.Scan(
&i.ID,
&i.Email,
&i.Username,
&i.HashedPassword,
&i.CreatedAt,
&i.UpdatedAt,
&i.Status,
&i.RBACRoles,
&i.LoginType,
&i.AvatarURL,
&i.Deleted,
&i.LastSeenAt,
&i.QuietHoursSchedule,
&i.Name,
&i.GithubComUserID,
&i.HashedOneTimePasscode,
&i.OneTimePasscodeExpiresAt,
&i.IsSystem,
&i.IsServiceAccount,
&i.ChatSpendLimitMicros,
)
return i, err
}
const updateUserLoginType = `-- name: UpdateUserLoginType :one
UPDATE
users
SET
login_type = $1,
hashed_password = CASE WHEN $1 = 'password' :: login_type THEN
users.hashed_password
ELSE
-- If the login type is not password, then the password should be
-- cleared.
'':: bytea
END
WHERE
id = $2
AND NOT is_system
RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, is_service_account, chat_spend_limit_micros
`
type UpdateUserLoginTypeParams struct {
NewLoginType LoginType `db:"new_login_type" json:"new_login_type"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
}
func (q *sqlQuerier) UpdateUserLoginType(ctx context.Context, arg UpdateUserLoginTypeParams) (User, error) {
row := q.db.QueryRowContext(ctx, updateUserLoginType, arg.NewLoginType, arg.UserID)
var i User
err := row.Scan(
&i.ID,
&i.Email,
&i.Username,
&i.HashedPassword,
&i.CreatedAt,
&i.UpdatedAt,
&i.Status,
&i.RBACRoles,
&i.LoginType,
&i.AvatarURL,
&i.Deleted,
&i.LastSeenAt,
&i.QuietHoursSchedule,
&i.Name,
&i.GithubComUserID,
&i.HashedOneTimePasscode,
&i.OneTimePasscodeExpiresAt,
&i.IsSystem,
&i.IsServiceAccount,
&i.ChatSpendLimitMicros,
)
return i, err
}
const updateUserProfile = `-- name: UpdateUserProfile :one
UPDATE
users
SET
email = $2,
username = $3,
avatar_url = $4,
updated_at = $5,
name = $6
WHERE
id = $1
RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, is_service_account, chat_spend_limit_micros
`
type UpdateUserProfileParams struct {
ID uuid.UUID `db:"id" json:"id"`
Email string `db:"email" json:"email"`
Username string `db:"username" json:"username"`
AvatarURL string `db:"avatar_url" json:"avatar_url"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Name string `db:"name" json:"name"`
}
func (q *sqlQuerier) UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) (User, error) {
row := q.db.QueryRowContext(ctx, updateUserProfile,
arg.ID,
arg.Email,
arg.Username,
arg.AvatarURL,
arg.UpdatedAt,
arg.Name,
)
var i User
err := row.Scan(
&i.ID,
&i.Email,
&i.Username,
&i.HashedPassword,
&i.CreatedAt,
&i.UpdatedAt,
&i.Status,
&i.RBACRoles,
&i.LoginType,
&i.AvatarURL,
&i.Deleted,
&i.LastSeenAt,
&i.QuietHoursSchedule,
&i.Name,
&i.GithubComUserID,
&i.HashedOneTimePasscode,
&i.OneTimePasscodeExpiresAt,
&i.IsSystem,
&i.IsServiceAccount,
&i.ChatSpendLimitMicros,
)
return i, err
}
const updateUserQuietHoursSchedule = `-- name: UpdateUserQuietHoursSchedule :one
UPDATE
users
SET
quiet_hours_schedule = $2
WHERE
id = $1
RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, is_service_account, chat_spend_limit_micros
`
type UpdateUserQuietHoursScheduleParams struct {
ID uuid.UUID `db:"id" json:"id"`
QuietHoursSchedule string `db:"quiet_hours_schedule" json:"quiet_hours_schedule"`
}
func (q *sqlQuerier) UpdateUserQuietHoursSchedule(ctx context.Context, arg UpdateUserQuietHoursScheduleParams) (User, error) {
row := q.db.QueryRowContext(ctx, updateUserQuietHoursSchedule, arg.ID, arg.QuietHoursSchedule)
var i User
err := row.Scan(
&i.ID,
&i.Email,
&i.Username,
&i.HashedPassword,
&i.CreatedAt,
&i.UpdatedAt,
&i.Status,
&i.RBACRoles,
&i.LoginType,
&i.AvatarURL,
&i.Deleted,
&i.LastSeenAt,
&i.QuietHoursSchedule,
&i.Name,
&i.GithubComUserID,
&i.HashedOneTimePasscode,
&i.OneTimePasscodeExpiresAt,
&i.IsSystem,
&i.IsServiceAccount,
&i.ChatSpendLimitMicros,
)
return i, err
}
const updateUserRoles = `-- name: UpdateUserRoles :one
UPDATE
users
SET
-- Remove all duplicates from the roles.
rbac_roles = ARRAY(SELECT DISTINCT UNNEST($1 :: text[]))
WHERE
id = $2
RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, is_service_account, chat_spend_limit_micros
`
type UpdateUserRolesParams struct {
GrantedRoles []string `db:"granted_roles" json:"granted_roles"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateUserRoles(ctx context.Context, arg UpdateUserRolesParams) (User, error) {
row := q.db.QueryRowContext(ctx, updateUserRoles, pq.Array(arg.GrantedRoles), arg.ID)
var i User
err := row.Scan(
&i.ID,
&i.Email,
&i.Username,
&i.HashedPassword,
&i.CreatedAt,
&i.UpdatedAt,
&i.Status,
&i.RBACRoles,
&i.LoginType,
&i.AvatarURL,
&i.Deleted,
&i.LastSeenAt,
&i.QuietHoursSchedule,
&i.Name,
&i.GithubComUserID,
&i.HashedOneTimePasscode,
&i.OneTimePasscodeExpiresAt,
&i.IsSystem,
&i.IsServiceAccount,
&i.ChatSpendLimitMicros,
)
return i, err
}
const updateUserShellToolDisplayMode = `-- name: UpdateUserShellToolDisplayMode :one
INSERT INTO
user_configs (user_id, key, value)
VALUES
($1, 'preference_shell_tool_display_mode', $2::text)
ON CONFLICT
ON CONSTRAINT user_configs_pkey
DO UPDATE
SET
value = $2
WHERE user_configs.user_id = $1
AND user_configs.key = 'preference_shell_tool_display_mode'
RETURNING value AS shell_tool_display_mode
`
type UpdateUserShellToolDisplayModeParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
ShellToolDisplayMode string `db:"shell_tool_display_mode" json:"shell_tool_display_mode"`
}
func (q *sqlQuerier) UpdateUserShellToolDisplayMode(ctx context.Context, arg UpdateUserShellToolDisplayModeParams) (string, error) {
row := q.db.QueryRowContext(ctx, updateUserShellToolDisplayMode, arg.UserID, arg.ShellToolDisplayMode)
var shell_tool_display_mode string
err := row.Scan(&shell_tool_display_mode)
return shell_tool_display_mode, err
}
const updateUserStatus = `-- name: UpdateUserStatus :one
UPDATE
users
SET
status = $2,
updated_at = $3,
-- If the user is logging in, set last_seen_at to updated_at.
last_seen_at = CASE WHEN $4 :: boolean THEN $3 :: timestamptz ELSE last_seen_at END
WHERE
id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, is_service_account, chat_spend_limit_micros
`
type UpdateUserStatusParams struct {
ID uuid.UUID `db:"id" json:"id"`
Status UserStatus `db:"status" json:"status"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
UserIsSeen bool `db:"user_is_seen" json:"user_is_seen"`
}
func (q *sqlQuerier) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusParams) (User, error) {
row := q.db.QueryRowContext(ctx, updateUserStatus,
arg.ID,
arg.Status,
arg.UpdatedAt,
arg.UserIsSeen,
)
var i User
err := row.Scan(
&i.ID,
&i.Email,
&i.Username,
&i.HashedPassword,
&i.CreatedAt,
&i.UpdatedAt,
&i.Status,
&i.RBACRoles,
&i.LoginType,
&i.AvatarURL,
&i.Deleted,
&i.LastSeenAt,
&i.QuietHoursSchedule,
&i.Name,
&i.GithubComUserID,
&i.HashedOneTimePasscode,
&i.OneTimePasscodeExpiresAt,
&i.IsSystem,
&i.IsServiceAccount,
&i.ChatSpendLimitMicros,
)
return i, err
}
const updateUserTaskNotificationAlertDismissed = `-- name: UpdateUserTaskNotificationAlertDismissed :one
INSERT INTO
user_configs (user_id, key, value)
VALUES
($1, 'preference_task_notification_alert_dismissed', ($2::boolean)::text)
ON CONFLICT
ON CONSTRAINT user_configs_pkey
DO UPDATE
SET
value = $2
WHERE user_configs.user_id = $1
AND user_configs.key = 'preference_task_notification_alert_dismissed'
RETURNING value::boolean AS task_notification_alert_dismissed
`
type UpdateUserTaskNotificationAlertDismissedParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
TaskNotificationAlertDismissed bool `db:"task_notification_alert_dismissed" json:"task_notification_alert_dismissed"`
}
func (q *sqlQuerier) UpdateUserTaskNotificationAlertDismissed(ctx context.Context, arg UpdateUserTaskNotificationAlertDismissedParams) (bool, error) {
row := q.db.QueryRowContext(ctx, updateUserTaskNotificationAlertDismissed, arg.UserID, arg.TaskNotificationAlertDismissed)
var task_notification_alert_dismissed bool
err := row.Scan(&task_notification_alert_dismissed)
return task_notification_alert_dismissed, err
}
const updateUserTerminalFont = `-- name: UpdateUserTerminalFont :one
INSERT INTO
user_configs (user_id, key, value)
VALUES
($1, 'terminal_font', $2)
ON CONFLICT
ON CONSTRAINT user_configs_pkey
DO UPDATE
SET
value = $2
WHERE user_configs.user_id = $1
AND user_configs.key = 'terminal_font'
RETURNING user_id, key, value
`
type UpdateUserTerminalFontParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
TerminalFont string `db:"terminal_font" json:"terminal_font"`
}
func (q *sqlQuerier) UpdateUserTerminalFont(ctx context.Context, arg UpdateUserTerminalFontParams) (UserConfig, error) {
row := q.db.QueryRowContext(ctx, updateUserTerminalFont, arg.UserID, arg.TerminalFont)
var i UserConfig
err := row.Scan(&i.UserID, &i.Key, &i.Value)
return i, err
}
const updateUserThemeDark = `-- name: UpdateUserThemeDark :one
INSERT INTO
user_configs (user_id, key, value)
VALUES
($1, 'theme_dark', $2)
ON CONFLICT
ON CONSTRAINT user_configs_pkey
DO UPDATE
SET
value = $2
WHERE user_configs.user_id = $1
AND user_configs.key = 'theme_dark'
RETURNING user_id, key, value
`
type UpdateUserThemeDarkParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
ThemeDark string `db:"theme_dark" json:"theme_dark"`
}
func (q *sqlQuerier) UpdateUserThemeDark(ctx context.Context, arg UpdateUserThemeDarkParams) (UserConfig, error) {
row := q.db.QueryRowContext(ctx, updateUserThemeDark, arg.UserID, arg.ThemeDark)
var i UserConfig
err := row.Scan(&i.UserID, &i.Key, &i.Value)
return i, err
}
const updateUserThemeLight = `-- name: UpdateUserThemeLight :one
INSERT INTO
user_configs (user_id, key, value)
VALUES
($1, 'theme_light', $2)
ON CONFLICT
ON CONSTRAINT user_configs_pkey
DO UPDATE
SET
value = $2
WHERE user_configs.user_id = $1
AND user_configs.key = 'theme_light'
RETURNING user_id, key, value
`
type UpdateUserThemeLightParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
ThemeLight string `db:"theme_light" json:"theme_light"`
}
func (q *sqlQuerier) UpdateUserThemeLight(ctx context.Context, arg UpdateUserThemeLightParams) (UserConfig, error) {
row := q.db.QueryRowContext(ctx, updateUserThemeLight, arg.UserID, arg.ThemeLight)
var i UserConfig
err := row.Scan(&i.UserID, &i.Key, &i.Value)
return i, err
}
const updateUserThemeMode = `-- name: UpdateUserThemeMode :one
INSERT INTO
user_configs (user_id, key, value)
VALUES
($1, 'theme_mode', $2)
ON CONFLICT
ON CONSTRAINT user_configs_pkey
DO UPDATE
SET
value = $2
WHERE user_configs.user_id = $1
AND user_configs.key = 'theme_mode'
RETURNING user_id, key, value
`
type UpdateUserThemeModeParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
ThemeMode string `db:"theme_mode" json:"theme_mode"`
}
func (q *sqlQuerier) UpdateUserThemeMode(ctx context.Context, arg UpdateUserThemeModeParams) (UserConfig, error) {
row := q.db.QueryRowContext(ctx, updateUserThemeMode, arg.UserID, arg.ThemeMode)
var i UserConfig
err := row.Scan(&i.UserID, &i.Key, &i.Value)
return i, err
}
const updateUserThemePreference = `-- name: UpdateUserThemePreference :one
INSERT INTO
user_configs (user_id, key, value)
VALUES
($1, 'theme_preference', $2)
ON CONFLICT
ON CONSTRAINT user_configs_pkey
DO UPDATE
SET
value = $2
WHERE user_configs.user_id = $1
AND user_configs.key = 'theme_preference'
RETURNING user_id, key, value
`
type UpdateUserThemePreferenceParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
ThemePreference string `db:"theme_preference" json:"theme_preference"`
}
func (q *sqlQuerier) UpdateUserThemePreference(ctx context.Context, arg UpdateUserThemePreferenceParams) (UserConfig, error) {
row := q.db.QueryRowContext(ctx, updateUserThemePreference, arg.UserID, arg.ThemePreference)
var i UserConfig
err := row.Scan(&i.UserID, &i.Key, &i.Value)
return i, err
}
const updateUserThinkingDisplayMode = `-- name: UpdateUserThinkingDisplayMode :one
INSERT INTO
user_configs (user_id, key, value)
VALUES
($1, 'preference_thinking_display_mode', $2::text)
ON CONFLICT
ON CONSTRAINT user_configs_pkey
DO UPDATE
SET
value = $2
WHERE user_configs.user_id = $1
AND user_configs.key = 'preference_thinking_display_mode'
RETURNING value AS thinking_display_mode
`
type UpdateUserThinkingDisplayModeParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
ThinkingDisplayMode string `db:"thinking_display_mode" json:"thinking_display_mode"`
}
func (q *sqlQuerier) UpdateUserThinkingDisplayMode(ctx context.Context, arg UpdateUserThinkingDisplayModeParams) (string, error) {
row := q.db.QueryRowContext(ctx, updateUserThinkingDisplayMode, arg.UserID, arg.ThinkingDisplayMode)
var thinking_display_mode string
err := row.Scan(&thinking_display_mode)
return thinking_display_mode, err
}
const upsertUserChatDebugLoggingEnabled = `-- name: UpsertUserChatDebugLoggingEnabled :exec
INSERT INTO user_configs (user_id, key, value)
VALUES (
$1,
'chat_debug_logging_enabled',
CASE
WHEN $2::bool THEN 'true'
ELSE 'false'
END
)
ON CONFLICT ON CONSTRAINT user_configs_pkey
DO UPDATE SET value = CASE
WHEN $2::bool THEN 'true'
ELSE 'false'
END
WHERE user_configs.user_id = $1
AND user_configs.key = 'chat_debug_logging_enabled'
`
type UpsertUserChatDebugLoggingEnabledParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
DebugLoggingEnabled bool `db:"debug_logging_enabled" json:"debug_logging_enabled"`
}
func (q *sqlQuerier) UpsertUserChatDebugLoggingEnabled(ctx context.Context, arg UpsertUserChatDebugLoggingEnabledParams) error {
_, err := q.db.ExecContext(ctx, upsertUserChatDebugLoggingEnabled, arg.UserID, arg.DebugLoggingEnabled)
return err
}
const upsertUserChatPersonalModelOverride = `-- name: UpsertUserChatPersonalModelOverride :exec
INSERT INTO user_configs (user_id, key, value)
VALUES ($1::uuid, $2::text, $3::text)
ON CONFLICT ON CONSTRAINT user_configs_pkey
DO UPDATE SET value = $3::text
`
type UpsertUserChatPersonalModelOverrideParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
Key string `db:"key" json:"key"`
Value string `db:"value" json:"value"`
}
func (q *sqlQuerier) UpsertUserChatPersonalModelOverride(ctx context.Context, arg UpsertUserChatPersonalModelOverrideParams) error {
_, err := q.db.ExecContext(ctx, upsertUserChatPersonalModelOverride, arg.UserID, arg.Key, arg.Value)
return err
}
const validateUserIDs = `-- name: ValidateUserIDs :one
WITH input AS (
SELECT
unnest($1::uuid[]) AS id
)
SELECT
array_agg(input.id)::uuid[] as invalid_user_ids,
COUNT(*) = 0 as ok
FROM
-- Preserve rows where there is not a matching left (users) row for each
-- right (input) row...
users
RIGHT JOIN input ON users.id = input.id
WHERE
-- ...so that we can retain exactly those rows where an input ID does not
-- match an existing user...
users.id IS NULL OR
-- ...or that only matches a user that was deleted.
users.deleted = true
`
type ValidateUserIDsRow struct {
InvalidUserIds []uuid.UUID `db:"invalid_user_ids" json:"invalid_user_ids"`
Ok bool `db:"ok" json:"ok"`
}
func (q *sqlQuerier) ValidateUserIDs(ctx context.Context, userIds []uuid.UUID) (ValidateUserIDsRow, error) {
row := q.db.QueryRowContext(ctx, validateUserIDs, pq.Array(userIds))
var i ValidateUserIDsRow
err := row.Scan(pq.Array(&i.InvalidUserIds), &i.Ok)
return i, err
}
const deleteStaleWorkspaceAgentContextResources = `-- name: DeleteStaleWorkspaceAgentContextResources :exec
DELETE FROM workspace_agent_context_resources
WHERE workspace_agent_id = $1
AND NOT (source = ANY($2 :: text[]))
`
type DeleteStaleWorkspaceAgentContextResourcesParams struct {
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
ActiveSources []string `db:"active_sources" json:"active_sources"`
}
// Deletes any resources for the agent whose source is not in the
// supplied active set. Atomic alongside the snapshot upsert so the
// stored snapshot and resource rows always agree.
func (q *sqlQuerier) DeleteStaleWorkspaceAgentContextResources(ctx context.Context, arg DeleteStaleWorkspaceAgentContextResourcesParams) error {
_, err := q.db.ExecContext(ctx, deleteStaleWorkspaceAgentContextResources, arg.WorkspaceAgentID, pq.Array(arg.ActiveSources))
return err
}
const getLatestWorkspaceAgentContextSnapshot = `-- name: GetLatestWorkspaceAgentContextSnapshot :one
SELECT workspace_agent_id, version, aggregate_hash, snapshot_error, received_at FROM workspace_agent_context_snapshots
WHERE workspace_agent_id = $1
`
func (q *sqlQuerier) GetLatestWorkspaceAgentContextSnapshot(ctx context.Context, workspaceAgentID uuid.UUID) (WorkspaceAgentContextSnapshot, error) {
row := q.db.QueryRowContext(ctx, getLatestWorkspaceAgentContextSnapshot, workspaceAgentID)
var i WorkspaceAgentContextSnapshot
err := row.Scan(
&i.WorkspaceAgentID,
&i.Version,
&i.AggregateHash,
&i.SnapshotError,
&i.ReceivedAt,
)
return i, err
}
const listWorkspaceAgentContextResources = `-- name: ListWorkspaceAgentContextResources :many
SELECT workspace_agent_id, source, body_kind, body, content_hash, size_bytes, status, error, source_path, created_at, updated_at FROM workspace_agent_context_resources
WHERE workspace_agent_id = $1
ORDER BY source ASC
`
func (q *sqlQuerier) ListWorkspaceAgentContextResources(ctx context.Context, workspaceAgentID uuid.UUID) ([]WorkspaceAgentContextResource, error) {
rows, err := q.db.QueryContext(ctx, listWorkspaceAgentContextResources, workspaceAgentID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAgentContextResource
for rows.Next() {
var i WorkspaceAgentContextResource
if err := rows.Scan(
&i.WorkspaceAgentID,
&i.Source,
&i.BodyKind,
&i.Body,
&i.ContentHash,
&i.SizeBytes,
&i.Status,
&i.Error,
&i.SourcePath,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const upsertWorkspaceAgentContextResource = `-- name: UpsertWorkspaceAgentContextResource :one
INSERT INTO workspace_agent_context_resources (
workspace_agent_id,
source,
body_kind,
body,
content_hash,
size_bytes,
status,
error,
source_path,
created_at,
updated_at
) VALUES (
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
$9,
$10,
$10
)
ON CONFLICT (workspace_agent_id, source) DO UPDATE SET
body_kind = EXCLUDED.body_kind,
body = EXCLUDED.body,
content_hash = EXCLUDED.content_hash,
size_bytes = EXCLUDED.size_bytes,
status = EXCLUDED.status,
error = EXCLUDED.error,
source_path = EXCLUDED.source_path,
updated_at = EXCLUDED.updated_at
RETURNING workspace_agent_id, source, body_kind, body, content_hash, size_bytes, status, error, source_path, created_at, updated_at
`
type UpsertWorkspaceAgentContextResourceParams struct {
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
Source string `db:"source" json:"source"`
BodyKind WorkspaceAgentContextBodyKind `db:"body_kind" json:"body_kind"`
Body json.RawMessage `db:"body" json:"body"`
ContentHash []byte `db:"content_hash" json:"content_hash"`
SizeBytes int64 `db:"size_bytes" json:"size_bytes"`
Status WorkspaceAgentContextResourceStatus `db:"status" json:"status"`
Error string `db:"error" json:"error"`
SourcePath string `db:"source_path" json:"source_path"`
Now time.Time `db:"now" json:"now"`
}
func (q *sqlQuerier) UpsertWorkspaceAgentContextResource(ctx context.Context, arg UpsertWorkspaceAgentContextResourceParams) (WorkspaceAgentContextResource, error) {
row := q.db.QueryRowContext(ctx, upsertWorkspaceAgentContextResource,
arg.WorkspaceAgentID,
arg.Source,
arg.BodyKind,
arg.Body,
arg.ContentHash,
arg.SizeBytes,
arg.Status,
arg.Error,
arg.SourcePath,
arg.Now,
)
var i WorkspaceAgentContextResource
err := row.Scan(
&i.WorkspaceAgentID,
&i.Source,
&i.BodyKind,
&i.Body,
&i.ContentHash,
&i.SizeBytes,
&i.Status,
&i.Error,
&i.SourcePath,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const upsertWorkspaceAgentContextSnapshot = `-- name: UpsertWorkspaceAgentContextSnapshot :one
INSERT INTO workspace_agent_context_snapshots (
workspace_agent_id,
version,
aggregate_hash,
snapshot_error,
received_at
) VALUES (
$1,
$2,
$3,
$4,
$5
)
ON CONFLICT (workspace_agent_id) DO UPDATE SET
version = EXCLUDED.version,
aggregate_hash = EXCLUDED.aggregate_hash,
snapshot_error = EXCLUDED.snapshot_error,
received_at = EXCLUDED.received_at
RETURNING workspace_agent_id, version, aggregate_hash, snapshot_error, received_at
`
type UpsertWorkspaceAgentContextSnapshotParams struct {
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
Version int64 `db:"version" json:"version"`
AggregateHash []byte `db:"aggregate_hash" json:"aggregate_hash"`
SnapshotError string `db:"snapshot_error" json:"snapshot_error"`
ReceivedAt time.Time `db:"received_at" json:"received_at"`
}
func (q *sqlQuerier) UpsertWorkspaceAgentContextSnapshot(ctx context.Context, arg UpsertWorkspaceAgentContextSnapshotParams) (WorkspaceAgentContextSnapshot, error) {
row := q.db.QueryRowContext(ctx, upsertWorkspaceAgentContextSnapshot,
arg.WorkspaceAgentID,
arg.Version,
arg.AggregateHash,
arg.SnapshotError,
arg.ReceivedAt,
)
var i WorkspaceAgentContextSnapshot
err := row.Scan(
&i.WorkspaceAgentID,
&i.Version,
&i.AggregateHash,
&i.SnapshotError,
&i.ReceivedAt,
)
return i, err
}
const getWorkspaceAgentDevcontainersByAgentID = `-- name: GetWorkspaceAgentDevcontainersByAgentID :many
SELECT
id, workspace_agent_id, created_at, workspace_folder, config_path, name, subagent_id
FROM
workspace_agent_devcontainers
WHERE
workspace_agent_id = $1
ORDER BY
created_at, id
`
func (q *sqlQuerier) GetWorkspaceAgentDevcontainersByAgentID(ctx context.Context, workspaceAgentID uuid.UUID) ([]WorkspaceAgentDevcontainer, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentDevcontainersByAgentID, workspaceAgentID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAgentDevcontainer
for rows.Next() {
var i WorkspaceAgentDevcontainer
if err := rows.Scan(
&i.ID,
&i.WorkspaceAgentID,
&i.CreatedAt,
&i.WorkspaceFolder,
&i.ConfigPath,
&i.Name,
&i.SubagentID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertWorkspaceAgentDevcontainers = `-- name: InsertWorkspaceAgentDevcontainers :many
INSERT INTO
workspace_agent_devcontainers (workspace_agent_id, created_at, id, name, workspace_folder, config_path, subagent_id)
SELECT
$1::uuid AS workspace_agent_id,
$2::timestamptz AS created_at,
unnest($3::uuid[]) AS id,
unnest($4::text[]) AS name,
unnest($5::text[]) AS workspace_folder,
unnest($6::text[]) AS config_path,
NULLIF(unnest($7::uuid[]), '00000000-0000-0000-0000-000000000000')::uuid AS subagent_id
RETURNING workspace_agent_devcontainers.id, workspace_agent_devcontainers.workspace_agent_id, workspace_agent_devcontainers.created_at, workspace_agent_devcontainers.workspace_folder, workspace_agent_devcontainers.config_path, workspace_agent_devcontainers.name, workspace_agent_devcontainers.subagent_id
`
type InsertWorkspaceAgentDevcontainersParams struct {
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
ID []uuid.UUID `db:"id" json:"id"`
Name []string `db:"name" json:"name"`
WorkspaceFolder []string `db:"workspace_folder" json:"workspace_folder"`
ConfigPath []string `db:"config_path" json:"config_path"`
SubagentID []uuid.UUID `db:"subagent_id" json:"subagent_id"`
}
func (q *sqlQuerier) InsertWorkspaceAgentDevcontainers(ctx context.Context, arg InsertWorkspaceAgentDevcontainersParams) ([]WorkspaceAgentDevcontainer, error) {
rows, err := q.db.QueryContext(ctx, insertWorkspaceAgentDevcontainers,
arg.WorkspaceAgentID,
arg.CreatedAt,
pq.Array(arg.ID),
pq.Array(arg.Name),
pq.Array(arg.WorkspaceFolder),
pq.Array(arg.ConfigPath),
pq.Array(arg.SubagentID),
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAgentDevcontainer
for rows.Next() {
var i WorkspaceAgentDevcontainer
if err := rows.Scan(
&i.ID,
&i.WorkspaceAgentID,
&i.CreatedAt,
&i.WorkspaceFolder,
&i.ConfigPath,
&i.Name,
&i.SubagentID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const deleteWorkspaceAgentPortShare = `-- name: DeleteWorkspaceAgentPortShare :exec
DELETE FROM
workspace_agent_port_share
WHERE
workspace_id = $1
AND agent_name = $2
AND port = $3
`
type DeleteWorkspaceAgentPortShareParams struct {
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
AgentName string `db:"agent_name" json:"agent_name"`
Port int32 `db:"port" json:"port"`
}
func (q *sqlQuerier) DeleteWorkspaceAgentPortShare(ctx context.Context, arg DeleteWorkspaceAgentPortShareParams) error {
_, err := q.db.ExecContext(ctx, deleteWorkspaceAgentPortShare, arg.WorkspaceID, arg.AgentName, arg.Port)
return err
}
const deleteWorkspaceAgentPortSharesByTemplate = `-- name: DeleteWorkspaceAgentPortSharesByTemplate :exec
DELETE FROM
workspace_agent_port_share
WHERE
workspace_id IN (
SELECT
id
FROM
workspaces
WHERE
template_id = $1
)
`
func (q *sqlQuerier) DeleteWorkspaceAgentPortSharesByTemplate(ctx context.Context, templateID uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteWorkspaceAgentPortSharesByTemplate, templateID)
return err
}
const getWorkspaceAgentPortShare = `-- name: GetWorkspaceAgentPortShare :one
SELECT
workspace_id, agent_name, port, share_level, protocol
FROM
workspace_agent_port_share
WHERE
workspace_id = $1
AND agent_name = $2
AND port = $3
`
type GetWorkspaceAgentPortShareParams struct {
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
AgentName string `db:"agent_name" json:"agent_name"`
Port int32 `db:"port" json:"port"`
}
func (q *sqlQuerier) GetWorkspaceAgentPortShare(ctx context.Context, arg GetWorkspaceAgentPortShareParams) (WorkspaceAgentPortShare, error) {
row := q.db.QueryRowContext(ctx, getWorkspaceAgentPortShare, arg.WorkspaceID, arg.AgentName, arg.Port)
var i WorkspaceAgentPortShare
err := row.Scan(
&i.WorkspaceID,
&i.AgentName,
&i.Port,
&i.ShareLevel,
&i.Protocol,
)
return i, err
}
const listWorkspaceAgentPortShares = `-- name: ListWorkspaceAgentPortShares :many
SELECT
workspace_id, agent_name, port, share_level, protocol
FROM
workspace_agent_port_share
WHERE
workspace_id = $1
`
func (q *sqlQuerier) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgentPortShare, error) {
rows, err := q.db.QueryContext(ctx, listWorkspaceAgentPortShares, workspaceID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAgentPortShare
for rows.Next() {
var i WorkspaceAgentPortShare
if err := rows.Scan(
&i.WorkspaceID,
&i.AgentName,
&i.Port,
&i.ShareLevel,
&i.Protocol,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const reduceWorkspaceAgentShareLevelToAuthenticatedByTemplate = `-- name: ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate :exec
UPDATE
workspace_agent_port_share
SET
share_level = 'authenticated'
WHERE
share_level = 'public'
AND workspace_id IN (
SELECT
id
FROM
workspaces
WHERE
template_id = $1
)
`
func (q *sqlQuerier) ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx context.Context, templateID uuid.UUID) error {
_, err := q.db.ExecContext(ctx, reduceWorkspaceAgentShareLevelToAuthenticatedByTemplate, templateID)
return err
}
const upsertWorkspaceAgentPortShare = `-- name: UpsertWorkspaceAgentPortShare :one
INSERT INTO
workspace_agent_port_share (
workspace_id,
agent_name,
port,
share_level,
protocol
)
VALUES (
$1,
$2,
$3,
$4,
$5
)
ON CONFLICT (
workspace_id,
agent_name,
port
)
DO UPDATE SET
share_level = $4,
protocol = $5
RETURNING workspace_id, agent_name, port, share_level, protocol
`
type UpsertWorkspaceAgentPortShareParams struct {
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
AgentName string `db:"agent_name" json:"agent_name"`
Port int32 `db:"port" json:"port"`
ShareLevel AppSharingLevel `db:"share_level" json:"share_level"`
Protocol PortShareProtocol `db:"protocol" json:"protocol"`
}
func (q *sqlQuerier) UpsertWorkspaceAgentPortShare(ctx context.Context, arg UpsertWorkspaceAgentPortShareParams) (WorkspaceAgentPortShare, error) {
row := q.db.QueryRowContext(ctx, upsertWorkspaceAgentPortShare,
arg.WorkspaceID,
arg.AgentName,
arg.Port,
arg.ShareLevel,
arg.Protocol,
)
var i WorkspaceAgentPortShare
err := row.Scan(
&i.WorkspaceID,
&i.AgentName,
&i.Port,
&i.ShareLevel,
&i.Protocol,
)
return i, err
}
const fetchMemoryResourceMonitorsByAgentID = `-- name: FetchMemoryResourceMonitorsByAgentID :one
SELECT
agent_id, enabled, threshold, created_at, updated_at, state, debounced_until
FROM
workspace_agent_memory_resource_monitors
WHERE
agent_id = $1
`
func (q *sqlQuerier) FetchMemoryResourceMonitorsByAgentID(ctx context.Context, agentID uuid.UUID) (WorkspaceAgentMemoryResourceMonitor, error) {
row := q.db.QueryRowContext(ctx, fetchMemoryResourceMonitorsByAgentID, agentID)
var i WorkspaceAgentMemoryResourceMonitor
err := row.Scan(
&i.AgentID,
&i.Enabled,
&i.Threshold,
&i.CreatedAt,
&i.UpdatedAt,
&i.State,
&i.DebouncedUntil,
)
return i, err
}
const fetchMemoryResourceMonitorsUpdatedAfter = `-- name: FetchMemoryResourceMonitorsUpdatedAfter :many
SELECT
agent_id, enabled, threshold, created_at, updated_at, state, debounced_until
FROM
workspace_agent_memory_resource_monitors
WHERE
updated_at > $1
`
func (q *sqlQuerier) FetchMemoryResourceMonitorsUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]WorkspaceAgentMemoryResourceMonitor, error) {
rows, err := q.db.QueryContext(ctx, fetchMemoryResourceMonitorsUpdatedAfter, updatedAt)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAgentMemoryResourceMonitor
for rows.Next() {
var i WorkspaceAgentMemoryResourceMonitor
if err := rows.Scan(
&i.AgentID,
&i.Enabled,
&i.Threshold,
&i.CreatedAt,
&i.UpdatedAt,
&i.State,
&i.DebouncedUntil,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const fetchVolumesResourceMonitorsByAgentID = `-- name: FetchVolumesResourceMonitorsByAgentID :many
SELECT
agent_id, enabled, threshold, path, created_at, updated_at, state, debounced_until
FROM
workspace_agent_volume_resource_monitors
WHERE
agent_id = $1
`
func (q *sqlQuerier) FetchVolumesResourceMonitorsByAgentID(ctx context.Context, agentID uuid.UUID) ([]WorkspaceAgentVolumeResourceMonitor, error) {
rows, err := q.db.QueryContext(ctx, fetchVolumesResourceMonitorsByAgentID, agentID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAgentVolumeResourceMonitor
for rows.Next() {
var i WorkspaceAgentVolumeResourceMonitor
if err := rows.Scan(
&i.AgentID,
&i.Enabled,
&i.Threshold,
&i.Path,
&i.CreatedAt,
&i.UpdatedAt,
&i.State,
&i.DebouncedUntil,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const fetchVolumesResourceMonitorsUpdatedAfter = `-- name: FetchVolumesResourceMonitorsUpdatedAfter :many
SELECT
agent_id, enabled, threshold, path, created_at, updated_at, state, debounced_until
FROM
workspace_agent_volume_resource_monitors
WHERE
updated_at > $1
`
func (q *sqlQuerier) FetchVolumesResourceMonitorsUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]WorkspaceAgentVolumeResourceMonitor, error) {
rows, err := q.db.QueryContext(ctx, fetchVolumesResourceMonitorsUpdatedAfter, updatedAt)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAgentVolumeResourceMonitor
for rows.Next() {
var i WorkspaceAgentVolumeResourceMonitor
if err := rows.Scan(
&i.AgentID,
&i.Enabled,
&i.Threshold,
&i.Path,
&i.CreatedAt,
&i.UpdatedAt,
&i.State,
&i.DebouncedUntil,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertMemoryResourceMonitor = `-- name: InsertMemoryResourceMonitor :one
INSERT INTO
workspace_agent_memory_resource_monitors (
agent_id,
enabled,
state,
threshold,
created_at,
updated_at,
debounced_until
)
VALUES
($1, $2, $3, $4, $5, $6, $7) RETURNING agent_id, enabled, threshold, created_at, updated_at, state, debounced_until
`
type InsertMemoryResourceMonitorParams struct {
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
Enabled bool `db:"enabled" json:"enabled"`
State WorkspaceAgentMonitorState `db:"state" json:"state"`
Threshold int32 `db:"threshold" json:"threshold"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
DebouncedUntil time.Time `db:"debounced_until" json:"debounced_until"`
}
func (q *sqlQuerier) InsertMemoryResourceMonitor(ctx context.Context, arg InsertMemoryResourceMonitorParams) (WorkspaceAgentMemoryResourceMonitor, error) {
row := q.db.QueryRowContext(ctx, insertMemoryResourceMonitor,
arg.AgentID,
arg.Enabled,
arg.State,
arg.Threshold,
arg.CreatedAt,
arg.UpdatedAt,
arg.DebouncedUntil,
)
var i WorkspaceAgentMemoryResourceMonitor
err := row.Scan(
&i.AgentID,
&i.Enabled,
&i.Threshold,
&i.CreatedAt,
&i.UpdatedAt,
&i.State,
&i.DebouncedUntil,
)
return i, err
}
const insertVolumeResourceMonitor = `-- name: InsertVolumeResourceMonitor :one
INSERT INTO
workspace_agent_volume_resource_monitors (
agent_id,
path,
enabled,
state,
threshold,
created_at,
updated_at,
debounced_until
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8) RETURNING agent_id, enabled, threshold, path, created_at, updated_at, state, debounced_until
`
type InsertVolumeResourceMonitorParams struct {
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
Path string `db:"path" json:"path"`
Enabled bool `db:"enabled" json:"enabled"`
State WorkspaceAgentMonitorState `db:"state" json:"state"`
Threshold int32 `db:"threshold" json:"threshold"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
DebouncedUntil time.Time `db:"debounced_until" json:"debounced_until"`
}
func (q *sqlQuerier) InsertVolumeResourceMonitor(ctx context.Context, arg InsertVolumeResourceMonitorParams) (WorkspaceAgentVolumeResourceMonitor, error) {
row := q.db.QueryRowContext(ctx, insertVolumeResourceMonitor,
arg.AgentID,
arg.Path,
arg.Enabled,
arg.State,
arg.Threshold,
arg.CreatedAt,
arg.UpdatedAt,
arg.DebouncedUntil,
)
var i WorkspaceAgentVolumeResourceMonitor
err := row.Scan(
&i.AgentID,
&i.Enabled,
&i.Threshold,
&i.Path,
&i.CreatedAt,
&i.UpdatedAt,
&i.State,
&i.DebouncedUntil,
)
return i, err
}
const updateMemoryResourceMonitor = `-- name: UpdateMemoryResourceMonitor :exec
UPDATE workspace_agent_memory_resource_monitors
SET
updated_at = $2,
state = $3,
debounced_until = $4
WHERE
agent_id = $1
`
type UpdateMemoryResourceMonitorParams struct {
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
State WorkspaceAgentMonitorState `db:"state" json:"state"`
DebouncedUntil time.Time `db:"debounced_until" json:"debounced_until"`
}
func (q *sqlQuerier) UpdateMemoryResourceMonitor(ctx context.Context, arg UpdateMemoryResourceMonitorParams) error {
_, err := q.db.ExecContext(ctx, updateMemoryResourceMonitor,
arg.AgentID,
arg.UpdatedAt,
arg.State,
arg.DebouncedUntil,
)
return err
}
const updateVolumeResourceMonitor = `-- name: UpdateVolumeResourceMonitor :exec
UPDATE workspace_agent_volume_resource_monitors
SET
updated_at = $3,
state = $4,
debounced_until = $5
WHERE
agent_id = $1 AND path = $2
`
type UpdateVolumeResourceMonitorParams struct {
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
Path string `db:"path" json:"path"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
State WorkspaceAgentMonitorState `db:"state" json:"state"`
DebouncedUntil time.Time `db:"debounced_until" json:"debounced_until"`
}
func (q *sqlQuerier) UpdateVolumeResourceMonitor(ctx context.Context, arg UpdateVolumeResourceMonitorParams) error {
_, err := q.db.ExecContext(ctx, updateVolumeResourceMonitor,
arg.AgentID,
arg.Path,
arg.UpdatedAt,
arg.State,
arg.DebouncedUntil,
)
return err
}
const batchUpdateWorkspaceAgentMetadata = `-- name: BatchUpdateWorkspaceAgentMetadata :exec
WITH metadata AS (
SELECT
unnest($1::uuid[]) AS workspace_agent_id,
unnest($2::text[]) AS key,
unnest($3::text[]) AS value,
unnest($4::text[]) AS error,
unnest($5::timestamptz[]) AS collected_at
)
UPDATE
workspace_agent_metadata wam
SET
value = m.value,
error = m.error,
collected_at = m.collected_at
FROM
metadata m
WHERE
wam.workspace_agent_id = m.workspace_agent_id
AND wam.key = m.key
`
type BatchUpdateWorkspaceAgentMetadataParams struct {
WorkspaceAgentID []uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
Key []string `db:"key" json:"key"`
Value []string `db:"value" json:"value"`
Error []string `db:"error" json:"error"`
CollectedAt []time.Time `db:"collected_at" json:"collected_at"`
}
func (q *sqlQuerier) BatchUpdateWorkspaceAgentMetadata(ctx context.Context, arg BatchUpdateWorkspaceAgentMetadataParams) error {
_, err := q.db.ExecContext(ctx, batchUpdateWorkspaceAgentMetadata,
pq.Array(arg.WorkspaceAgentID),
pq.Array(arg.Key),
pq.Array(arg.Value),
pq.Array(arg.Error),
pq.Array(arg.CollectedAt),
)
return err
}
const deleteOldWorkspaceAgentLogs = `-- name: DeleteOldWorkspaceAgentLogs :execrows
WITH
latest_builds AS (
SELECT
workspace_id, max(build_number) AS max_build_number
FROM
workspace_builds
GROUP BY
workspace_id
),
old_agents AS (
SELECT
wa.id
FROM
workspace_agents AS wa
JOIN
workspace_resources AS wr
ON
wa.resource_id = wr.id
JOIN
workspace_builds AS wb
ON
wb.job_id = wr.job_id
LEFT JOIN
latest_builds
ON
latest_builds.workspace_id = wb.workspace_id
AND
latest_builds.max_build_number = wb.build_number
WHERE
-- Filter out the latest builds for each workspace.
latest_builds.workspace_id IS NULL
AND CASE
-- If the last time the agent connected was before @threshold
WHEN wa.last_connected_at IS NOT NULL THEN
wa.last_connected_at < $1 :: timestamptz
-- The agent never connected, and was created before @threshold
ELSE wa.created_at < $1 :: timestamptz
END
)
DELETE FROM workspace_agent_logs WHERE agent_id IN (SELECT id FROM old_agents)
`
// If an agent hasn't connected within the retention period, we purge its logs.
// Exception: if the logs are related to the latest build, we keep those around.
// Logs can take up a lot of space, so it's important we clean up frequently.
func (q *sqlQuerier) DeleteOldWorkspaceAgentLogs(ctx context.Context, threshold time.Time) (int64, error) {
result, err := q.db.ExecContext(ctx, deleteOldWorkspaceAgentLogs, threshold)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const deleteWorkspaceSubAgentByID = `-- name: DeleteWorkspaceSubAgentByID :exec
WITH soft_deleted_agents AS (
UPDATE workspace_agents
SET deleted = TRUE
WHERE id = $1
AND parent_id IS NOT NULL
AND deleted = FALSE
RETURNING id
), purged_context_resources AS (
DELETE FROM workspace_agent_context_resources
WHERE workspace_agent_id IN (SELECT id FROM soft_deleted_agents)
)
DELETE FROM workspace_agent_context_snapshots
WHERE workspace_agent_id IN (SELECT id FROM soft_deleted_agents)
`
// Soft-deletes a single sub-agent (a child agent such as a devcontainer
// agent). Called from the DeleteSubAgent RPC when a sub-agent is torn
// down, which can happen mid-build without a full workspace rebuild.
//
// Agent context rows are hard-deleted for the same reason as in
// SoftDeletePriorWorkspaceAgents: they only describe live agents, the
// rebuild-time soft-delete queries skip already-deleted agents, and
// agents are never hard-deleted, so the rows would otherwise orphan
// forever.
func (q *sqlQuerier) DeleteWorkspaceSubAgentByID(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteWorkspaceSubAgentByID, id)
return err
}
const getAuthenticatedWorkspaceAgentAndBuildByAuthToken = `-- name: GetAuthenticatedWorkspaceAgentAndBuildByAuthToken :one
SELECT
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at, workspaces.group_acl, workspaces.user_acl,
workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order, workspace_agents.parent_id, workspace_agents.api_key_scope, workspace_agents.deleted,
workspace_build_with_user.id, workspace_build_with_user.created_at, workspace_build_with_user.updated_at, workspace_build_with_user.workspace_id, workspace_build_with_user.template_version_id, workspace_build_with_user.build_number, workspace_build_with_user.transition, workspace_build_with_user.initiator_id, workspace_build_with_user.job_id, workspace_build_with_user.deadline, workspace_build_with_user.reason, workspace_build_with_user.daily_cost, workspace_build_with_user.max_deadline, workspace_build_with_user.template_version_preset_id, workspace_build_with_user.has_ai_task, workspace_build_with_user.has_external_agent, workspace_build_with_user.notified_autostop_deadline, workspace_build_with_user.initiator_by_avatar_url, workspace_build_with_user.initiator_by_username, workspace_build_with_user.initiator_by_name,
tasks.id AS task_id
FROM
workspace_agents
JOIN
workspace_resources
ON
workspace_agents.resource_id = workspace_resources.id
JOIN
workspace_build_with_user
ON
workspace_resources.job_id = workspace_build_with_user.job_id
JOIN
workspaces
ON
workspace_build_with_user.workspace_id = workspaces.id
LEFT JOIN
tasks
ON
tasks.workspace_id = workspaces.id
WHERE
-- This should only match 1 agent, so 1 returned row or 0.
workspace_agents.auth_token = $1::uuid
AND workspaces.deleted = FALSE
-- Filter out deleted sub agents.
AND workspace_agents.deleted = FALSE
-- Filter out builds that are not the latest, with exception for shutdown case.
-- Use CASE for short-circuiting: check normal case first (most common), then shutdown case.
AND CASE
-- Normal case: Agent's build is the latest build.
WHEN workspace_build_with_user.build_number = (
SELECT
MAX(build_number)
FROM
workspace_builds
WHERE
workspace_id = workspace_build_with_user.workspace_id
) THEN TRUE
-- Shutdown case: Agent from previous START build during STOP build execution.
WHEN workspace_build_with_user.transition = 'start'
-- Agent's START build job succeeded.
AND (SELECT job_status FROM provisioner_jobs WHERE id = workspace_build_with_user.job_id) = 'succeeded'
-- Latest build is a STOP build whose job is still active,
-- and agent's build is immediately previous.
AND EXISTS (
SELECT 1
FROM workspace_builds latest
JOIN provisioner_jobs pj ON pj.id = latest.job_id
WHERE latest.workspace_id = workspace_build_with_user.workspace_id
AND latest.build_number = workspace_build_with_user.build_number + 1
AND latest.build_number = (
SELECT MAX(build_number)
FROM workspace_builds l2
WHERE l2.workspace_id = latest.workspace_id
)
AND latest.transition = 'stop'
AND pj.job_status IN ('pending', 'running')
) THEN TRUE
ELSE FALSE
END
`
type GetAuthenticatedWorkspaceAgentAndBuildByAuthTokenRow struct {
WorkspaceTable WorkspaceTable `db:"workspace_table" json:"workspace_table"`
WorkspaceAgent WorkspaceAgent `db:"workspace_agent" json:"workspace_agent"`
WorkspaceBuild WorkspaceBuild `db:"workspace_build" json:"workspace_build"`
TaskID uuid.NullUUID `db:"task_id" json:"task_id"`
}
// GetAuthenticatedWorkspaceAgentAndBuildByAuthToken returns an authenticated
// workspace agent and its associated build. During normal operation, this is
// the latest build. During shutdown, this may be the previous START build while
// the STOP build is executing, allowing shutdown scripts to authenticate (see
// issue #19467).
func (q *sqlQuerier) GetAuthenticatedWorkspaceAgentAndBuildByAuthToken(ctx context.Context, authToken uuid.UUID) (GetAuthenticatedWorkspaceAgentAndBuildByAuthTokenRow, error) {
row := q.db.QueryRowContext(ctx, getAuthenticatedWorkspaceAgentAndBuildByAuthToken, authToken)
var i GetAuthenticatedWorkspaceAgentAndBuildByAuthTokenRow
err := row.Scan(
&i.WorkspaceTable.ID,
&i.WorkspaceTable.CreatedAt,
&i.WorkspaceTable.UpdatedAt,
&i.WorkspaceTable.OwnerID,
&i.WorkspaceTable.OrganizationID,
&i.WorkspaceTable.TemplateID,
&i.WorkspaceTable.Deleted,
&i.WorkspaceTable.Name,
&i.WorkspaceTable.AutostartSchedule,
&i.WorkspaceTable.Ttl,
&i.WorkspaceTable.LastUsedAt,
&i.WorkspaceTable.DormantAt,
&i.WorkspaceTable.DeletingAt,
&i.WorkspaceTable.AutomaticUpdates,
&i.WorkspaceTable.Favorite,
&i.WorkspaceTable.NextStartAt,
&i.WorkspaceTable.GroupACL,
&i.WorkspaceTable.UserACL,
&i.WorkspaceAgent.ID,
&i.WorkspaceAgent.CreatedAt,
&i.WorkspaceAgent.UpdatedAt,
&i.WorkspaceAgent.Name,
&i.WorkspaceAgent.FirstConnectedAt,
&i.WorkspaceAgent.LastConnectedAt,
&i.WorkspaceAgent.DisconnectedAt,
&i.WorkspaceAgent.ResourceID,
&i.WorkspaceAgent.AuthToken,
&i.WorkspaceAgent.AuthInstanceID,
&i.WorkspaceAgent.Architecture,
&i.WorkspaceAgent.EnvironmentVariables,
&i.WorkspaceAgent.OperatingSystem,
&i.WorkspaceAgent.InstanceMetadata,
&i.WorkspaceAgent.ResourceMetadata,
&i.WorkspaceAgent.Directory,
&i.WorkspaceAgent.Version,
&i.WorkspaceAgent.LastConnectedReplicaID,
&i.WorkspaceAgent.ConnectionTimeoutSeconds,
&i.WorkspaceAgent.TroubleshootingURL,
&i.WorkspaceAgent.MOTDFile,
&i.WorkspaceAgent.LifecycleState,
&i.WorkspaceAgent.ExpandedDirectory,
&i.WorkspaceAgent.LogsLength,
&i.WorkspaceAgent.LogsOverflowed,
&i.WorkspaceAgent.StartedAt,
&i.WorkspaceAgent.ReadyAt,
pq.Array(&i.WorkspaceAgent.Subsystems),
pq.Array(&i.WorkspaceAgent.DisplayApps),
&i.WorkspaceAgent.APIVersion,
&i.WorkspaceAgent.DisplayOrder,
&i.WorkspaceAgent.ParentID,
&i.WorkspaceAgent.APIKeyScope,
&i.WorkspaceAgent.Deleted,
&i.WorkspaceBuild.ID,
&i.WorkspaceBuild.CreatedAt,
&i.WorkspaceBuild.UpdatedAt,
&i.WorkspaceBuild.WorkspaceID,
&i.WorkspaceBuild.TemplateVersionID,
&i.WorkspaceBuild.BuildNumber,
&i.WorkspaceBuild.Transition,
&i.WorkspaceBuild.InitiatorID,
&i.WorkspaceBuild.JobID,
&i.WorkspaceBuild.Deadline,
&i.WorkspaceBuild.Reason,
&i.WorkspaceBuild.DailyCost,
&i.WorkspaceBuild.MaxDeadline,
&i.WorkspaceBuild.TemplateVersionPresetID,
&i.WorkspaceBuild.HasAITask,
&i.WorkspaceBuild.HasExternalAgent,
&i.WorkspaceBuild.NotifiedAutostopDeadline,
&i.WorkspaceBuild.InitiatorByAvatarUrl,
&i.WorkspaceBuild.InitiatorByUsername,
&i.WorkspaceBuild.InitiatorByName,
&i.TaskID,
)
return i, err
}
const getExternalAgentTokensByTemplateID = `-- name: GetExternalAgentTokensByTemplateID :many
SELECT
workspaces.id AS workspace_id,
workspaces.name AS workspace_name,
workspace_agents.id AS agent_id,
workspace_agents.name AS agent_name,
workspace_agents.auth_token AS agent_token
FROM
workspaces
JOIN (
-- latest build per workspace
SELECT DISTINCT ON (workspace_id)
id, workspace_id, job_id, transition, has_external_agent
FROM
workspace_builds
ORDER BY
workspace_id, build_number DESC
) AS latest_builds
ON
latest_builds.workspace_id = workspaces.id
JOIN
provisioner_jobs
ON
provisioner_jobs.id = latest_builds.job_id
JOIN
workspace_resources
ON
workspace_resources.job_id = latest_builds.job_id
JOIN
workspace_agents
ON
workspace_agents.resource_id = workspace_resources.id
WHERE
workspaces.template_id = $1
AND (
$2 :: uuid = '00000000-0000-0000-0000-000000000000' :: uuid
OR workspaces.owner_id = $2
)
AND workspaces.deleted = FALSE
AND latest_builds.has_external_agent = TRUE
AND latest_builds.transition = 'start' :: workspace_transition
AND provisioner_jobs.job_status = 'succeeded' :: provisioner_job_status
AND workspace_agents.deleted = FALSE
AND workspace_agents.auth_instance_id IS NULL
`
type GetExternalAgentTokensByTemplateIDParams struct {
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
}
type GetExternalAgentTokensByTemplateIDRow struct {
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
WorkspaceName string `db:"workspace_name" json:"workspace_name"`
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
AgentName string `db:"agent_name" json:"agent_name"`
AgentToken uuid.UUID `db:"agent_token" json:"agent_token"`
}
// GetExternalAgentTokensByTemplateID returns the auth tokens for all
// non-deleted external agents on the latest build of every running workspace
// of the given template. "Running" means the latest build has
// transition=start and job_status=succeeded (matches the workspace-status
// definition used by coderd/database/queries/workspaces.sql).
// An owner_id of '00000000-0000-0000-0000-000000000000' (uuid.Nil) means
// "all owners"; any other value restricts results to workspaces owned by
// that user.
func (q *sqlQuerier) GetExternalAgentTokensByTemplateID(ctx context.Context, arg GetExternalAgentTokensByTemplateIDParams) ([]GetExternalAgentTokensByTemplateIDRow, error) {
rows, err := q.db.QueryContext(ctx, getExternalAgentTokensByTemplateID, arg.TemplateID, arg.OwnerID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetExternalAgentTokensByTemplateIDRow
for rows.Next() {
var i GetExternalAgentTokensByTemplateIDRow
if err := rows.Scan(
&i.WorkspaceID,
&i.WorkspaceName,
&i.AgentID,
&i.AgentName,
&i.AgentToken,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceAgentAndWorkspaceByID = `-- name: GetWorkspaceAgentAndWorkspaceByID :one
SELECT
workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order, workspace_agents.parent_id, workspace_agents.api_key_scope, workspace_agents.deleted,
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at, workspaces.group_acl, workspaces.user_acl,
users.username as owner_username
FROM
workspace_agents
JOIN
workspace_resources ON workspace_agents.resource_id = workspace_resources.id
JOIN
provisioner_jobs ON workspace_resources.job_id = provisioner_jobs.id
JOIN
workspace_builds ON provisioner_jobs.id = workspace_builds.job_id
JOIN
workspaces ON workspace_builds.workspace_id = workspaces.id
JOIN
users ON workspaces.owner_id = users.id
WHERE
workspace_agents.id = $1
AND workspace_agents.deleted = FALSE
AND provisioner_jobs.type = 'workspace_build'::provisioner_job_type
AND workspaces.deleted = FALSE
AND users.deleted = FALSE
LIMIT 1
`
type GetWorkspaceAgentAndWorkspaceByIDRow struct {
WorkspaceAgent WorkspaceAgent `db:"workspace_agent" json:"workspace_agent"`
WorkspaceTable WorkspaceTable `db:"workspace_table" json:"workspace_table"`
OwnerUsername string `db:"owner_username" json:"owner_username"`
}
func (q *sqlQuerier) GetWorkspaceAgentAndWorkspaceByID(ctx context.Context, id uuid.UUID) (GetWorkspaceAgentAndWorkspaceByIDRow, error) {
row := q.db.QueryRowContext(ctx, getWorkspaceAgentAndWorkspaceByID, id)
var i GetWorkspaceAgentAndWorkspaceByIDRow
err := row.Scan(
&i.WorkspaceAgent.ID,
&i.WorkspaceAgent.CreatedAt,
&i.WorkspaceAgent.UpdatedAt,
&i.WorkspaceAgent.Name,
&i.WorkspaceAgent.FirstConnectedAt,
&i.WorkspaceAgent.LastConnectedAt,
&i.WorkspaceAgent.DisconnectedAt,
&i.WorkspaceAgent.ResourceID,
&i.WorkspaceAgent.AuthToken,
&i.WorkspaceAgent.AuthInstanceID,
&i.WorkspaceAgent.Architecture,
&i.WorkspaceAgent.EnvironmentVariables,
&i.WorkspaceAgent.OperatingSystem,
&i.WorkspaceAgent.InstanceMetadata,
&i.WorkspaceAgent.ResourceMetadata,
&i.WorkspaceAgent.Directory,
&i.WorkspaceAgent.Version,
&i.WorkspaceAgent.LastConnectedReplicaID,
&i.WorkspaceAgent.ConnectionTimeoutSeconds,
&i.WorkspaceAgent.TroubleshootingURL,
&i.WorkspaceAgent.MOTDFile,
&i.WorkspaceAgent.LifecycleState,
&i.WorkspaceAgent.ExpandedDirectory,
&i.WorkspaceAgent.LogsLength,
&i.WorkspaceAgent.LogsOverflowed,
&i.WorkspaceAgent.StartedAt,
&i.WorkspaceAgent.ReadyAt,
pq.Array(&i.WorkspaceAgent.Subsystems),
pq.Array(&i.WorkspaceAgent.DisplayApps),
&i.WorkspaceAgent.APIVersion,
&i.WorkspaceAgent.DisplayOrder,
&i.WorkspaceAgent.ParentID,
&i.WorkspaceAgent.APIKeyScope,
&i.WorkspaceAgent.Deleted,
&i.WorkspaceTable.ID,
&i.WorkspaceTable.CreatedAt,
&i.WorkspaceTable.UpdatedAt,
&i.WorkspaceTable.OwnerID,
&i.WorkspaceTable.OrganizationID,
&i.WorkspaceTable.TemplateID,
&i.WorkspaceTable.Deleted,
&i.WorkspaceTable.Name,
&i.WorkspaceTable.AutostartSchedule,
&i.WorkspaceTable.Ttl,
&i.WorkspaceTable.LastUsedAt,
&i.WorkspaceTable.DormantAt,
&i.WorkspaceTable.DeletingAt,
&i.WorkspaceTable.AutomaticUpdates,
&i.WorkspaceTable.Favorite,
&i.WorkspaceTable.NextStartAt,
&i.WorkspaceTable.GroupACL,
&i.WorkspaceTable.UserACL,
&i.OwnerUsername,
)
return i, err
}
const getWorkspaceAgentByID = `-- name: GetWorkspaceAgentByID :one
SELECT
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope, deleted
FROM
workspace_agents
WHERE
id = $1
-- Filter out deleted sub agents.
AND deleted = FALSE
`
func (q *sqlQuerier) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (WorkspaceAgent, error) {
row := q.db.QueryRowContext(ctx, getWorkspaceAgentByID, id)
var i WorkspaceAgent
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Name,
&i.FirstConnectedAt,
&i.LastConnectedAt,
&i.DisconnectedAt,
&i.ResourceID,
&i.AuthToken,
&i.AuthInstanceID,
&i.Architecture,
&i.EnvironmentVariables,
&i.OperatingSystem,
&i.InstanceMetadata,
&i.ResourceMetadata,
&i.Directory,
&i.Version,
&i.LastConnectedReplicaID,
&i.ConnectionTimeoutSeconds,
&i.TroubleshootingURL,
&i.MOTDFile,
&i.LifecycleState,
&i.ExpandedDirectory,
&i.LogsLength,
&i.LogsOverflowed,
&i.StartedAt,
&i.ReadyAt,
pq.Array(&i.Subsystems),
pq.Array(&i.DisplayApps),
&i.APIVersion,
&i.DisplayOrder,
&i.ParentID,
&i.APIKeyScope,
&i.Deleted,
)
return i, err
}
const getWorkspaceAgentLifecycleStateByID = `-- name: GetWorkspaceAgentLifecycleStateByID :one
SELECT
lifecycle_state,
started_at,
ready_at
FROM
workspace_agents
WHERE
id = $1
`
type GetWorkspaceAgentLifecycleStateByIDRow struct {
LifecycleState WorkspaceAgentLifecycleState `db:"lifecycle_state" json:"lifecycle_state"`
StartedAt sql.NullTime `db:"started_at" json:"started_at"`
ReadyAt sql.NullTime `db:"ready_at" json:"ready_at"`
}
func (q *sqlQuerier) GetWorkspaceAgentLifecycleStateByID(ctx context.Context, id uuid.UUID) (GetWorkspaceAgentLifecycleStateByIDRow, error) {
row := q.db.QueryRowContext(ctx, getWorkspaceAgentLifecycleStateByID, id)
var i GetWorkspaceAgentLifecycleStateByIDRow
err := row.Scan(&i.LifecycleState, &i.StartedAt, &i.ReadyAt)
return i, err
}
const getWorkspaceAgentLogSourcesByAgentIDs = `-- name: GetWorkspaceAgentLogSourcesByAgentIDs :many
SELECT workspace_agent_id, id, created_at, display_name, icon FROM workspace_agent_log_sources WHERE workspace_agent_id = ANY($1 :: uuid [ ])
`
func (q *sqlQuerier) GetWorkspaceAgentLogSourcesByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentLogSource, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentLogSourcesByAgentIDs, pq.Array(ids))
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAgentLogSource
for rows.Next() {
var i WorkspaceAgentLogSource
if err := rows.Scan(
&i.WorkspaceAgentID,
&i.ID,
&i.CreatedAt,
&i.DisplayName,
&i.Icon,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceAgentLogsAfter = `-- name: GetWorkspaceAgentLogsAfter :many
SELECT
agent_id, created_at, output, id, level, log_source_id
FROM
workspace_agent_logs
WHERE
agent_id = $1
AND (
id > $2
) ORDER BY id ASC
`
type GetWorkspaceAgentLogsAfterParams struct {
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
CreatedAfter int64 `db:"created_after" json:"created_after"`
}
func (q *sqlQuerier) GetWorkspaceAgentLogsAfter(ctx context.Context, arg GetWorkspaceAgentLogsAfterParams) ([]WorkspaceAgentLog, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentLogsAfter, arg.AgentID, arg.CreatedAfter)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAgentLog
for rows.Next() {
var i WorkspaceAgentLog
if err := rows.Scan(
&i.AgentID,
&i.CreatedAt,
&i.Output,
&i.ID,
&i.Level,
&i.LogSourceID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceAgentMetadata = `-- name: GetWorkspaceAgentMetadata :many
SELECT
workspace_agent_id, display_name, key, script, value, error, timeout, interval, collected_at, display_order
FROM
workspace_agent_metadata
WHERE
workspace_agent_id = $1
AND CASE WHEN COALESCE(array_length($2::text[], 1), 0) > 0 THEN key = ANY($2::text[]) ELSE TRUE END
`
type GetWorkspaceAgentMetadataParams struct {
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
Keys []string `db:"keys" json:"keys"`
}
func (q *sqlQuerier) GetWorkspaceAgentMetadata(ctx context.Context, arg GetWorkspaceAgentMetadataParams) ([]WorkspaceAgentMetadatum, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentMetadata, arg.WorkspaceAgentID, pq.Array(arg.Keys))
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAgentMetadatum
for rows.Next() {
var i WorkspaceAgentMetadatum
if err := rows.Scan(
&i.WorkspaceAgentID,
&i.DisplayName,
&i.Key,
&i.Script,
&i.Value,
&i.Error,
&i.Timeout,
&i.Interval,
&i.CollectedAt,
&i.DisplayOrder,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceAgentScriptTimingsByBuildID = `-- name: GetWorkspaceAgentScriptTimingsByBuildID :many
SELECT
DISTINCT ON (workspace_agent_script_timings.script_id) workspace_agent_script_timings.script_id, workspace_agent_script_timings.started_at, workspace_agent_script_timings.ended_at, workspace_agent_script_timings.exit_code, workspace_agent_script_timings.stage, workspace_agent_script_timings.status,
workspace_agent_scripts.display_name,
workspace_agents.id as workspace_agent_id,
workspace_agents.name as workspace_agent_name
FROM workspace_agent_script_timings
INNER JOIN workspace_agent_scripts ON workspace_agent_scripts.id = workspace_agent_script_timings.script_id
INNER JOIN workspace_agents ON workspace_agents.id = workspace_agent_scripts.workspace_agent_id
INNER JOIN workspace_resources ON workspace_resources.id = workspace_agents.resource_id
INNER JOIN workspace_builds ON workspace_builds.job_id = workspace_resources.job_id
WHERE workspace_builds.id = $1
ORDER BY workspace_agent_script_timings.script_id, workspace_agent_script_timings.started_at
`
type GetWorkspaceAgentScriptTimingsByBuildIDRow struct {
ScriptID uuid.UUID `db:"script_id" json:"script_id"`
StartedAt time.Time `db:"started_at" json:"started_at"`
EndedAt time.Time `db:"ended_at" json:"ended_at"`
ExitCode int32 `db:"exit_code" json:"exit_code"`
Stage WorkspaceAgentScriptTimingStage `db:"stage" json:"stage"`
Status WorkspaceAgentScriptTimingStatus `db:"status" json:"status"`
DisplayName string `db:"display_name" json:"display_name"`
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
WorkspaceAgentName string `db:"workspace_agent_name" json:"workspace_agent_name"`
}
func (q *sqlQuerier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Context, id uuid.UUID) ([]GetWorkspaceAgentScriptTimingsByBuildIDRow, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentScriptTimingsByBuildID, id)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetWorkspaceAgentScriptTimingsByBuildIDRow
for rows.Next() {
var i GetWorkspaceAgentScriptTimingsByBuildIDRow
if err := rows.Scan(
&i.ScriptID,
&i.StartedAt,
&i.EndedAt,
&i.ExitCode,
&i.Stage,
&i.Status,
&i.DisplayName,
&i.WorkspaceAgentID,
&i.WorkspaceAgentName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceAgentsByInstanceID = `-- name: GetWorkspaceAgentsByInstanceID :many
SELECT
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope, deleted
FROM
workspace_agents
WHERE
auth_instance_id = $1 :: TEXT
-- Filter out deleted agents.
AND deleted = FALSE
-- Filter out sub agents, they do not authenticate with auth_instance_id.
AND parent_id IS NULL
ORDER BY
created_at DESC
`
func (q *sqlQuerier) GetWorkspaceAgentsByInstanceID(ctx context.Context, authInstanceID string) ([]WorkspaceAgent, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsByInstanceID, authInstanceID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAgent
for rows.Next() {
var i WorkspaceAgent
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Name,
&i.FirstConnectedAt,
&i.LastConnectedAt,
&i.DisconnectedAt,
&i.ResourceID,
&i.AuthToken,
&i.AuthInstanceID,
&i.Architecture,
&i.EnvironmentVariables,
&i.OperatingSystem,
&i.InstanceMetadata,
&i.ResourceMetadata,
&i.Directory,
&i.Version,
&i.LastConnectedReplicaID,
&i.ConnectionTimeoutSeconds,
&i.TroubleshootingURL,
&i.MOTDFile,
&i.LifecycleState,
&i.ExpandedDirectory,
&i.LogsLength,
&i.LogsOverflowed,
&i.StartedAt,
&i.ReadyAt,
pq.Array(&i.Subsystems),
pq.Array(&i.DisplayApps),
&i.APIVersion,
&i.DisplayOrder,
&i.ParentID,
&i.APIKeyScope,
&i.Deleted,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceAgentsByParentID = `-- name: GetWorkspaceAgentsByParentID :many
SELECT
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope, deleted
FROM
workspace_agents
WHERE
parent_id = $1::uuid
AND deleted = FALSE
`
func (q *sqlQuerier) GetWorkspaceAgentsByParentID(ctx context.Context, parentID uuid.UUID) ([]WorkspaceAgent, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsByParentID, parentID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAgent
for rows.Next() {
var i WorkspaceAgent
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Name,
&i.FirstConnectedAt,
&i.LastConnectedAt,
&i.DisconnectedAt,
&i.ResourceID,
&i.AuthToken,
&i.AuthInstanceID,
&i.Architecture,
&i.EnvironmentVariables,
&i.OperatingSystem,
&i.InstanceMetadata,
&i.ResourceMetadata,
&i.Directory,
&i.Version,
&i.LastConnectedReplicaID,
&i.ConnectionTimeoutSeconds,
&i.TroubleshootingURL,
&i.MOTDFile,
&i.LifecycleState,
&i.ExpandedDirectory,
&i.LogsLength,
&i.LogsOverflowed,
&i.StartedAt,
&i.ReadyAt,
pq.Array(&i.Subsystems),
pq.Array(&i.DisplayApps),
&i.APIVersion,
&i.DisplayOrder,
&i.ParentID,
&i.APIKeyScope,
&i.Deleted,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceAgentsByResourceIDs = `-- name: GetWorkspaceAgentsByResourceIDs :many
SELECT
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope, deleted
FROM
workspace_agents
WHERE
resource_id = ANY($1 :: uuid [ ])
-- Filter out deleted sub agents.
AND deleted = FALSE
`
func (q *sqlQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgent, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsByResourceIDs, pq.Array(ids))
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAgent
for rows.Next() {
var i WorkspaceAgent
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Name,
&i.FirstConnectedAt,
&i.LastConnectedAt,
&i.DisconnectedAt,
&i.ResourceID,
&i.AuthToken,
&i.AuthInstanceID,
&i.Architecture,
&i.EnvironmentVariables,
&i.OperatingSystem,
&i.InstanceMetadata,
&i.ResourceMetadata,
&i.Directory,
&i.Version,
&i.LastConnectedReplicaID,
&i.ConnectionTimeoutSeconds,
&i.TroubleshootingURL,
&i.MOTDFile,
&i.LifecycleState,
&i.ExpandedDirectory,
&i.LogsLength,
&i.LogsOverflowed,
&i.StartedAt,
&i.ReadyAt,
pq.Array(&i.Subsystems),
pq.Array(&i.DisplayApps),
&i.APIVersion,
&i.DisplayOrder,
&i.ParentID,
&i.APIKeyScope,
&i.Deleted,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceAgentsByWorkspaceAndBuildNumber = `-- name: GetWorkspaceAgentsByWorkspaceAndBuildNumber :many
SELECT
workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order, workspace_agents.parent_id, workspace_agents.api_key_scope, workspace_agents.deleted
FROM
workspace_agents
JOIN
workspace_resources ON workspace_agents.resource_id = workspace_resources.id
JOIN
workspace_builds ON workspace_resources.job_id = workspace_builds.job_id
WHERE
workspace_builds.workspace_id = $1 :: uuid AND
workspace_builds.build_number = $2 :: int
-- Filter out deleted sub agents.
AND workspace_agents.deleted = FALSE
`
type GetWorkspaceAgentsByWorkspaceAndBuildNumberParams struct {
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
BuildNumber int32 `db:"build_number" json:"build_number"`
}
func (q *sqlQuerier) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Context, arg GetWorkspaceAgentsByWorkspaceAndBuildNumberParams) ([]WorkspaceAgent, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsByWorkspaceAndBuildNumber, arg.WorkspaceID, arg.BuildNumber)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAgent
for rows.Next() {
var i WorkspaceAgent
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Name,
&i.FirstConnectedAt,
&i.LastConnectedAt,
&i.DisconnectedAt,
&i.ResourceID,
&i.AuthToken,
&i.AuthInstanceID,
&i.Architecture,
&i.EnvironmentVariables,
&i.OperatingSystem,
&i.InstanceMetadata,
&i.ResourceMetadata,
&i.Directory,
&i.Version,
&i.LastConnectedReplicaID,
&i.ConnectionTimeoutSeconds,
&i.TroubleshootingURL,
&i.MOTDFile,
&i.LifecycleState,
&i.ExpandedDirectory,
&i.LogsLength,
&i.LogsOverflowed,
&i.StartedAt,
&i.ReadyAt,
pq.Array(&i.Subsystems),
pq.Array(&i.DisplayApps),
&i.APIVersion,
&i.DisplayOrder,
&i.ParentID,
&i.APIKeyScope,
&i.Deleted,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceAgentsCreatedAfter = `-- name: GetWorkspaceAgentsCreatedAfter :many
SELECT id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope, deleted FROM workspace_agents
WHERE
created_at > $1
-- Filter out deleted sub agents.
AND deleted = FALSE
`
func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceAgent, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsCreatedAfter, createdAt)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAgent
for rows.Next() {
var i WorkspaceAgent
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Name,
&i.FirstConnectedAt,
&i.LastConnectedAt,
&i.DisconnectedAt,
&i.ResourceID,
&i.AuthToken,
&i.AuthInstanceID,
&i.Architecture,
&i.EnvironmentVariables,
&i.OperatingSystem,
&i.InstanceMetadata,
&i.ResourceMetadata,
&i.Directory,
&i.Version,
&i.LastConnectedReplicaID,
&i.ConnectionTimeoutSeconds,
&i.TroubleshootingURL,
&i.MOTDFile,
&i.LifecycleState,
&i.ExpandedDirectory,
&i.LogsLength,
&i.LogsOverflowed,
&i.StartedAt,
&i.ReadyAt,
pq.Array(&i.Subsystems),
pq.Array(&i.DisplayApps),
&i.APIVersion,
&i.DisplayOrder,
&i.ParentID,
&i.APIKeyScope,
&i.Deleted,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceAgentsForMetrics = `-- name: GetWorkspaceAgentsForMetrics :many
SELECT
w.id as workspace_id,
w.name as workspace_name,
u.username as owner_username,
t.name as template_name,
tv.name as template_version_name,
workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order, workspace_agents.parent_id, workspace_agents.api_key_scope, workspace_agents.deleted
FROM workspaces w
JOIN users u ON w.owner_id = u.id
JOIN templates t ON w.template_id = t.id
JOIN workspace_builds wb ON w.id = wb.workspace_id
LEFT JOIN template_versions tv ON wb.template_version_id = tv.id
JOIN workspace_resources wr ON wb.job_id = wr.job_id
JOIN workspace_agents ON wr.id = workspace_agents.resource_id
WHERE w.deleted = false
AND wb.build_number = (
SELECT MAX(wb2.build_number)
FROM workspace_builds wb2
WHERE wb2.workspace_id = w.id
)
AND workspace_agents.deleted = FALSE
`
type GetWorkspaceAgentsForMetricsRow struct {
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
WorkspaceName string `db:"workspace_name" json:"workspace_name"`
OwnerUsername string `db:"owner_username" json:"owner_username"`
TemplateName string `db:"template_name" json:"template_name"`
TemplateVersionName sql.NullString `db:"template_version_name" json:"template_version_name"`
WorkspaceAgent WorkspaceAgent `db:"workspace_agent" json:"workspace_agent"`
}
func (q *sqlQuerier) GetWorkspaceAgentsForMetrics(ctx context.Context) ([]GetWorkspaceAgentsForMetricsRow, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsForMetrics)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetWorkspaceAgentsForMetricsRow
for rows.Next() {
var i GetWorkspaceAgentsForMetricsRow
if err := rows.Scan(
&i.WorkspaceID,
&i.WorkspaceName,
&i.OwnerUsername,
&i.TemplateName,
&i.TemplateVersionName,
&i.WorkspaceAgent.ID,
&i.WorkspaceAgent.CreatedAt,
&i.WorkspaceAgent.UpdatedAt,
&i.WorkspaceAgent.Name,
&i.WorkspaceAgent.FirstConnectedAt,
&i.WorkspaceAgent.LastConnectedAt,
&i.WorkspaceAgent.DisconnectedAt,
&i.WorkspaceAgent.ResourceID,
&i.WorkspaceAgent.AuthToken,
&i.WorkspaceAgent.AuthInstanceID,
&i.WorkspaceAgent.Architecture,
&i.WorkspaceAgent.EnvironmentVariables,
&i.WorkspaceAgent.OperatingSystem,
&i.WorkspaceAgent.InstanceMetadata,
&i.WorkspaceAgent.ResourceMetadata,
&i.WorkspaceAgent.Directory,
&i.WorkspaceAgent.Version,
&i.WorkspaceAgent.LastConnectedReplicaID,
&i.WorkspaceAgent.ConnectionTimeoutSeconds,
&i.WorkspaceAgent.TroubleshootingURL,
&i.WorkspaceAgent.MOTDFile,
&i.WorkspaceAgent.LifecycleState,
&i.WorkspaceAgent.ExpandedDirectory,
&i.WorkspaceAgent.LogsLength,
&i.WorkspaceAgent.LogsOverflowed,
&i.WorkspaceAgent.StartedAt,
&i.WorkspaceAgent.ReadyAt,
pq.Array(&i.WorkspaceAgent.Subsystems),
pq.Array(&i.WorkspaceAgent.DisplayApps),
&i.WorkspaceAgent.APIVersion,
&i.WorkspaceAgent.DisplayOrder,
&i.WorkspaceAgent.ParentID,
&i.WorkspaceAgent.APIKeyScope,
&i.WorkspaceAgent.Deleted,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceAgentsInLatestBuildByWorkspaceID = `-- name: GetWorkspaceAgentsInLatestBuildByWorkspaceID :many
SELECT
workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order, workspace_agents.parent_id, workspace_agents.api_key_scope, workspace_agents.deleted
FROM
workspace_agents
JOIN
workspace_resources ON workspace_agents.resource_id = workspace_resources.id
JOIN
workspace_builds ON workspace_resources.job_id = workspace_builds.job_id
WHERE
workspace_builds.workspace_id = $1 :: uuid AND
workspace_builds.build_number = (
SELECT
MAX(build_number)
FROM
workspace_builds AS wb
WHERE
wb.workspace_id = $1 :: uuid
)
-- Filter out deleted sub agents.
AND workspace_agents.deleted = FALSE
`
func (q *sqlQuerier) GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgent, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsInLatestBuildByWorkspaceID, workspaceID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAgent
for rows.Next() {
var i WorkspaceAgent
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Name,
&i.FirstConnectedAt,
&i.LastConnectedAt,
&i.DisconnectedAt,
&i.ResourceID,
&i.AuthToken,
&i.AuthInstanceID,
&i.Architecture,
&i.EnvironmentVariables,
&i.OperatingSystem,
&i.InstanceMetadata,
&i.ResourceMetadata,
&i.Directory,
&i.Version,
&i.LastConnectedReplicaID,
&i.ConnectionTimeoutSeconds,
&i.TroubleshootingURL,
&i.MOTDFile,
&i.LifecycleState,
&i.ExpandedDirectory,
&i.LogsLength,
&i.LogsOverflowed,
&i.StartedAt,
&i.ReadyAt,
pq.Array(&i.Subsystems),
pq.Array(&i.DisplayApps),
&i.APIVersion,
&i.DisplayOrder,
&i.ParentID,
&i.APIKeyScope,
&i.Deleted,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceAgentsInLatestBuildByWorkspaceIDs = `-- name: GetWorkspaceAgentsInLatestBuildByWorkspaceIDs :many
SELECT
workspace_builds.workspace_id,
workspace_builds.id AS build_id,
workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order, workspace_agents.parent_id, workspace_agents.api_key_scope, workspace_agents.deleted
FROM
workspace_agents
JOIN
workspace_resources ON workspace_agents.resource_id = workspace_resources.id
JOIN
workspace_builds ON workspace_resources.job_id = workspace_builds.job_id
JOIN (
SELECT
workspace_id,
MAX(build_number) AS build_number
FROM
workspace_builds
WHERE
workspace_id = ANY($1 :: uuid [ ])
GROUP BY
workspace_id
) AS latest_builds ON
latest_builds.workspace_id = workspace_builds.workspace_id AND
latest_builds.build_number = workspace_builds.build_number
WHERE
workspace_agents.deleted = FALSE
`
type GetWorkspaceAgentsInLatestBuildByWorkspaceIDsRow struct {
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
BuildID uuid.UUID `db:"build_id" json:"build_id"`
WorkspaceAgent WorkspaceAgent `db:"workspace_agent" json:"workspace_agent"`
}
func (q *sqlQuerier) GetWorkspaceAgentsInLatestBuildByWorkspaceIDs(ctx context.Context, workspaceIds []uuid.UUID) ([]GetWorkspaceAgentsInLatestBuildByWorkspaceIDsRow, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsInLatestBuildByWorkspaceIDs, pq.Array(workspaceIds))
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetWorkspaceAgentsInLatestBuildByWorkspaceIDsRow
for rows.Next() {
var i GetWorkspaceAgentsInLatestBuildByWorkspaceIDsRow
if err := rows.Scan(
&i.WorkspaceID,
&i.BuildID,
&i.WorkspaceAgent.ID,
&i.WorkspaceAgent.CreatedAt,
&i.WorkspaceAgent.UpdatedAt,
&i.WorkspaceAgent.Name,
&i.WorkspaceAgent.FirstConnectedAt,
&i.WorkspaceAgent.LastConnectedAt,
&i.WorkspaceAgent.DisconnectedAt,
&i.WorkspaceAgent.ResourceID,
&i.WorkspaceAgent.AuthToken,
&i.WorkspaceAgent.AuthInstanceID,
&i.WorkspaceAgent.Architecture,
&i.WorkspaceAgent.EnvironmentVariables,
&i.WorkspaceAgent.OperatingSystem,
&i.WorkspaceAgent.InstanceMetadata,
&i.WorkspaceAgent.ResourceMetadata,
&i.WorkspaceAgent.Directory,
&i.WorkspaceAgent.Version,
&i.WorkspaceAgent.LastConnectedReplicaID,
&i.WorkspaceAgent.ConnectionTimeoutSeconds,
&i.WorkspaceAgent.TroubleshootingURL,
&i.WorkspaceAgent.MOTDFile,
&i.WorkspaceAgent.LifecycleState,
&i.WorkspaceAgent.ExpandedDirectory,
&i.WorkspaceAgent.LogsLength,
&i.WorkspaceAgent.LogsOverflowed,
&i.WorkspaceAgent.StartedAt,
&i.WorkspaceAgent.ReadyAt,
pq.Array(&i.WorkspaceAgent.Subsystems),
pq.Array(&i.WorkspaceAgent.DisplayApps),
&i.WorkspaceAgent.APIVersion,
&i.WorkspaceAgent.DisplayOrder,
&i.WorkspaceAgent.ParentID,
&i.WorkspaceAgent.APIKeyScope,
&i.WorkspaceAgent.Deleted,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceBuildAgentsByInstanceID = `-- name: GetWorkspaceBuildAgentsByInstanceID :many
SELECT
workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order, workspace_agents.parent_id, workspace_agents.api_key_scope, workspace_agents.deleted,
workspace_builds.id AS workspace_build_id,
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at, workspaces.group_acl, workspaces.user_acl
FROM
workspace_agents
JOIN
workspace_resources
ON
workspace_resources.id = workspace_agents.resource_id
JOIN
workspace_builds
ON
workspace_builds.job_id = workspace_resources.job_id
JOIN
provisioner_jobs
ON
provisioner_jobs.id = workspace_builds.job_id
JOIN
workspaces
ON
workspaces.id = workspace_builds.workspace_id
WHERE
workspace_agents.auth_instance_id = $1 :: TEXT
AND workspace_agents.deleted = FALSE
AND workspace_agents.parent_id IS NULL
AND provisioner_jobs.type = 'workspace_build'::provisioner_job_type
AND workspaces.deleted = FALSE
ORDER BY
workspace_agents.created_at DESC
`
type GetWorkspaceBuildAgentsByInstanceIDRow struct {
WorkspaceAgent WorkspaceAgent `db:"workspace_agent" json:"workspace_agent"`
WorkspaceBuildID uuid.UUID `db:"workspace_build_id" json:"workspace_build_id"`
WorkspaceTable WorkspaceTable `db:"workspace_table" json:"workspace_table"`
}
func (q *sqlQuerier) GetWorkspaceBuildAgentsByInstanceID(ctx context.Context, authInstanceID string) ([]GetWorkspaceBuildAgentsByInstanceIDRow, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceBuildAgentsByInstanceID, authInstanceID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetWorkspaceBuildAgentsByInstanceIDRow
for rows.Next() {
var i GetWorkspaceBuildAgentsByInstanceIDRow
if err := rows.Scan(
&i.WorkspaceAgent.ID,
&i.WorkspaceAgent.CreatedAt,
&i.WorkspaceAgent.UpdatedAt,
&i.WorkspaceAgent.Name,
&i.WorkspaceAgent.FirstConnectedAt,
&i.WorkspaceAgent.LastConnectedAt,
&i.WorkspaceAgent.DisconnectedAt,
&i.WorkspaceAgent.ResourceID,
&i.WorkspaceAgent.AuthToken,
&i.WorkspaceAgent.AuthInstanceID,
&i.WorkspaceAgent.Architecture,
&i.WorkspaceAgent.EnvironmentVariables,
&i.WorkspaceAgent.OperatingSystem,
&i.WorkspaceAgent.InstanceMetadata,
&i.WorkspaceAgent.ResourceMetadata,
&i.WorkspaceAgent.Directory,
&i.WorkspaceAgent.Version,
&i.WorkspaceAgent.LastConnectedReplicaID,
&i.WorkspaceAgent.ConnectionTimeoutSeconds,
&i.WorkspaceAgent.TroubleshootingURL,
&i.WorkspaceAgent.MOTDFile,
&i.WorkspaceAgent.LifecycleState,
&i.WorkspaceAgent.ExpandedDirectory,
&i.WorkspaceAgent.LogsLength,
&i.WorkspaceAgent.LogsOverflowed,
&i.WorkspaceAgent.StartedAt,
&i.WorkspaceAgent.ReadyAt,
pq.Array(&i.WorkspaceAgent.Subsystems),
pq.Array(&i.WorkspaceAgent.DisplayApps),
&i.WorkspaceAgent.APIVersion,
&i.WorkspaceAgent.DisplayOrder,
&i.WorkspaceAgent.ParentID,
&i.WorkspaceAgent.APIKeyScope,
&i.WorkspaceAgent.Deleted,
&i.WorkspaceBuildID,
&i.WorkspaceTable.ID,
&i.WorkspaceTable.CreatedAt,
&i.WorkspaceTable.UpdatedAt,
&i.WorkspaceTable.OwnerID,
&i.WorkspaceTable.OrganizationID,
&i.WorkspaceTable.TemplateID,
&i.WorkspaceTable.Deleted,
&i.WorkspaceTable.Name,
&i.WorkspaceTable.AutostartSchedule,
&i.WorkspaceTable.Ttl,
&i.WorkspaceTable.LastUsedAt,
&i.WorkspaceTable.DormantAt,
&i.WorkspaceTable.DeletingAt,
&i.WorkspaceTable.AutomaticUpdates,
&i.WorkspaceTable.Favorite,
&i.WorkspaceTable.NextStartAt,
&i.WorkspaceTable.GroupACL,
&i.WorkspaceTable.UserACL,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertWorkspaceAgent = `-- name: InsertWorkspaceAgent :one
INSERT INTO
workspace_agents (
id,
parent_id,
created_at,
updated_at,
name,
resource_id,
auth_token,
auth_instance_id,
architecture,
environment_variables,
operating_system,
directory,
instance_metadata,
resource_metadata,
connection_timeout_seconds,
troubleshooting_url,
motd_file,
display_apps,
display_order,
api_key_scope
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20) RETURNING id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope, deleted
`
type InsertWorkspaceAgentParams struct {
ID uuid.UUID `db:"id" json:"id"`
ParentID uuid.NullUUID `db:"parent_id" json:"parent_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Name string `db:"name" json:"name"`
ResourceID uuid.UUID `db:"resource_id" json:"resource_id"`
AuthToken uuid.UUID `db:"auth_token" json:"auth_token"`
AuthInstanceID sql.NullString `db:"auth_instance_id" json:"auth_instance_id"`
Architecture string `db:"architecture" json:"architecture"`
EnvironmentVariables pqtype.NullRawMessage `db:"environment_variables" json:"environment_variables"`
OperatingSystem string `db:"operating_system" json:"operating_system"`
Directory string `db:"directory" json:"directory"`
InstanceMetadata pqtype.NullRawMessage `db:"instance_metadata" json:"instance_metadata"`
ResourceMetadata pqtype.NullRawMessage `db:"resource_metadata" json:"resource_metadata"`
ConnectionTimeoutSeconds int32 `db:"connection_timeout_seconds" json:"connection_timeout_seconds"`
TroubleshootingURL string `db:"troubleshooting_url" json:"troubleshooting_url"`
MOTDFile string `db:"motd_file" json:"motd_file"`
DisplayApps []DisplayApp `db:"display_apps" json:"display_apps"`
DisplayOrder int32 `db:"display_order" json:"display_order"`
APIKeyScope AgentKeyScopeEnum `db:"api_key_scope" json:"api_key_scope"`
}
func (q *sqlQuerier) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspaceAgentParams) (WorkspaceAgent, error) {
row := q.db.QueryRowContext(ctx, insertWorkspaceAgent,
arg.ID,
arg.ParentID,
arg.CreatedAt,
arg.UpdatedAt,
arg.Name,
arg.ResourceID,
arg.AuthToken,
arg.AuthInstanceID,
arg.Architecture,
arg.EnvironmentVariables,
arg.OperatingSystem,
arg.Directory,
arg.InstanceMetadata,
arg.ResourceMetadata,
arg.ConnectionTimeoutSeconds,
arg.TroubleshootingURL,
arg.MOTDFile,
pq.Array(arg.DisplayApps),
arg.DisplayOrder,
arg.APIKeyScope,
)
var i WorkspaceAgent
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Name,
&i.FirstConnectedAt,
&i.LastConnectedAt,
&i.DisconnectedAt,
&i.ResourceID,
&i.AuthToken,
&i.AuthInstanceID,
&i.Architecture,
&i.EnvironmentVariables,
&i.OperatingSystem,
&i.InstanceMetadata,
&i.ResourceMetadata,
&i.Directory,
&i.Version,
&i.LastConnectedReplicaID,
&i.ConnectionTimeoutSeconds,
&i.TroubleshootingURL,
&i.MOTDFile,
&i.LifecycleState,
&i.ExpandedDirectory,
&i.LogsLength,
&i.LogsOverflowed,
&i.StartedAt,
&i.ReadyAt,
pq.Array(&i.Subsystems),
pq.Array(&i.DisplayApps),
&i.APIVersion,
&i.DisplayOrder,
&i.ParentID,
&i.APIKeyScope,
&i.Deleted,
)
return i, err
}
const insertWorkspaceAgentLogSources = `-- name: InsertWorkspaceAgentLogSources :many
INSERT INTO
workspace_agent_log_sources (workspace_agent_id, created_at, id, display_name, icon)
SELECT
$1 :: uuid AS workspace_agent_id,
$2 :: timestamptz AS created_at,
unnest($3 :: uuid [ ]) AS id,
unnest($4 :: VARCHAR(127) [ ]) AS display_name,
unnest($5 :: text [ ]) AS icon
RETURNING workspace_agent_log_sources.workspace_agent_id, workspace_agent_log_sources.id, workspace_agent_log_sources.created_at, workspace_agent_log_sources.display_name, workspace_agent_log_sources.icon
`
type InsertWorkspaceAgentLogSourcesParams struct {
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
ID []uuid.UUID `db:"id" json:"id"`
DisplayName []string `db:"display_name" json:"display_name"`
Icon []string `db:"icon" json:"icon"`
}
func (q *sqlQuerier) InsertWorkspaceAgentLogSources(ctx context.Context, arg InsertWorkspaceAgentLogSourcesParams) ([]WorkspaceAgentLogSource, error) {
rows, err := q.db.QueryContext(ctx, insertWorkspaceAgentLogSources,
arg.WorkspaceAgentID,
arg.CreatedAt,
pq.Array(arg.ID),
pq.Array(arg.DisplayName),
pq.Array(arg.Icon),
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAgentLogSource
for rows.Next() {
var i WorkspaceAgentLogSource
if err := rows.Scan(
&i.WorkspaceAgentID,
&i.ID,
&i.CreatedAt,
&i.DisplayName,
&i.Icon,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertWorkspaceAgentLogs = `-- name: InsertWorkspaceAgentLogs :many
WITH new_length AS (
UPDATE workspace_agents SET
logs_length = logs_length + $6 WHERE workspace_agents.id = $1
)
INSERT INTO
workspace_agent_logs (agent_id, created_at, output, level, log_source_id)
SELECT
$1 :: uuid AS agent_id,
$2 :: timestamptz AS created_at,
unnest($3 :: VARCHAR(1024) [ ]) AS output,
unnest($4 :: log_level [ ]) AS level,
$5 :: uuid AS log_source_id
RETURNING workspace_agent_logs.agent_id, workspace_agent_logs.created_at, workspace_agent_logs.output, workspace_agent_logs.id, workspace_agent_logs.level, workspace_agent_logs.log_source_id
`
type InsertWorkspaceAgentLogsParams struct {
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
Output []string `db:"output" json:"output"`
Level []LogLevel `db:"level" json:"level"`
LogSourceID uuid.UUID `db:"log_source_id" json:"log_source_id"`
OutputLength int32 `db:"output_length" json:"output_length"`
}
func (q *sqlQuerier) InsertWorkspaceAgentLogs(ctx context.Context, arg InsertWorkspaceAgentLogsParams) ([]WorkspaceAgentLog, error) {
rows, err := q.db.QueryContext(ctx, insertWorkspaceAgentLogs,
arg.AgentID,
arg.CreatedAt,
pq.Array(arg.Output),
pq.Array(arg.Level),
arg.LogSourceID,
arg.OutputLength,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAgentLog
for rows.Next() {
var i WorkspaceAgentLog
if err := rows.Scan(
&i.AgentID,
&i.CreatedAt,
&i.Output,
&i.ID,
&i.Level,
&i.LogSourceID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertWorkspaceAgentMetadata = `-- name: InsertWorkspaceAgentMetadata :exec
INSERT INTO
workspace_agent_metadata (
workspace_agent_id,
display_name,
key,
script,
timeout,
interval,
display_order
)
VALUES
($1, $2, $3, $4, $5, $6, $7)
`
type InsertWorkspaceAgentMetadataParams struct {
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
DisplayName string `db:"display_name" json:"display_name"`
Key string `db:"key" json:"key"`
Script string `db:"script" json:"script"`
Timeout int64 `db:"timeout" json:"timeout"`
Interval int64 `db:"interval" json:"interval"`
DisplayOrder int32 `db:"display_order" json:"display_order"`
}
func (q *sqlQuerier) InsertWorkspaceAgentMetadata(ctx context.Context, arg InsertWorkspaceAgentMetadataParams) error {
_, err := q.db.ExecContext(ctx, insertWorkspaceAgentMetadata,
arg.WorkspaceAgentID,
arg.DisplayName,
arg.Key,
arg.Script,
arg.Timeout,
arg.Interval,
arg.DisplayOrder,
)
return err
}
const insertWorkspaceAgentScriptTimings = `-- name: InsertWorkspaceAgentScriptTimings :one
INSERT INTO
workspace_agent_script_timings (
script_id,
started_at,
ended_at,
exit_code,
stage,
status
)
VALUES
($1, $2, $3, $4, $5, $6)
RETURNING workspace_agent_script_timings.script_id, workspace_agent_script_timings.started_at, workspace_agent_script_timings.ended_at, workspace_agent_script_timings.exit_code, workspace_agent_script_timings.stage, workspace_agent_script_timings.status
`
type InsertWorkspaceAgentScriptTimingsParams struct {
ScriptID uuid.UUID `db:"script_id" json:"script_id"`
StartedAt time.Time `db:"started_at" json:"started_at"`
EndedAt time.Time `db:"ended_at" json:"ended_at"`
ExitCode int32 `db:"exit_code" json:"exit_code"`
Stage WorkspaceAgentScriptTimingStage `db:"stage" json:"stage"`
Status WorkspaceAgentScriptTimingStatus `db:"status" json:"status"`
}
func (q *sqlQuerier) InsertWorkspaceAgentScriptTimings(ctx context.Context, arg InsertWorkspaceAgentScriptTimingsParams) (WorkspaceAgentScriptTiming, error) {
row := q.db.QueryRowContext(ctx, insertWorkspaceAgentScriptTimings,
arg.ScriptID,
arg.StartedAt,
arg.EndedAt,
arg.ExitCode,
arg.Stage,
arg.Status,
)
var i WorkspaceAgentScriptTiming
err := row.Scan(
&i.ScriptID,
&i.StartedAt,
&i.EndedAt,
&i.ExitCode,
&i.Stage,
&i.Status,
)
return i, err
}
const softDeletePriorWorkspaceAgents = `-- name: SoftDeletePriorWorkspaceAgents :exec
WITH soft_deleted_agents AS (
UPDATE workspace_agents
SET deleted = TRUE
WHERE id IN (
SELECT wa.id
FROM workspace_agents wa
JOIN workspace_resources wr ON wr.id = wa.resource_id
JOIN workspace_builds wb ON wb.job_id = wr.job_id
WHERE wb.workspace_id = $1
AND wb.id <> $2
AND wa.deleted = FALSE
)
RETURNING id
), purged_context_resources AS (
DELETE FROM workspace_agent_context_resources
WHERE workspace_agent_id IN (SELECT id FROM soft_deleted_agents)
)
DELETE FROM workspace_agent_context_snapshots
WHERE workspace_agent_id IN (SELECT id FROM soft_deleted_agents)
`
type SoftDeletePriorWorkspaceAgentsParams struct {
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
CurrentBuildID uuid.UUID `db:"current_build_id" json:"current_build_id"`
}
// Marks agents from all prior builds of this workspace as deleted,
// preserving only agents belonging to @current_build_id. Called from
// provisionerdserver when a workspace build completes, after the new
// build's agents have been inserted, so running agents are not
// deleted while a build is still queued or provisioning.
//
// Agent context rows (workspace_agent_context_snapshots and
// workspace_agent_context_resources) only describe live agents, and
// agents are never un-deleted, so they are hard-deleted here instead
// of accumulating alongside the soft-deleted agent rows.
func (q *sqlQuerier) SoftDeletePriorWorkspaceAgents(ctx context.Context, arg SoftDeletePriorWorkspaceAgentsParams) error {
_, err := q.db.ExecContext(ctx, softDeletePriorWorkspaceAgents, arg.WorkspaceID, arg.CurrentBuildID)
return err
}
const softDeleteWorkspaceAgentsByWorkspaceID = `-- name: SoftDeleteWorkspaceAgentsByWorkspaceID :exec
WITH soft_deleted_agents AS (
UPDATE workspace_agents
SET deleted = TRUE
WHERE id IN (
SELECT wa.id
FROM workspace_agents wa
JOIN workspace_resources wr ON wr.id = wa.resource_id
JOIN workspace_builds wb ON wb.job_id = wr.job_id
WHERE wb.workspace_id = $1
AND wa.deleted = FALSE
)
RETURNING id
), purged_context_resources AS (
DELETE FROM workspace_agent_context_resources
WHERE workspace_agent_id IN (SELECT id FROM soft_deleted_agents)
)
DELETE FROM workspace_agent_context_snapshots
WHERE workspace_agent_id IN (SELECT id FROM soft_deleted_agents)
`
// Marks every non-deleted agent belonging to the given workspace as
// deleted. Called alongside UpdateWorkspaceDeletedByID when a workspace
// itself is soft-deleted, so the agent instance-identity auth path
// (which filters on workspace_agents.deleted) doesn't keep seeing
// orphaned rows.
//
// Agent context rows are hard-deleted for the same reason as in
// SoftDeletePriorWorkspaceAgents.
func (q *sqlQuerier) SoftDeleteWorkspaceAgentsByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) error {
_, err := q.db.ExecContext(ctx, softDeleteWorkspaceAgentsByWorkspaceID, workspaceID)
return err
}
const updateWorkspaceAgentConnectionByID = `-- name: UpdateWorkspaceAgentConnectionByID :exec
UPDATE
workspace_agents
SET
first_connected_at = $2,
last_connected_at = $3,
last_connected_replica_id = $4,
disconnected_at = $5,
updated_at = $6
WHERE
id = $1
`
type UpdateWorkspaceAgentConnectionByIDParams struct {
ID uuid.UUID `db:"id" json:"id"`
FirstConnectedAt sql.NullTime `db:"first_connected_at" json:"first_connected_at"`
LastConnectedAt sql.NullTime `db:"last_connected_at" json:"last_connected_at"`
LastConnectedReplicaID uuid.NullUUID `db:"last_connected_replica_id" json:"last_connected_replica_id"`
DisconnectedAt sql.NullTime `db:"disconnected_at" json:"disconnected_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
func (q *sqlQuerier) UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg UpdateWorkspaceAgentConnectionByIDParams) error {
_, err := q.db.ExecContext(ctx, updateWorkspaceAgentConnectionByID,
arg.ID,
arg.FirstConnectedAt,
arg.LastConnectedAt,
arg.LastConnectedReplicaID,
arg.DisconnectedAt,
arg.UpdatedAt,
)
return err
}
const updateWorkspaceAgentDirectoryByID = `-- name: UpdateWorkspaceAgentDirectoryByID :exec
UPDATE
workspace_agents
SET
directory = $2, updated_at = $3
WHERE
id = $1
`
type UpdateWorkspaceAgentDirectoryByIDParams struct {
ID uuid.UUID `db:"id" json:"id"`
Directory string `db:"directory" json:"directory"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
func (q *sqlQuerier) UpdateWorkspaceAgentDirectoryByID(ctx context.Context, arg UpdateWorkspaceAgentDirectoryByIDParams) error {
_, err := q.db.ExecContext(ctx, updateWorkspaceAgentDirectoryByID, arg.ID, arg.Directory, arg.UpdatedAt)
return err
}
const updateWorkspaceAgentDisplayAppsByID = `-- name: UpdateWorkspaceAgentDisplayAppsByID :exec
UPDATE
workspace_agents
SET
display_apps = $2, updated_at = $3
WHERE
id = $1
`
type UpdateWorkspaceAgentDisplayAppsByIDParams struct {
ID uuid.UUID `db:"id" json:"id"`
DisplayApps []DisplayApp `db:"display_apps" json:"display_apps"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
func (q *sqlQuerier) UpdateWorkspaceAgentDisplayAppsByID(ctx context.Context, arg UpdateWorkspaceAgentDisplayAppsByIDParams) error {
_, err := q.db.ExecContext(ctx, updateWorkspaceAgentDisplayAppsByID, arg.ID, pq.Array(arg.DisplayApps), arg.UpdatedAt)
return err
}
const updateWorkspaceAgentLifecycleStateByID = `-- name: UpdateWorkspaceAgentLifecycleStateByID :exec
UPDATE
workspace_agents
SET
lifecycle_state = $2,
started_at = $3,
ready_at = $4
WHERE
id = $1
`
type UpdateWorkspaceAgentLifecycleStateByIDParams struct {
ID uuid.UUID `db:"id" json:"id"`
LifecycleState WorkspaceAgentLifecycleState `db:"lifecycle_state" json:"lifecycle_state"`
StartedAt sql.NullTime `db:"started_at" json:"started_at"`
ReadyAt sql.NullTime `db:"ready_at" json:"ready_at"`
}
func (q *sqlQuerier) UpdateWorkspaceAgentLifecycleStateByID(ctx context.Context, arg UpdateWorkspaceAgentLifecycleStateByIDParams) error {
_, err := q.db.ExecContext(ctx, updateWorkspaceAgentLifecycleStateByID,
arg.ID,
arg.LifecycleState,
arg.StartedAt,
arg.ReadyAt,
)
return err
}
const updateWorkspaceAgentLogOverflowByID = `-- name: UpdateWorkspaceAgentLogOverflowByID :exec
UPDATE
workspace_agents
SET
logs_overflowed = $2
WHERE
id = $1
`
type UpdateWorkspaceAgentLogOverflowByIDParams struct {
ID uuid.UUID `db:"id" json:"id"`
LogsOverflowed bool `db:"logs_overflowed" json:"logs_overflowed"`
}
func (q *sqlQuerier) UpdateWorkspaceAgentLogOverflowByID(ctx context.Context, arg UpdateWorkspaceAgentLogOverflowByIDParams) error {
_, err := q.db.ExecContext(ctx, updateWorkspaceAgentLogOverflowByID, arg.ID, arg.LogsOverflowed)
return err
}
const updateWorkspaceAgentMetadata = `-- name: UpdateWorkspaceAgentMetadata :exec
WITH metadata AS (
SELECT
unnest($2::text[]) AS key,
unnest($3::text[]) AS value,
unnest($4::text[]) AS error,
unnest($5::timestamptz[]) AS collected_at
)
UPDATE
workspace_agent_metadata wam
SET
value = m.value,
error = m.error,
collected_at = m.collected_at
FROM
metadata m
WHERE
wam.workspace_agent_id = $1
AND wam.key = m.key
`
type UpdateWorkspaceAgentMetadataParams struct {
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
Key []string `db:"key" json:"key"`
Value []string `db:"value" json:"value"`
Error []string `db:"error" json:"error"`
CollectedAt []time.Time `db:"collected_at" json:"collected_at"`
}
func (q *sqlQuerier) UpdateWorkspaceAgentMetadata(ctx context.Context, arg UpdateWorkspaceAgentMetadataParams) error {
_, err := q.db.ExecContext(ctx, updateWorkspaceAgentMetadata,
arg.WorkspaceAgentID,
pq.Array(arg.Key),
pq.Array(arg.Value),
pq.Array(arg.Error),
pq.Array(arg.CollectedAt),
)
return err
}
const updateWorkspaceAgentStartupByID = `-- name: UpdateWorkspaceAgentStartupByID :exec
UPDATE
workspace_agents
SET
version = $2,
expanded_directory = $3,
subsystems = $4,
api_version = $5
WHERE
id = $1
`
type UpdateWorkspaceAgentStartupByIDParams struct {
ID uuid.UUID `db:"id" json:"id"`
Version string `db:"version" json:"version"`
ExpandedDirectory string `db:"expanded_directory" json:"expanded_directory"`
Subsystems []WorkspaceAgentSubsystem `db:"subsystems" json:"subsystems"`
APIVersion string `db:"api_version" json:"api_version"`
}
func (q *sqlQuerier) UpdateWorkspaceAgentStartupByID(ctx context.Context, arg UpdateWorkspaceAgentStartupByIDParams) error {
_, err := q.db.ExecContext(ctx, updateWorkspaceAgentStartupByID,
arg.ID,
arg.Version,
arg.ExpandedDirectory,
pq.Array(arg.Subsystems),
arg.APIVersion,
)
return err
}
const deleteOldWorkspaceAgentStats = `-- name: DeleteOldWorkspaceAgentStats :exec
DELETE FROM
workspace_agent_stats
WHERE
created_at < (
SELECT
COALESCE(
-- When generating initial template usage stats, all the
-- raw agent stats are needed, after that only ~30 mins
-- from last rollup is needed. Deployment stats seem to
-- use between 15 mins and 1 hour of data. We keep a
-- little bit more (1 day) just in case.
MAX(start_time) - '1 days'::interval,
-- Fall back to ~6 months ago if there are no template
-- usage stats so that we don't delete the data before
-- it's rolled up.
NOW() - '180 days'::interval
)
FROM
template_usage_stats
)
AND created_at < (
-- Delete at most in batches of 4 hours (with this batch size, assuming
-- 1 iteration / 10 minutes, we can clear out the previous 6 months of
-- data in 7.5 days) whilst keeping the DB load low.
SELECT
COALESCE(MIN(created_at) + '4 hours'::interval, NOW())
FROM
workspace_agent_stats
)
`
func (q *sqlQuerier) DeleteOldWorkspaceAgentStats(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, deleteOldWorkspaceAgentStats)
return err
}
const getDeploymentWorkspaceAgentStats = `-- name: GetDeploymentWorkspaceAgentStats :one
WITH stats AS (
SELECT
agent_id,
created_at,
rx_bytes,
tx_bytes,
connection_median_latency_ms,
session_count_vscode,
session_count_ssh,
session_count_jetbrains,
session_count_reconnecting_pty,
ROW_NUMBER() OVER (PARTITION BY agent_id ORDER BY created_at DESC) AS rn
FROM workspace_agent_stats
WHERE created_at > $1
)
SELECT
coalesce(SUM(rx_bytes), 0)::bigint AS workspace_rx_bytes,
coalesce(SUM(tx_bytes), 0)::bigint AS workspace_tx_bytes,
-- The greater than 0 is to support legacy agents that don't report connection_median_latency_ms.
coalesce((PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY connection_median_latency_ms) FILTER (WHERE connection_median_latency_ms > 0)), -1)::FLOAT AS workspace_connection_latency_50,
coalesce((PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY connection_median_latency_ms) FILTER (WHERE connection_median_latency_ms > 0)), -1)::FLOAT AS workspace_connection_latency_95,
coalesce(SUM(session_count_vscode) FILTER (WHERE rn = 1), 0)::bigint AS session_count_vscode,
coalesce(SUM(session_count_ssh) FILTER (WHERE rn = 1), 0)::bigint AS session_count_ssh,
coalesce(SUM(session_count_jetbrains) FILTER (WHERE rn = 1), 0)::bigint AS session_count_jetbrains,
coalesce(SUM(session_count_reconnecting_pty) FILTER (WHERE rn = 1), 0)::bigint AS session_count_reconnecting_pty
FROM stats
`
type GetDeploymentWorkspaceAgentStatsRow struct {
WorkspaceRxBytes int64 `db:"workspace_rx_bytes" json:"workspace_rx_bytes"`
WorkspaceTxBytes int64 `db:"workspace_tx_bytes" json:"workspace_tx_bytes"`
WorkspaceConnectionLatency50 float64 `db:"workspace_connection_latency_50" json:"workspace_connection_latency_50"`
WorkspaceConnectionLatency95 float64 `db:"workspace_connection_latency_95" json:"workspace_connection_latency_95"`
SessionCountVSCode int64 `db:"session_count_vscode" json:"session_count_vscode"`
SessionCountSSH int64 `db:"session_count_ssh" json:"session_count_ssh"`
SessionCountJetBrains int64 `db:"session_count_jetbrains" json:"session_count_jetbrains"`
SessionCountReconnectingPTY int64 `db:"session_count_reconnecting_pty" json:"session_count_reconnecting_pty"`
}
func (q *sqlQuerier) GetDeploymentWorkspaceAgentStats(ctx context.Context, createdAt time.Time) (GetDeploymentWorkspaceAgentStatsRow, error) {
row := q.db.QueryRowContext(ctx, getDeploymentWorkspaceAgentStats, createdAt)
var i GetDeploymentWorkspaceAgentStatsRow
err := row.Scan(
&i.WorkspaceRxBytes,
&i.WorkspaceTxBytes,
&i.WorkspaceConnectionLatency50,
&i.WorkspaceConnectionLatency95,
&i.SessionCountVSCode,
&i.SessionCountSSH,
&i.SessionCountJetBrains,
&i.SessionCountReconnectingPTY,
)
return i, err
}
const getDeploymentWorkspaceAgentUsageStats = `-- name: GetDeploymentWorkspaceAgentUsageStats :one
WITH agent_stats AS (
SELECT
coalesce(SUM(rx_bytes), 0)::bigint AS workspace_rx_bytes,
coalesce(SUM(tx_bytes), 0)::bigint AS workspace_tx_bytes,
coalesce((PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_50,
coalesce((PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_95
FROM workspace_agent_stats
-- The greater than 0 is to support legacy agents that don't report connection_median_latency_ms.
WHERE workspace_agent_stats.created_at > $1 AND connection_median_latency_ms > 0
),
minute_buckets AS (
SELECT
agent_id,
date_trunc('minute', created_at) AS minute_bucket,
coalesce(SUM(session_count_vscode), 0)::bigint AS session_count_vscode,
coalesce(SUM(session_count_ssh), 0)::bigint AS session_count_ssh,
coalesce(SUM(session_count_jetbrains), 0)::bigint AS session_count_jetbrains,
coalesce(SUM(session_count_reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty
FROM
workspace_agent_stats
WHERE
created_at >= $1
AND created_at < date_trunc('minute', now()) -- Exclude current partial minute
AND usage = true
GROUP BY
agent_id,
minute_bucket
),
latest_buckets AS (
SELECT DISTINCT ON (agent_id)
agent_id,
minute_bucket,
session_count_vscode,
session_count_jetbrains,
session_count_reconnecting_pty,
session_count_ssh
FROM
minute_buckets
ORDER BY
agent_id,
minute_bucket DESC
),
latest_agent_stats AS (
SELECT
coalesce(SUM(session_count_vscode), 0)::bigint AS session_count_vscode,
coalesce(SUM(session_count_ssh), 0)::bigint AS session_count_ssh,
coalesce(SUM(session_count_jetbrains), 0)::bigint AS session_count_jetbrains,
coalesce(SUM(session_count_reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty
FROM
latest_buckets
)
SELECT workspace_rx_bytes, workspace_tx_bytes, workspace_connection_latency_50, workspace_connection_latency_95, session_count_vscode, session_count_ssh, session_count_jetbrains, session_count_reconnecting_pty FROM agent_stats, latest_agent_stats
`
type GetDeploymentWorkspaceAgentUsageStatsRow struct {
WorkspaceRxBytes int64 `db:"workspace_rx_bytes" json:"workspace_rx_bytes"`
WorkspaceTxBytes int64 `db:"workspace_tx_bytes" json:"workspace_tx_bytes"`
WorkspaceConnectionLatency50 float64 `db:"workspace_connection_latency_50" json:"workspace_connection_latency_50"`
WorkspaceConnectionLatency95 float64 `db:"workspace_connection_latency_95" json:"workspace_connection_latency_95"`
SessionCountVSCode int64 `db:"session_count_vscode" json:"session_count_vscode"`
SessionCountSSH int64 `db:"session_count_ssh" json:"session_count_ssh"`
SessionCountJetBrains int64 `db:"session_count_jetbrains" json:"session_count_jetbrains"`
SessionCountReconnectingPTY int64 `db:"session_count_reconnecting_pty" json:"session_count_reconnecting_pty"`
}
func (q *sqlQuerier) GetDeploymentWorkspaceAgentUsageStats(ctx context.Context, createdAt time.Time) (GetDeploymentWorkspaceAgentUsageStatsRow, error) {
row := q.db.QueryRowContext(ctx, getDeploymentWorkspaceAgentUsageStats, createdAt)
var i GetDeploymentWorkspaceAgentUsageStatsRow
err := row.Scan(
&i.WorkspaceRxBytes,
&i.WorkspaceTxBytes,
&i.WorkspaceConnectionLatency50,
&i.WorkspaceConnectionLatency95,
&i.SessionCountVSCode,
&i.SessionCountSSH,
&i.SessionCountJetBrains,
&i.SessionCountReconnectingPTY,
)
return i, err
}
const getWorkspaceAgentStats = `-- name: GetWorkspaceAgentStats :many
WITH agent_stats AS (
SELECT
user_id,
agent_id,
workspace_id,
template_id,
MIN(created_at)::timestamptz AS aggregated_from,
coalesce(SUM(rx_bytes), 0)::bigint AS workspace_rx_bytes,
coalesce(SUM(tx_bytes), 0)::bigint AS workspace_tx_bytes,
coalesce((PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_50,
coalesce((PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_95
FROM workspace_agent_stats
-- The greater than 0 is to support legacy agents that don't report connection_median_latency_ms.
WHERE workspace_agent_stats.created_at > $1 AND connection_median_latency_ms > 0
GROUP BY user_id, agent_id, workspace_id, template_id
), latest_agent_stats AS (
SELECT
a.agent_id,
coalesce(SUM(session_count_vscode), 0)::bigint AS session_count_vscode,
coalesce(SUM(session_count_ssh), 0)::bigint AS session_count_ssh,
coalesce(SUM(session_count_jetbrains), 0)::bigint AS session_count_jetbrains,
coalesce(SUM(session_count_reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty
FROM (
SELECT id, created_at, user_id, agent_id, workspace_id, template_id, connections_by_proto, connection_count, rx_packets, rx_bytes, tx_packets, tx_bytes, connection_median_latency_ms, session_count_vscode, session_count_jetbrains, session_count_reconnecting_pty, session_count_ssh, usage, ROW_NUMBER() OVER(PARTITION BY agent_id ORDER BY created_at DESC) AS rn
FROM workspace_agent_stats WHERE created_at > $1
) AS a WHERE a.rn = 1 GROUP BY a.user_id, a.agent_id, a.workspace_id, a.template_id
)
SELECT user_id, agent_stats.agent_id, workspace_id, template_id, aggregated_from, workspace_rx_bytes, workspace_tx_bytes, workspace_connection_latency_50, workspace_connection_latency_95, latest_agent_stats.agent_id, session_count_vscode, session_count_ssh, session_count_jetbrains, session_count_reconnecting_pty FROM agent_stats JOIN latest_agent_stats ON agent_stats.agent_id = latest_agent_stats.agent_id
`
type GetWorkspaceAgentStatsRow struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
AggregatedFrom time.Time `db:"aggregated_from" json:"aggregated_from"`
WorkspaceRxBytes int64 `db:"workspace_rx_bytes" json:"workspace_rx_bytes"`
WorkspaceTxBytes int64 `db:"workspace_tx_bytes" json:"workspace_tx_bytes"`
WorkspaceConnectionLatency50 float64 `db:"workspace_connection_latency_50" json:"workspace_connection_latency_50"`
WorkspaceConnectionLatency95 float64 `db:"workspace_connection_latency_95" json:"workspace_connection_latency_95"`
AgentID_2 uuid.UUID `db:"agent_id_2" json:"agent_id_2"`
SessionCountVSCode int64 `db:"session_count_vscode" json:"session_count_vscode"`
SessionCountSSH int64 `db:"session_count_ssh" json:"session_count_ssh"`
SessionCountJetBrains int64 `db:"session_count_jetbrains" json:"session_count_jetbrains"`
SessionCountReconnectingPTY int64 `db:"session_count_reconnecting_pty" json:"session_count_reconnecting_pty"`
}
func (q *sqlQuerier) GetWorkspaceAgentStats(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentStatsRow, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentStats, createdAt)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetWorkspaceAgentStatsRow
for rows.Next() {
var i GetWorkspaceAgentStatsRow
if err := rows.Scan(
&i.UserID,
&i.AgentID,
&i.WorkspaceID,
&i.TemplateID,
&i.AggregatedFrom,
&i.WorkspaceRxBytes,
&i.WorkspaceTxBytes,
&i.WorkspaceConnectionLatency50,
&i.WorkspaceConnectionLatency95,
&i.AgentID_2,
&i.SessionCountVSCode,
&i.SessionCountSSH,
&i.SessionCountJetBrains,
&i.SessionCountReconnectingPTY,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceAgentStatsAndLabels = `-- name: GetWorkspaceAgentStatsAndLabels :many
WITH agent_stats AS (
SELECT
user_id,
agent_id,
workspace_id,
coalesce(SUM(rx_bytes), 0)::bigint AS rx_bytes,
coalesce(SUM(tx_bytes), 0)::bigint AS tx_bytes
FROM workspace_agent_stats
WHERE workspace_agent_stats.created_at > $1
GROUP BY user_id, agent_id, workspace_id
), latest_agent_stats AS (
SELECT
a.agent_id,
coalesce(SUM(session_count_vscode), 0)::bigint AS session_count_vscode,
coalesce(SUM(session_count_ssh), 0)::bigint AS session_count_ssh,
coalesce(SUM(session_count_jetbrains), 0)::bigint AS session_count_jetbrains,
coalesce(SUM(session_count_reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty,
coalesce(SUM(connection_count), 0)::bigint AS connection_count,
coalesce(MAX(connection_median_latency_ms), 0)::float AS connection_median_latency_ms
FROM (
SELECT id, created_at, user_id, agent_id, workspace_id, template_id, connections_by_proto, connection_count, rx_packets, rx_bytes, tx_packets, tx_bytes, connection_median_latency_ms, session_count_vscode, session_count_jetbrains, session_count_reconnecting_pty, session_count_ssh, usage, ROW_NUMBER() OVER(PARTITION BY agent_id ORDER BY created_at DESC) AS rn
FROM workspace_agent_stats
-- The greater than 0 is to support legacy agents that don't report connection_median_latency_ms.
WHERE created_at > $1 AND connection_median_latency_ms > 0
) AS a
WHERE a.rn = 1
GROUP BY a.user_id, a.agent_id, a.workspace_id
)
SELECT
users.username, workspace_agents.name AS agent_name, workspaces.name AS workspace_name, rx_bytes, tx_bytes,
session_count_vscode, session_count_ssh, session_count_jetbrains, session_count_reconnecting_pty,
connection_count, connection_median_latency_ms
FROM
agent_stats
JOIN
latest_agent_stats
ON
agent_stats.agent_id = latest_agent_stats.agent_id
JOIN
users
ON
users.id = agent_stats.user_id
JOIN
workspace_agents
ON
workspace_agents.id = agent_stats.agent_id
JOIN
workspaces
ON
workspaces.id = agent_stats.workspace_id
`
type GetWorkspaceAgentStatsAndLabelsRow struct {
Username string `db:"username" json:"username"`
AgentName string `db:"agent_name" json:"agent_name"`
WorkspaceName string `db:"workspace_name" json:"workspace_name"`
RxBytes int64 `db:"rx_bytes" json:"rx_bytes"`
TxBytes int64 `db:"tx_bytes" json:"tx_bytes"`
SessionCountVSCode int64 `db:"session_count_vscode" json:"session_count_vscode"`
SessionCountSSH int64 `db:"session_count_ssh" json:"session_count_ssh"`
SessionCountJetBrains int64 `db:"session_count_jetbrains" json:"session_count_jetbrains"`
SessionCountReconnectingPTY int64 `db:"session_count_reconnecting_pty" json:"session_count_reconnecting_pty"`
ConnectionCount int64 `db:"connection_count" json:"connection_count"`
ConnectionMedianLatencyMS float64 `db:"connection_median_latency_ms" json:"connection_median_latency_ms"`
}
func (q *sqlQuerier) GetWorkspaceAgentStatsAndLabels(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentStatsAndLabelsRow, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentStatsAndLabels, createdAt)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetWorkspaceAgentStatsAndLabelsRow
for rows.Next() {
var i GetWorkspaceAgentStatsAndLabelsRow
if err := rows.Scan(
&i.Username,
&i.AgentName,
&i.WorkspaceName,
&i.RxBytes,
&i.TxBytes,
&i.SessionCountVSCode,
&i.SessionCountSSH,
&i.SessionCountJetBrains,
&i.SessionCountReconnectingPTY,
&i.ConnectionCount,
&i.ConnectionMedianLatencyMS,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceAgentUsageStats = `-- name: GetWorkspaceAgentUsageStats :many
WITH agent_stats AS (
SELECT
user_id,
agent_id,
workspace_id,
template_id,
MIN(created_at)::timestamptz AS aggregated_from,
coalesce(SUM(rx_bytes), 0)::bigint AS workspace_rx_bytes,
coalesce(SUM(tx_bytes), 0)::bigint AS workspace_tx_bytes,
coalesce((PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_50,
coalesce((PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_95
FROM workspace_agent_stats
-- The greater than 0 is to support legacy agents that don't report connection_median_latency_ms.
WHERE workspace_agent_stats.created_at > $1 AND connection_median_latency_ms > 0
GROUP BY user_id, agent_id, workspace_id, template_id
),
minute_buckets AS (
SELECT
agent_id,
date_trunc('minute', created_at) AS minute_bucket,
coalesce(SUM(session_count_vscode), 0)::bigint AS session_count_vscode,
coalesce(SUM(session_count_ssh), 0)::bigint AS session_count_ssh,
coalesce(SUM(session_count_jetbrains), 0)::bigint AS session_count_jetbrains,
coalesce(SUM(session_count_reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty
FROM
workspace_agent_stats
WHERE
created_at >= $1
AND created_at < date_trunc('minute', now()) -- Exclude current partial minute
AND usage = true
GROUP BY
agent_id,
minute_bucket,
user_id,
agent_id,
workspace_id,
template_id
),
latest_buckets AS (
SELECT DISTINCT ON (agent_id)
agent_id,
session_count_vscode,
session_count_ssh,
session_count_jetbrains,
session_count_reconnecting_pty
FROM
minute_buckets
ORDER BY
agent_id,
minute_bucket DESC
)
SELECT user_id,
agent_stats.agent_id,
workspace_id,
template_id,
aggregated_from,
workspace_rx_bytes,
workspace_tx_bytes,
workspace_connection_latency_50,
workspace_connection_latency_95,
coalesce(latest_buckets.agent_id,agent_stats.agent_id) AS agent_id,
coalesce(session_count_vscode, 0)::bigint AS session_count_vscode,
coalesce(session_count_ssh, 0)::bigint AS session_count_ssh,
coalesce(session_count_jetbrains, 0)::bigint AS session_count_jetbrains,
coalesce(session_count_reconnecting_pty, 0)::bigint AS session_count_reconnecting_pty
FROM agent_stats LEFT JOIN latest_buckets ON agent_stats.agent_id = latest_buckets.agent_id
`
type GetWorkspaceAgentUsageStatsRow struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
AggregatedFrom time.Time `db:"aggregated_from" json:"aggregated_from"`
WorkspaceRxBytes int64 `db:"workspace_rx_bytes" json:"workspace_rx_bytes"`
WorkspaceTxBytes int64 `db:"workspace_tx_bytes" json:"workspace_tx_bytes"`
WorkspaceConnectionLatency50 float64 `db:"workspace_connection_latency_50" json:"workspace_connection_latency_50"`
WorkspaceConnectionLatency95 float64 `db:"workspace_connection_latency_95" json:"workspace_connection_latency_95"`
AgentID_2 uuid.UUID `db:"agent_id_2" json:"agent_id_2"`
SessionCountVSCode int64 `db:"session_count_vscode" json:"session_count_vscode"`
SessionCountSSH int64 `db:"session_count_ssh" json:"session_count_ssh"`
SessionCountJetBrains int64 `db:"session_count_jetbrains" json:"session_count_jetbrains"`
SessionCountReconnectingPTY int64 `db:"session_count_reconnecting_pty" json:"session_count_reconnecting_pty"`
}
// `minute_buckets` could return 0 rows if there are no usage stats since `created_at`.
func (q *sqlQuerier) GetWorkspaceAgentUsageStats(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentUsageStatsRow, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentUsageStats, createdAt)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetWorkspaceAgentUsageStatsRow
for rows.Next() {
var i GetWorkspaceAgentUsageStatsRow
if err := rows.Scan(
&i.UserID,
&i.AgentID,
&i.WorkspaceID,
&i.TemplateID,
&i.AggregatedFrom,
&i.WorkspaceRxBytes,
&i.WorkspaceTxBytes,
&i.WorkspaceConnectionLatency50,
&i.WorkspaceConnectionLatency95,
&i.AgentID_2,
&i.SessionCountVSCode,
&i.SessionCountSSH,
&i.SessionCountJetBrains,
&i.SessionCountReconnectingPTY,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceAgentUsageStatsAndLabels = `-- name: GetWorkspaceAgentUsageStatsAndLabels :many
WITH agent_stats AS (
SELECT
user_id,
agent_id,
workspace_id,
coalesce(SUM(rx_bytes), 0)::bigint AS rx_bytes,
coalesce(SUM(tx_bytes), 0)::bigint AS tx_bytes,
coalesce(MAX(connection_median_latency_ms), 0)::float AS connection_median_latency_ms
FROM workspace_agent_stats
-- The greater than 0 is to support legacy agents that don't report connection_median_latency_ms.
WHERE workspace_agent_stats.created_at > $1 AND connection_median_latency_ms > 0
GROUP BY user_id, agent_id, workspace_id
), latest_agent_stats AS (
SELECT
agent_id,
coalesce(SUM(session_count_vscode), 0)::bigint AS session_count_vscode,
coalesce(SUM(session_count_ssh), 0)::bigint AS session_count_ssh,
coalesce(SUM(session_count_jetbrains), 0)::bigint AS session_count_jetbrains,
coalesce(SUM(session_count_reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty,
coalesce(SUM(connection_count), 0)::bigint AS connection_count
FROM workspace_agent_stats
-- We only want the latest stats, but those stats might be
-- spread across multiple rows.
WHERE usage = true AND created_at > now() - '1 minute'::interval
GROUP BY user_id, agent_id, workspace_id
)
SELECT
users.username, workspace_agents.name AS agent_name, workspaces.name AS workspace_name, rx_bytes, tx_bytes,
coalesce(session_count_vscode, 0)::bigint AS session_count_vscode,
coalesce(session_count_ssh, 0)::bigint AS session_count_ssh,
coalesce(session_count_jetbrains, 0)::bigint AS session_count_jetbrains,
coalesce(session_count_reconnecting_pty, 0)::bigint AS session_count_reconnecting_pty,
coalesce(connection_count, 0)::bigint AS connection_count,
connection_median_latency_ms
FROM
agent_stats
LEFT JOIN
latest_agent_stats
ON
agent_stats.agent_id = latest_agent_stats.agent_id
JOIN
users
ON
users.id = agent_stats.user_id
JOIN
workspace_agents
ON
workspace_agents.id = agent_stats.agent_id
JOIN
workspaces
ON
workspaces.id = agent_stats.workspace_id
`
type GetWorkspaceAgentUsageStatsAndLabelsRow struct {
Username string `db:"username" json:"username"`
AgentName string `db:"agent_name" json:"agent_name"`
WorkspaceName string `db:"workspace_name" json:"workspace_name"`
RxBytes int64 `db:"rx_bytes" json:"rx_bytes"`
TxBytes int64 `db:"tx_bytes" json:"tx_bytes"`
SessionCountVSCode int64 `db:"session_count_vscode" json:"session_count_vscode"`
SessionCountSSH int64 `db:"session_count_ssh" json:"session_count_ssh"`
SessionCountJetBrains int64 `db:"session_count_jetbrains" json:"session_count_jetbrains"`
SessionCountReconnectingPTY int64 `db:"session_count_reconnecting_pty" json:"session_count_reconnecting_pty"`
ConnectionCount int64 `db:"connection_count" json:"connection_count"`
ConnectionMedianLatencyMS float64 `db:"connection_median_latency_ms" json:"connection_median_latency_ms"`
}
func (q *sqlQuerier) GetWorkspaceAgentUsageStatsAndLabels(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentUsageStatsAndLabelsRow, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentUsageStatsAndLabels, createdAt)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetWorkspaceAgentUsageStatsAndLabelsRow
for rows.Next() {
var i GetWorkspaceAgentUsageStatsAndLabelsRow
if err := rows.Scan(
&i.Username,
&i.AgentName,
&i.WorkspaceName,
&i.RxBytes,
&i.TxBytes,
&i.SessionCountVSCode,
&i.SessionCountSSH,
&i.SessionCountJetBrains,
&i.SessionCountReconnectingPTY,
&i.ConnectionCount,
&i.ConnectionMedianLatencyMS,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertWorkspaceAgentStats = `-- name: InsertWorkspaceAgentStats :exec
INSERT INTO
workspace_agent_stats (
id,
created_at,
user_id,
workspace_id,
template_id,
agent_id,
connections_by_proto,
connection_count,
rx_packets,
rx_bytes,
tx_packets,
tx_bytes,
session_count_vscode,
session_count_jetbrains,
session_count_reconnecting_pty,
session_count_ssh,
connection_median_latency_ms,
usage
)
SELECT
unnest($1 :: uuid[]) AS id,
unnest($2 :: timestamptz[]) AS created_at,
unnest($3 :: uuid[]) AS user_id,
unnest($4 :: uuid[]) AS workspace_id,
unnest($5 :: uuid[]) AS template_id,
unnest($6 :: uuid[]) AS agent_id,
jsonb_array_elements($7 :: jsonb) AS connections_by_proto,
unnest($8 :: bigint[]) AS connection_count,
unnest($9 :: bigint[]) AS rx_packets,
unnest($10 :: bigint[]) AS rx_bytes,
unnest($11 :: bigint[]) AS tx_packets,
unnest($12 :: bigint[]) AS tx_bytes,
unnest($13 :: bigint[]) AS session_count_vscode,
unnest($14 :: bigint[]) AS session_count_jetbrains,
unnest($15 :: bigint[]) AS session_count_reconnecting_pty,
unnest($16 :: bigint[]) AS session_count_ssh,
unnest($17 :: double precision[]) AS connection_median_latency_ms,
unnest($18 :: boolean[]) AS usage
`
type InsertWorkspaceAgentStatsParams struct {
ID []uuid.UUID `db:"id" json:"id"`
CreatedAt []time.Time `db:"created_at" json:"created_at"`
UserID []uuid.UUID `db:"user_id" json:"user_id"`
WorkspaceID []uuid.UUID `db:"workspace_id" json:"workspace_id"`
TemplateID []uuid.UUID `db:"template_id" json:"template_id"`
AgentID []uuid.UUID `db:"agent_id" json:"agent_id"`
ConnectionsByProto json.RawMessage `db:"connections_by_proto" json:"connections_by_proto"`
ConnectionCount []int64 `db:"connection_count" json:"connection_count"`
RxPackets []int64 `db:"rx_packets" json:"rx_packets"`
RxBytes []int64 `db:"rx_bytes" json:"rx_bytes"`
TxPackets []int64 `db:"tx_packets" json:"tx_packets"`
TxBytes []int64 `db:"tx_bytes" json:"tx_bytes"`
SessionCountVSCode []int64 `db:"session_count_vscode" json:"session_count_vscode"`
SessionCountJetBrains []int64 `db:"session_count_jetbrains" json:"session_count_jetbrains"`
SessionCountReconnectingPTY []int64 `db:"session_count_reconnecting_pty" json:"session_count_reconnecting_pty"`
SessionCountSSH []int64 `db:"session_count_ssh" json:"session_count_ssh"`
ConnectionMedianLatencyMS []float64 `db:"connection_median_latency_ms" json:"connection_median_latency_ms"`
Usage []bool `db:"usage" json:"usage"`
}
func (q *sqlQuerier) InsertWorkspaceAgentStats(ctx context.Context, arg InsertWorkspaceAgentStatsParams) error {
_, err := q.db.ExecContext(ctx, insertWorkspaceAgentStats,
pq.Array(arg.ID),
pq.Array(arg.CreatedAt),
pq.Array(arg.UserID),
pq.Array(arg.WorkspaceID),
pq.Array(arg.TemplateID),
pq.Array(arg.AgentID),
arg.ConnectionsByProto,
pq.Array(arg.ConnectionCount),
pq.Array(arg.RxPackets),
pq.Array(arg.RxBytes),
pq.Array(arg.TxPackets),
pq.Array(arg.TxBytes),
pq.Array(arg.SessionCountVSCode),
pq.Array(arg.SessionCountJetBrains),
pq.Array(arg.SessionCountReconnectingPTY),
pq.Array(arg.SessionCountSSH),
pq.Array(arg.ConnectionMedianLatencyMS),
pq.Array(arg.Usage),
)
return err
}
const upsertWorkspaceAppAuditSession = `-- name: UpsertWorkspaceAppAuditSession :one
INSERT INTO
workspace_app_audit_sessions (
id,
agent_id,
app_id,
user_id,
ip,
user_agent,
slug_or_port,
status_code,
started_at,
updated_at
)
VALUES
(
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
$9,
$10
)
ON CONFLICT
(agent_id, app_id, user_id, ip, user_agent, slug_or_port, status_code)
DO
UPDATE
SET
-- ID is used to know if session was reset on upsert.
id = CASE
WHEN workspace_app_audit_sessions.updated_at > NOW() - ($11::bigint || ' ms')::interval
THEN workspace_app_audit_sessions.id
ELSE EXCLUDED.id
END,
started_at = CASE
WHEN workspace_app_audit_sessions.updated_at > NOW() - ($11::bigint || ' ms')::interval
THEN workspace_app_audit_sessions.started_at
ELSE EXCLUDED.started_at
END,
updated_at = EXCLUDED.updated_at
RETURNING
id = $1 AS new_or_stale
`
type UpsertWorkspaceAppAuditSessionParams struct {
ID uuid.UUID `db:"id" json:"id"`
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
AppID uuid.UUID `db:"app_id" json:"app_id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
Ip string `db:"ip" json:"ip"`
UserAgent string `db:"user_agent" json:"user_agent"`
SlugOrPort string `db:"slug_or_port" json:"slug_or_port"`
StatusCode int32 `db:"status_code" json:"status_code"`
StartedAt time.Time `db:"started_at" json:"started_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
StaleIntervalMS int64 `db:"stale_interval_ms" json:"stale_interval_ms"`
}
// The returned boolean, new_or_stale, can be used to deduce if a new session
// was started. This means that a new row was inserted (no previous session) or
// the updated_at is older than stale interval.
func (q *sqlQuerier) UpsertWorkspaceAppAuditSession(ctx context.Context, arg UpsertWorkspaceAppAuditSessionParams) (bool, error) {
row := q.db.QueryRowContext(ctx, upsertWorkspaceAppAuditSession,
arg.ID,
arg.AgentID,
arg.AppID,
arg.UserID,
arg.Ip,
arg.UserAgent,
arg.SlugOrPort,
arg.StatusCode,
arg.StartedAt,
arg.UpdatedAt,
arg.StaleIntervalMS,
)
var new_or_stale bool
err := row.Scan(&new_or_stale)
return new_or_stale, err
}
const getLatestWorkspaceAppStatusByAppID = `-- name: GetLatestWorkspaceAppStatusByAppID :one
SELECT id, created_at, agent_id, app_id, workspace_id, state, message, uri
FROM workspace_app_statuses
WHERE app_id = $1::uuid
ORDER BY created_at DESC, id DESC
LIMIT 1
`
func (q *sqlQuerier) GetLatestWorkspaceAppStatusByAppID(ctx context.Context, appID uuid.UUID) (WorkspaceAppStatus, error) {
row := q.db.QueryRowContext(ctx, getLatestWorkspaceAppStatusByAppID, appID)
var i WorkspaceAppStatus
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.AgentID,
&i.AppID,
&i.WorkspaceID,
&i.State,
&i.Message,
&i.Uri,
)
return i, err
}
const getLatestWorkspaceAppStatusesByWorkspaceIDs = `-- name: GetLatestWorkspaceAppStatusesByWorkspaceIDs :many
SELECT DISTINCT ON (workspace_id)
id, created_at, agent_id, app_id, workspace_id, state, message, uri
FROM workspace_app_statuses
WHERE workspace_id = ANY($1 :: uuid[])
ORDER BY workspace_id, created_at DESC, id DESC
`
// id DESC is a stability tiebreaker, not an insertion-order signal: back-to-back
// inserts can share a created_at on platforms with coarse time.Now() resolution,
// and id is a random UUID, so this only guarantees a deterministic pick, not the
// later row. Callers must not depend on sub-microsecond recency here.
func (q *sqlQuerier) GetLatestWorkspaceAppStatusesByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAppStatus, error) {
rows, err := q.db.QueryContext(ctx, getLatestWorkspaceAppStatusesByWorkspaceIDs, pq.Array(ids))
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAppStatus
for rows.Next() {
var i WorkspaceAppStatus
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.AgentID,
&i.AppID,
&i.WorkspaceID,
&i.State,
&i.Message,
&i.Uri,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceAppByAgentIDAndSlug = `-- name: GetWorkspaceAppByAgentIDAndSlug :one
SELECT id, created_at, agent_id, display_name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level, slug, external, display_order, hidden, open_in, display_group, tooltip FROM workspace_apps WHERE agent_id = $1 AND slug = $2
`
type GetWorkspaceAppByAgentIDAndSlugParams struct {
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
Slug string `db:"slug" json:"slug"`
}
func (q *sqlQuerier) GetWorkspaceAppByAgentIDAndSlug(ctx context.Context, arg GetWorkspaceAppByAgentIDAndSlugParams) (WorkspaceApp, error) {
row := q.db.QueryRowContext(ctx, getWorkspaceAppByAgentIDAndSlug, arg.AgentID, arg.Slug)
var i WorkspaceApp
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.AgentID,
&i.DisplayName,
&i.Icon,
&i.Command,
&i.Url,
&i.HealthcheckUrl,
&i.HealthcheckInterval,
&i.HealthcheckThreshold,
&i.Health,
&i.Subdomain,
&i.SharingLevel,
&i.Slug,
&i.External,
&i.DisplayOrder,
&i.Hidden,
&i.OpenIn,
&i.DisplayGroup,
&i.Tooltip,
)
return i, err
}
const getWorkspaceAppStatusesByAppIDs = `-- name: GetWorkspaceAppStatusesByAppIDs :many
SELECT id, created_at, agent_id, app_id, workspace_id, state, message, uri FROM workspace_app_statuses WHERE app_id = ANY($1 :: uuid [ ])
ORDER BY created_at DESC, id DESC
`
func (q *sqlQuerier) GetWorkspaceAppStatusesByAppIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAppStatus, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceAppStatusesByAppIDs, pq.Array(ids))
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAppStatus
for rows.Next() {
var i WorkspaceAppStatus
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.AgentID,
&i.AppID,
&i.WorkspaceID,
&i.State,
&i.Message,
&i.Uri,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceAppsByAgentID = `-- name: GetWorkspaceAppsByAgentID :many
SELECT id, created_at, agent_id, display_name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level, slug, external, display_order, hidden, open_in, display_group, tooltip FROM workspace_apps WHERE agent_id = $1 ORDER BY slug ASC
`
func (q *sqlQuerier) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid.UUID) ([]WorkspaceApp, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceAppsByAgentID, agentID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceApp
for rows.Next() {
var i WorkspaceApp
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.AgentID,
&i.DisplayName,
&i.Icon,
&i.Command,
&i.Url,
&i.HealthcheckUrl,
&i.HealthcheckInterval,
&i.HealthcheckThreshold,
&i.Health,
&i.Subdomain,
&i.SharingLevel,
&i.Slug,
&i.External,
&i.DisplayOrder,
&i.Hidden,
&i.OpenIn,
&i.DisplayGroup,
&i.Tooltip,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceAppsByAgentIDs = `-- name: GetWorkspaceAppsByAgentIDs :many
SELECT id, created_at, agent_id, display_name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level, slug, external, display_order, hidden, open_in, display_group, tooltip FROM workspace_apps WHERE agent_id = ANY($1 :: uuid [ ]) ORDER BY slug ASC
`
func (q *sqlQuerier) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceApp, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceAppsByAgentIDs, pq.Array(ids))
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceApp
for rows.Next() {
var i WorkspaceApp
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.AgentID,
&i.DisplayName,
&i.Icon,
&i.Command,
&i.Url,
&i.HealthcheckUrl,
&i.HealthcheckInterval,
&i.HealthcheckThreshold,
&i.Health,
&i.Subdomain,
&i.SharingLevel,
&i.Slug,
&i.External,
&i.DisplayOrder,
&i.Hidden,
&i.OpenIn,
&i.DisplayGroup,
&i.Tooltip,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceAppsCreatedAfter = `-- name: GetWorkspaceAppsCreatedAfter :many
SELECT id, created_at, agent_id, display_name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level, slug, external, display_order, hidden, open_in, display_group, tooltip FROM workspace_apps WHERE created_at > $1 ORDER BY slug ASC
`
func (q *sqlQuerier) GetWorkspaceAppsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceApp, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceAppsCreatedAfter, createdAt)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceApp
for rows.Next() {
var i WorkspaceApp
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.AgentID,
&i.DisplayName,
&i.Icon,
&i.Command,
&i.Url,
&i.HealthcheckUrl,
&i.HealthcheckInterval,
&i.HealthcheckThreshold,
&i.Health,
&i.Subdomain,
&i.SharingLevel,
&i.Slug,
&i.External,
&i.DisplayOrder,
&i.Hidden,
&i.OpenIn,
&i.DisplayGroup,
&i.Tooltip,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertWorkspaceAppStatus = `-- name: InsertWorkspaceAppStatus :one
INSERT INTO workspace_app_statuses (id, created_at, workspace_id, agent_id, app_id, state, message, uri)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, created_at, agent_id, app_id, workspace_id, state, message, uri
`
type InsertWorkspaceAppStatusParams struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
AppID uuid.UUID `db:"app_id" json:"app_id"`
State WorkspaceAppStatusState `db:"state" json:"state"`
Message string `db:"message" json:"message"`
Uri sql.NullString `db:"uri" json:"uri"`
}
func (q *sqlQuerier) InsertWorkspaceAppStatus(ctx context.Context, arg InsertWorkspaceAppStatusParams) (WorkspaceAppStatus, error) {
row := q.db.QueryRowContext(ctx, insertWorkspaceAppStatus,
arg.ID,
arg.CreatedAt,
arg.WorkspaceID,
arg.AgentID,
arg.AppID,
arg.State,
arg.Message,
arg.Uri,
)
var i WorkspaceAppStatus
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.AgentID,
&i.AppID,
&i.WorkspaceID,
&i.State,
&i.Message,
&i.Uri,
)
return i, err
}
const updateWorkspaceAppHealthByID = `-- name: UpdateWorkspaceAppHealthByID :exec
UPDATE
workspace_apps
SET
health = $2
WHERE
id = $1
`
type UpdateWorkspaceAppHealthByIDParams struct {
ID uuid.UUID `db:"id" json:"id"`
Health WorkspaceAppHealth `db:"health" json:"health"`
}
func (q *sqlQuerier) UpdateWorkspaceAppHealthByID(ctx context.Context, arg UpdateWorkspaceAppHealthByIDParams) error {
_, err := q.db.ExecContext(ctx, updateWorkspaceAppHealthByID, arg.ID, arg.Health)
return err
}
const upsertWorkspaceApp = `-- name: UpsertWorkspaceApp :one
INSERT INTO
workspace_apps (
id,
created_at,
agent_id,
slug,
display_name,
icon,
command,
url,
external,
subdomain,
sharing_level,
healthcheck_url,
healthcheck_interval,
healthcheck_threshold,
health,
display_order,
hidden,
open_in,
display_group,
tooltip
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)
ON CONFLICT (id) DO UPDATE SET
display_name = EXCLUDED.display_name,
icon = EXCLUDED.icon,
command = EXCLUDED.command,
url = EXCLUDED.url,
external = EXCLUDED.external,
subdomain = EXCLUDED.subdomain,
sharing_level = EXCLUDED.sharing_level,
healthcheck_url = EXCLUDED.healthcheck_url,
healthcheck_interval = EXCLUDED.healthcheck_interval,
healthcheck_threshold = EXCLUDED.healthcheck_threshold,
health = EXCLUDED.health,
display_order = EXCLUDED.display_order,
hidden = EXCLUDED.hidden,
open_in = EXCLUDED.open_in,
display_group = EXCLUDED.display_group,
agent_id = EXCLUDED.agent_id,
slug = EXCLUDED.slug,
tooltip = EXCLUDED.tooltip
WHERE
-- Prevent cross-tenant/cross-workspace agent rebinding (SEC-91).
-- App IDs persist across builds of the same workspace, but agent IDs are
-- regenerated every build, so compare by the workspace that owns the agent
-- rather than by agent_id. Permit unowned apps to be claimed and permit
-- same-workspace rebuilds. If an existing app belongs to a workspace, block
-- moves to both different workspaces and template import or dry-run agents
-- that resolve to no workspace. The conflicting row is then left untouched,
-- and the :one query returns no row, which the caller treats as a
-- rejection.
NOT EXISTS (
SELECT 1
FROM workspace_agents AS existing_agent
INNER JOIN workspace_resources AS existing_resource
ON existing_agent.resource_id = existing_resource.id
INNER JOIN workspace_builds AS existing_build
ON existing_resource.job_id = existing_build.job_id
WHERE existing_agent.id = workspace_apps.agent_id
)
OR EXISTS (
SELECT 1
FROM workspace_agents AS existing_agent
INNER JOIN workspace_resources AS existing_resource
ON existing_agent.resource_id = existing_resource.id
INNER JOIN workspace_builds AS existing_build
ON existing_resource.job_id = existing_build.job_id
INNER JOIN workspace_agents AS incoming_agent
ON incoming_agent.id = EXCLUDED.agent_id
INNER JOIN workspace_resources AS incoming_resource
ON incoming_agent.resource_id = incoming_resource.id
INNER JOIN workspace_builds AS incoming_build
ON incoming_resource.job_id = incoming_build.job_id
WHERE
existing_agent.id = workspace_apps.agent_id
AND existing_build.workspace_id = incoming_build.workspace_id
)
RETURNING id, created_at, agent_id, display_name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level, slug, external, display_order, hidden, open_in, display_group, tooltip
`
type UpsertWorkspaceAppParams struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
Slug string `db:"slug" json:"slug"`
DisplayName string `db:"display_name" json:"display_name"`
Icon string `db:"icon" json:"icon"`
Command sql.NullString `db:"command" json:"command"`
Url sql.NullString `db:"url" json:"url"`
External bool `db:"external" json:"external"`
Subdomain bool `db:"subdomain" json:"subdomain"`
SharingLevel AppSharingLevel `db:"sharing_level" json:"sharing_level"`
HealthcheckUrl string `db:"healthcheck_url" json:"healthcheck_url"`
HealthcheckInterval int32 `db:"healthcheck_interval" json:"healthcheck_interval"`
HealthcheckThreshold int32 `db:"healthcheck_threshold" json:"healthcheck_threshold"`
Health WorkspaceAppHealth `db:"health" json:"health"`
DisplayOrder int32 `db:"display_order" json:"display_order"`
Hidden bool `db:"hidden" json:"hidden"`
OpenIn WorkspaceAppOpenIn `db:"open_in" json:"open_in"`
DisplayGroup sql.NullString `db:"display_group" json:"display_group"`
Tooltip string `db:"tooltip" json:"tooltip"`
}
func (q *sqlQuerier) UpsertWorkspaceApp(ctx context.Context, arg UpsertWorkspaceAppParams) (WorkspaceApp, error) {
row := q.db.QueryRowContext(ctx, upsertWorkspaceApp,
arg.ID,
arg.CreatedAt,
arg.AgentID,
arg.Slug,
arg.DisplayName,
arg.Icon,
arg.Command,
arg.Url,
arg.External,
arg.Subdomain,
arg.SharingLevel,
arg.HealthcheckUrl,
arg.HealthcheckInterval,
arg.HealthcheckThreshold,
arg.Health,
arg.DisplayOrder,
arg.Hidden,
arg.OpenIn,
arg.DisplayGroup,
arg.Tooltip,
)
var i WorkspaceApp
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.AgentID,
&i.DisplayName,
&i.Icon,
&i.Command,
&i.Url,
&i.HealthcheckUrl,
&i.HealthcheckInterval,
&i.HealthcheckThreshold,
&i.Health,
&i.Subdomain,
&i.SharingLevel,
&i.Slug,
&i.External,
&i.DisplayOrder,
&i.Hidden,
&i.OpenIn,
&i.DisplayGroup,
&i.Tooltip,
)
return i, err
}
const insertWorkspaceAppStats = `-- name: InsertWorkspaceAppStats :exec
INSERT INTO
workspace_app_stats (
user_id,
workspace_id,
agent_id,
access_method,
slug_or_port,
session_id,
session_started_at,
session_ended_at,
requests
)
SELECT
unnest($1::uuid[]) AS user_id,
unnest($2::uuid[]) AS workspace_id,
unnest($3::uuid[]) AS agent_id,
unnest($4::text[]) AS access_method,
unnest($5::text[]) AS slug_or_port,
unnest($6::uuid[]) AS session_id,
unnest($7::timestamptz[]) AS session_started_at,
unnest($8::timestamptz[]) AS session_ended_at,
unnest($9::int[]) AS requests
ON CONFLICT
(user_id, agent_id, session_id)
DO
UPDATE SET
session_ended_at = EXCLUDED.session_ended_at,
requests = EXCLUDED.requests
WHERE
workspace_app_stats.user_id = EXCLUDED.user_id
AND workspace_app_stats.agent_id = EXCLUDED.agent_id
AND workspace_app_stats.session_id = EXCLUDED.session_id
-- Since stats are updated in place as time progresses, we only
-- want to update this row if it's fresh.
AND workspace_app_stats.session_ended_at <= EXCLUDED.session_ended_at
AND workspace_app_stats.requests <= EXCLUDED.requests
`
type InsertWorkspaceAppStatsParams struct {
UserID []uuid.UUID `db:"user_id" json:"user_id"`
WorkspaceID []uuid.UUID `db:"workspace_id" json:"workspace_id"`
AgentID []uuid.UUID `db:"agent_id" json:"agent_id"`
AccessMethod []string `db:"access_method" json:"access_method"`
SlugOrPort []string `db:"slug_or_port" json:"slug_or_port"`
SessionID []uuid.UUID `db:"session_id" json:"session_id"`
SessionStartedAt []time.Time `db:"session_started_at" json:"session_started_at"`
SessionEndedAt []time.Time `db:"session_ended_at" json:"session_ended_at"`
Requests []int32 `db:"requests" json:"requests"`
}
func (q *sqlQuerier) InsertWorkspaceAppStats(ctx context.Context, arg InsertWorkspaceAppStatsParams) error {
_, err := q.db.ExecContext(ctx, insertWorkspaceAppStats,
pq.Array(arg.UserID),
pq.Array(arg.WorkspaceID),
pq.Array(arg.AgentID),
pq.Array(arg.AccessMethod),
pq.Array(arg.SlugOrPort),
pq.Array(arg.SessionID),
pq.Array(arg.SessionStartedAt),
pq.Array(arg.SessionEndedAt),
pq.Array(arg.Requests),
)
return err
}
const deleteOldWorkspaceBuildOrchestrations = `-- name: DeleteOldWorkspaceBuildOrchestrations :execrows
WITH deletable AS (
SELECT
id
FROM
workspace_build_orchestrations
WHERE
status IN ('completed', 'failed', 'canceled')
AND updated_at < $1::timestamptz
ORDER BY
updated_at ASC
LIMIT $2::int
)
DELETE FROM workspace_build_orchestrations
USING deletable
WHERE workspace_build_orchestrations.id = deletable.id
`
type DeleteOldWorkspaceBuildOrchestrationsParams struct {
BeforeTime time.Time `db:"before_time" json:"before_time"`
LimitCount int32 `db:"limit_count" json:"limit_count"`
}
func (q *sqlQuerier) DeleteOldWorkspaceBuildOrchestrations(ctx context.Context, arg DeleteOldWorkspaceBuildOrchestrationsParams) (int64, error) {
result, err := q.db.ExecContext(ctx, deleteOldWorkspaceBuildOrchestrations, arg.BeforeTime, arg.LimitCount)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const getNextPendingWorkspaceBuildOrchestrationForUpdate = `-- name: GetNextPendingWorkspaceBuildOrchestrationForUpdate :one
SELECT
wbo.id, wbo.created_at, wbo.updated_at, wbo.workspace_id, wbo.parent_build_id, wbo.child_build_id, wbo.child_transition, wbo.child_template_version_id, wbo.child_template_version_preset_id, wbo.child_rich_parameter_values, wbo.child_log_level, wbo.child_reason, wbo.attempt_count, wbo.next_retry_after, wbo.status, wbo.error
FROM
workspace_build_orchestrations wbo
JOIN workspace_builds wb ON wbo.parent_build_id = wb.id
JOIN provisioner_jobs pj ON wb.job_id = pj.id
WHERE
wbo.status = 'pending'
AND (
wbo.next_retry_after IS NULL
OR wbo.next_retry_after <= NOW()
)
-- Include all terminal parent states so pending orchestration
-- rows are processed and resolved even when no child build should
-- be created.
AND pj.job_status IN ('succeeded', 'failed', 'canceled')
ORDER BY
wbo.created_at ASC
LIMIT 1
FOR UPDATE OF wbo SKIP LOCKED
`
// Must be called from within a transaction. The row lock is released
// when the transaction ends.
func (q *sqlQuerier) GetNextPendingWorkspaceBuildOrchestrationForUpdate(ctx context.Context) (WorkspaceBuildOrchestration, error) {
row := q.db.QueryRowContext(ctx, getNextPendingWorkspaceBuildOrchestrationForUpdate)
var i WorkspaceBuildOrchestration
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.WorkspaceID,
&i.ParentBuildID,
&i.ChildBuildID,
&i.ChildTransition,
&i.ChildTemplateVersionID,
&i.ChildTemplateVersionPresetID,
&i.ChildRichParameterValues,
&i.ChildLogLevel,
&i.ChildReason,
&i.AttemptCount,
&i.NextRetryAfter,
&i.Status,
&i.Error,
)
return i, err
}
const insertWorkspaceBuildOrchestration = `-- name: InsertWorkspaceBuildOrchestration :one
INSERT INTO workspace_build_orchestrations (
id,
created_at,
updated_at,
parent_build_id,
workspace_id,
child_transition,
child_template_version_id,
child_template_version_preset_id,
child_rich_parameter_values,
child_log_level,
child_reason,
status,
error
)
VALUES (
$1,
$2,
$3,
$4,
(SELECT workspace_id FROM workspace_builds WHERE id = $4),
$5,
$6,
$7,
$8,
$9,
$10,
'pending',
NULL
)
RETURNING id, created_at, updated_at, workspace_id, parent_build_id, child_build_id, child_transition, child_template_version_id, child_template_version_preset_id, child_rich_parameter_values, child_log_level, child_reason, attempt_count, next_retry_after, status, error
`
type InsertWorkspaceBuildOrchestrationParams struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
ParentBuildID uuid.UUID `db:"parent_build_id" json:"parent_build_id"`
ChildTransition WorkspaceTransition `db:"child_transition" json:"child_transition"`
ChildTemplateVersionID uuid.NullUUID `db:"child_template_version_id" json:"child_template_version_id"`
ChildTemplateVersionPresetID uuid.NullUUID `db:"child_template_version_preset_id" json:"child_template_version_preset_id"`
ChildRichParameterValues json.RawMessage `db:"child_rich_parameter_values" json:"child_rich_parameter_values"`
ChildLogLevel string `db:"child_log_level" json:"child_log_level"`
ChildReason NullBuildReason `db:"child_reason" json:"child_reason"`
}
func (q *sqlQuerier) InsertWorkspaceBuildOrchestration(ctx context.Context, arg InsertWorkspaceBuildOrchestrationParams) (WorkspaceBuildOrchestration, error) {
row := q.db.QueryRowContext(ctx, insertWorkspaceBuildOrchestration,
arg.ID,
arg.CreatedAt,
arg.UpdatedAt,
arg.ParentBuildID,
arg.ChildTransition,
arg.ChildTemplateVersionID,
arg.ChildTemplateVersionPresetID,
arg.ChildRichParameterValues,
arg.ChildLogLevel,
arg.ChildReason,
)
var i WorkspaceBuildOrchestration
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.WorkspaceID,
&i.ParentBuildID,
&i.ChildBuildID,
&i.ChildTransition,
&i.ChildTemplateVersionID,
&i.ChildTemplateVersionPresetID,
&i.ChildRichParameterValues,
&i.ChildLogLevel,
&i.ChildReason,
&i.AttemptCount,
&i.NextRetryAfter,
&i.Status,
&i.Error,
)
return i, err
}
const updateWorkspaceBuildOrchestrationCanceledByID = `-- name: UpdateWorkspaceBuildOrchestrationCanceledByID :one
UPDATE
workspace_build_orchestrations
SET
status = 'canceled',
next_retry_after = NULL,
error = NULL,
updated_at = $1
WHERE
id = $2
AND status = 'pending'
RETURNING id, created_at, updated_at, workspace_id, parent_build_id, child_build_id, child_transition, child_template_version_id, child_template_version_preset_id, child_rich_parameter_values, child_log_level, child_reason, attempt_count, next_retry_after, status, error
`
type UpdateWorkspaceBuildOrchestrationCanceledByIDParams struct {
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateWorkspaceBuildOrchestrationCanceledByID(ctx context.Context, arg UpdateWorkspaceBuildOrchestrationCanceledByIDParams) (WorkspaceBuildOrchestration, error) {
row := q.db.QueryRowContext(ctx, updateWorkspaceBuildOrchestrationCanceledByID, arg.UpdatedAt, arg.ID)
var i WorkspaceBuildOrchestration
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.WorkspaceID,
&i.ParentBuildID,
&i.ChildBuildID,
&i.ChildTransition,
&i.ChildTemplateVersionID,
&i.ChildTemplateVersionPresetID,
&i.ChildRichParameterValues,
&i.ChildLogLevel,
&i.ChildReason,
&i.AttemptCount,
&i.NextRetryAfter,
&i.Status,
&i.Error,
)
return i, err
}
const updateWorkspaceBuildOrchestrationCompletedByID = `-- name: UpdateWorkspaceBuildOrchestrationCompletedByID :one
UPDATE
workspace_build_orchestrations
SET
child_build_id = $1,
status = 'completed',
next_retry_after = NULL,
error = NULL,
updated_at = $2
WHERE
id = $3
AND status = 'pending'
RETURNING id, created_at, updated_at, workspace_id, parent_build_id, child_build_id, child_transition, child_template_version_id, child_template_version_preset_id, child_rich_parameter_values, child_log_level, child_reason, attempt_count, next_retry_after, status, error
`
type UpdateWorkspaceBuildOrchestrationCompletedByIDParams struct {
ChildBuildID uuid.NullUUID `db:"child_build_id" json:"child_build_id"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateWorkspaceBuildOrchestrationCompletedByID(ctx context.Context, arg UpdateWorkspaceBuildOrchestrationCompletedByIDParams) (WorkspaceBuildOrchestration, error) {
row := q.db.QueryRowContext(ctx, updateWorkspaceBuildOrchestrationCompletedByID, arg.ChildBuildID, arg.UpdatedAt, arg.ID)
var i WorkspaceBuildOrchestration
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.WorkspaceID,
&i.ParentBuildID,
&i.ChildBuildID,
&i.ChildTransition,
&i.ChildTemplateVersionID,
&i.ChildTemplateVersionPresetID,
&i.ChildRichParameterValues,
&i.ChildLogLevel,
&i.ChildReason,
&i.AttemptCount,
&i.NextRetryAfter,
&i.Status,
&i.Error,
)
return i, err
}
const updateWorkspaceBuildOrchestrationFailedByID = `-- name: UpdateWorkspaceBuildOrchestrationFailedByID :one
UPDATE
workspace_build_orchestrations
SET
status = 'failed',
next_retry_after = NULL,
error = $1,
updated_at = $2
WHERE
id = $3
AND status = 'pending'
RETURNING id, created_at, updated_at, workspace_id, parent_build_id, child_build_id, child_transition, child_template_version_id, child_template_version_preset_id, child_rich_parameter_values, child_log_level, child_reason, attempt_count, next_retry_after, status, error
`
type UpdateWorkspaceBuildOrchestrationFailedByIDParams struct {
Error sql.NullString `db:"error" json:"error"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateWorkspaceBuildOrchestrationFailedByID(ctx context.Context, arg UpdateWorkspaceBuildOrchestrationFailedByIDParams) (WorkspaceBuildOrchestration, error) {
row := q.db.QueryRowContext(ctx, updateWorkspaceBuildOrchestrationFailedByID, arg.Error, arg.UpdatedAt, arg.ID)
var i WorkspaceBuildOrchestration
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.WorkspaceID,
&i.ParentBuildID,
&i.ChildBuildID,
&i.ChildTransition,
&i.ChildTemplateVersionID,
&i.ChildTemplateVersionPresetID,
&i.ChildRichParameterValues,
&i.ChildLogLevel,
&i.ChildReason,
&i.AttemptCount,
&i.NextRetryAfter,
&i.Status,
&i.Error,
)
return i, err
}
const updateWorkspaceBuildOrchestrationRetryByID = `-- name: UpdateWorkspaceBuildOrchestrationRetryByID :one
UPDATE
workspace_build_orchestrations
SET
attempt_count = attempt_count + 1,
next_retry_after = CASE
WHEN attempt_count + 1 >= $1::int THEN NULL
ELSE $2::timestamptz
END,
status = CASE
WHEN attempt_count + 1 >= $1::int THEN 'failed'
ELSE status
END,
error = $3,
updated_at = $4
WHERE
id = $5
AND status = 'pending'
RETURNING id, created_at, updated_at, workspace_id, parent_build_id, child_build_id, child_transition, child_template_version_id, child_template_version_preset_id, child_rich_parameter_values, child_log_level, child_reason, attempt_count, next_retry_after, status, error
`
type UpdateWorkspaceBuildOrchestrationRetryByIDParams struct {
MaxAttemptCount int32 `db:"max_attempt_count" json:"max_attempt_count"`
NextRetryAfter time.Time `db:"next_retry_after" json:"next_retry_after"`
Error sql.NullString `db:"error" json:"error"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateWorkspaceBuildOrchestrationRetryByID(ctx context.Context, arg UpdateWorkspaceBuildOrchestrationRetryByIDParams) (WorkspaceBuildOrchestration, error) {
row := q.db.QueryRowContext(ctx, updateWorkspaceBuildOrchestrationRetryByID,
arg.MaxAttemptCount,
arg.NextRetryAfter,
arg.Error,
arg.UpdatedAt,
arg.ID,
)
var i WorkspaceBuildOrchestration
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.WorkspaceID,
&i.ParentBuildID,
&i.ChildBuildID,
&i.ChildTransition,
&i.ChildTemplateVersionID,
&i.ChildTemplateVersionPresetID,
&i.ChildRichParameterValues,
&i.ChildLogLevel,
&i.ChildReason,
&i.AttemptCount,
&i.NextRetryAfter,
&i.Status,
&i.Error,
)
return i, err
}
const getUserWorkspaceBuildParameters = `-- name: GetUserWorkspaceBuildParameters :many
SELECT name, value
FROM (
SELECT DISTINCT ON (tvp.name)
tvp.name,
wbp.value,
wb.created_at
FROM
workspace_build_parameters wbp
JOIN
workspace_builds wb ON wb.id = wbp.workspace_build_id
JOIN
workspaces w ON w.id = wb.workspace_id
JOIN
template_version_parameters tvp ON tvp.template_version_id = wb.template_version_id
WHERE
w.owner_id = $1
AND wb.transition = 'start'
AND w.template_id = $2
AND tvp.ephemeral = false
AND tvp.name = wbp.name
ORDER BY
tvp.name, wb.created_at DESC
) q1
ORDER BY created_at DESC, name
LIMIT 100
`
type GetUserWorkspaceBuildParametersParams struct {
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
}
type GetUserWorkspaceBuildParametersRow struct {
Name string `db:"name" json:"name"`
Value string `db:"value" json:"value"`
}
func (q *sqlQuerier) GetUserWorkspaceBuildParameters(ctx context.Context, arg GetUserWorkspaceBuildParametersParams) ([]GetUserWorkspaceBuildParametersRow, error) {
rows, err := q.db.QueryContext(ctx, getUserWorkspaceBuildParameters, arg.OwnerID, arg.TemplateID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetUserWorkspaceBuildParametersRow
for rows.Next() {
var i GetUserWorkspaceBuildParametersRow
if err := rows.Scan(&i.Name, &i.Value); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceBuildParameters = `-- name: GetWorkspaceBuildParameters :many
SELECT
workspace_build_id, name, value
FROM
workspace_build_parameters
WHERE
workspace_build_id = $1
`
func (q *sqlQuerier) GetWorkspaceBuildParameters(ctx context.Context, workspaceBuildID uuid.UUID) ([]WorkspaceBuildParameter, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceBuildParameters, workspaceBuildID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceBuildParameter
for rows.Next() {
var i WorkspaceBuildParameter
if err := rows.Scan(&i.WorkspaceBuildID, &i.Name, &i.Value); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertWorkspaceBuildParameters = `-- name: InsertWorkspaceBuildParameters :exec
INSERT INTO
workspace_build_parameters (workspace_build_id, name, value)
SELECT
$1 :: uuid AS workspace_build_id,
unnest($2 :: text[]) AS name,
unnest($3 :: text[]) AS value
RETURNING workspace_build_id, name, value
`
type InsertWorkspaceBuildParametersParams struct {
WorkspaceBuildID uuid.UUID `db:"workspace_build_id" json:"workspace_build_id"`
Name []string `db:"name" json:"name"`
Value []string `db:"value" json:"value"`
}
func (q *sqlQuerier) InsertWorkspaceBuildParameters(ctx context.Context, arg InsertWorkspaceBuildParametersParams) error {
_, err := q.db.ExecContext(ctx, insertWorkspaceBuildParameters, arg.WorkspaceBuildID, pq.Array(arg.Name), pq.Array(arg.Value))
return err
}
const getActiveWorkspaceBuildsByTemplateID = `-- name: GetActiveWorkspaceBuildsByTemplateID :many
SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.job_id, wb.deadline, wb.reason, wb.daily_cost, wb.max_deadline, wb.template_version_preset_id, wb.has_ai_task, wb.has_external_agent, wb.notified_autostop_deadline, wb.initiator_by_avatar_url, wb.initiator_by_username, wb.initiator_by_name
FROM (
SELECT
workspace_id, MAX(build_number) as max_build_number
FROM
workspace_build_with_user AS workspace_builds
WHERE
workspace_id IN (
SELECT
id
FROM
workspaces
WHERE
template_id = $1
)
GROUP BY
workspace_id
) m
JOIN
workspace_build_with_user AS wb
ON m.workspace_id = wb.workspace_id AND m.max_build_number = wb.build_number
JOIN
provisioner_jobs AS pj
ON wb.job_id = pj.id
WHERE
wb.transition = 'start'::workspace_transition
AND
pj.completed_at IS NOT NULL
`
func (q *sqlQuerier) GetActiveWorkspaceBuildsByTemplateID(ctx context.Context, templateID uuid.UUID) ([]WorkspaceBuild, error) {
rows, err := q.db.QueryContext(ctx, getActiveWorkspaceBuildsByTemplateID, templateID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceBuild
for rows.Next() {
var i WorkspaceBuild
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.WorkspaceID,
&i.TemplateVersionID,
&i.BuildNumber,
&i.Transition,
&i.InitiatorID,
&i.JobID,
&i.Deadline,
&i.Reason,
&i.DailyCost,
&i.MaxDeadline,
&i.TemplateVersionPresetID,
&i.HasAITask,
&i.HasExternalAgent,
&i.NotifiedAutostopDeadline,
&i.InitiatorByAvatarUrl,
&i.InitiatorByUsername,
&i.InitiatorByName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getFailedWorkspaceBuildsByTemplateID = `-- name: GetFailedWorkspaceBuildsByTemplateID :many
SELECT
tv.name AS template_version_name,
u.username AS workspace_owner_username,
w.name AS workspace_name,
w.id AS workspace_id,
wb.build_number AS workspace_build_number
FROM
workspace_build_with_user AS wb
JOIN
workspaces AS w
ON
wb.workspace_id = w.id
JOIN
users AS u
ON
w.owner_id = u.id
JOIN
provisioner_jobs AS pj
ON
wb.job_id = pj.id
JOIN
templates AS t
ON
w.template_id = t.id
JOIN
template_versions AS tv
ON
wb.template_version_id = tv.id
WHERE
w.template_id = $1
AND wb.created_at >= $2
AND pj.completed_at IS NOT NULL
AND pj.job_status = 'failed'
ORDER BY
tv.name ASC, wb.build_number DESC
`
type GetFailedWorkspaceBuildsByTemplateIDParams struct {
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
Since time.Time `db:"since" json:"since"`
}
type GetFailedWorkspaceBuildsByTemplateIDRow struct {
TemplateVersionName string `db:"template_version_name" json:"template_version_name"`
WorkspaceOwnerUsername string `db:"workspace_owner_username" json:"workspace_owner_username"`
WorkspaceName string `db:"workspace_name" json:"workspace_name"`
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
WorkspaceBuildNumber int32 `db:"workspace_build_number" json:"workspace_build_number"`
}
func (q *sqlQuerier) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, arg GetFailedWorkspaceBuildsByTemplateIDParams) ([]GetFailedWorkspaceBuildsByTemplateIDRow, error) {
rows, err := q.db.QueryContext(ctx, getFailedWorkspaceBuildsByTemplateID, arg.TemplateID, arg.Since)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetFailedWorkspaceBuildsByTemplateIDRow
for rows.Next() {
var i GetFailedWorkspaceBuildsByTemplateIDRow
if err := rows.Scan(
&i.TemplateVersionName,
&i.WorkspaceOwnerUsername,
&i.WorkspaceName,
&i.WorkspaceID,
&i.WorkspaceBuildNumber,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getLatestWorkspaceBuildByWorkspaceID = `-- name: GetLatestWorkspaceBuildByWorkspaceID :one
SELECT
id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, notified_autostop_deadline, initiator_by_avatar_url, initiator_by_username, initiator_by_name
FROM
workspace_build_with_user AS workspace_builds
WHERE
workspace_id = $1
ORDER BY
build_number desc
LIMIT
1
`
func (q *sqlQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (WorkspaceBuild, error) {
row := q.db.QueryRowContext(ctx, getLatestWorkspaceBuildByWorkspaceID, workspaceID)
var i WorkspaceBuild
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.WorkspaceID,
&i.TemplateVersionID,
&i.BuildNumber,
&i.Transition,
&i.InitiatorID,
&i.JobID,
&i.Deadline,
&i.Reason,
&i.DailyCost,
&i.MaxDeadline,
&i.TemplateVersionPresetID,
&i.HasAITask,
&i.HasExternalAgent,
&i.NotifiedAutostopDeadline,
&i.InitiatorByAvatarUrl,
&i.InitiatorByUsername,
&i.InitiatorByName,
)
return i, err
}
const getLatestWorkspaceBuildWithStatusByWorkspaceID = `-- name: GetLatestWorkspaceBuildWithStatusByWorkspaceID :one
SELECT
workspace_builds.transition, workspace_builds.build_number, provisioner_jobs.job_status,
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at, workspaces.group_acl, workspaces.user_acl -- Used for dbauthz fetch() checks
FROM
workspace_builds
INNER JOIN
provisioner_jobs ON workspace_builds.job_id = provisioner_jobs.id
INNER JOIN
workspaces ON workspace_builds.workspace_id = workspaces.id
WHERE
workspace_builds.workspace_id = $1 AND
workspaces.deleted = false
ORDER BY
workspace_builds.build_number desc
LIMIT
1
`
type GetLatestWorkspaceBuildWithStatusByWorkspaceIDRow struct {
Transition WorkspaceTransition `db:"transition" json:"transition"`
BuildNumber int32 `db:"build_number" json:"build_number"`
JobStatus ProvisionerJobStatus `db:"job_status" json:"job_status"`
WorkspaceTable WorkspaceTable `db:"workspace_table" json:"workspace_table"`
}
func (q *sqlQuerier) GetLatestWorkspaceBuildWithStatusByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (GetLatestWorkspaceBuildWithStatusByWorkspaceIDRow, error) {
row := q.db.QueryRowContext(ctx, getLatestWorkspaceBuildWithStatusByWorkspaceID, workspaceID)
var i GetLatestWorkspaceBuildWithStatusByWorkspaceIDRow
err := row.Scan(
&i.Transition,
&i.BuildNumber,
&i.JobStatus,
&i.WorkspaceTable.ID,
&i.WorkspaceTable.CreatedAt,
&i.WorkspaceTable.UpdatedAt,
&i.WorkspaceTable.OwnerID,
&i.WorkspaceTable.OrganizationID,
&i.WorkspaceTable.TemplateID,
&i.WorkspaceTable.Deleted,
&i.WorkspaceTable.Name,
&i.WorkspaceTable.AutostartSchedule,
&i.WorkspaceTable.Ttl,
&i.WorkspaceTable.LastUsedAt,
&i.WorkspaceTable.DormantAt,
&i.WorkspaceTable.DeletingAt,
&i.WorkspaceTable.AutomaticUpdates,
&i.WorkspaceTable.Favorite,
&i.WorkspaceTable.NextStartAt,
&i.WorkspaceTable.GroupACL,
&i.WorkspaceTable.UserACL,
)
return i, err
}
const getLatestWorkspaceBuildsByWorkspaceIDs = `-- name: GetLatestWorkspaceBuildsByWorkspaceIDs :many
SELECT
DISTINCT ON (workspace_id)
id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, notified_autostop_deadline, initiator_by_avatar_url, initiator_by_username, initiator_by_name
FROM
workspace_build_with_user AS workspace_builds
WHERE
workspace_id = ANY($1 :: uuid [ ])
ORDER BY
workspace_id, build_number DESC -- latest first
`
func (q *sqlQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceBuild, error) {
rows, err := q.db.QueryContext(ctx, getLatestWorkspaceBuildsByWorkspaceIDs, pq.Array(ids))
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceBuild
for rows.Next() {
var i WorkspaceBuild
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.WorkspaceID,
&i.TemplateVersionID,
&i.BuildNumber,
&i.Transition,
&i.InitiatorID,
&i.JobID,
&i.Deadline,
&i.Reason,
&i.DailyCost,
&i.MaxDeadline,
&i.TemplateVersionPresetID,
&i.HasAITask,
&i.HasExternalAgent,
&i.NotifiedAutostopDeadline,
&i.InitiatorByAvatarUrl,
&i.InitiatorByUsername,
&i.InitiatorByName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceBuildByID = `-- name: GetWorkspaceBuildByID :one
SELECT
id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, notified_autostop_deadline, initiator_by_avatar_url, initiator_by_username, initiator_by_name
FROM
workspace_build_with_user AS workspace_builds
WHERE
id = $1
LIMIT
1
`
func (q *sqlQuerier) GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (WorkspaceBuild, error) {
row := q.db.QueryRowContext(ctx, getWorkspaceBuildByID, id)
var i WorkspaceBuild
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.WorkspaceID,
&i.TemplateVersionID,
&i.BuildNumber,
&i.Transition,
&i.InitiatorID,
&i.JobID,
&i.Deadline,
&i.Reason,
&i.DailyCost,
&i.MaxDeadline,
&i.TemplateVersionPresetID,
&i.HasAITask,
&i.HasExternalAgent,
&i.NotifiedAutostopDeadline,
&i.InitiatorByAvatarUrl,
&i.InitiatorByUsername,
&i.InitiatorByName,
)
return i, err
}
const getWorkspaceBuildByJobID = `-- name: GetWorkspaceBuildByJobID :one
SELECT
id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, notified_autostop_deadline, initiator_by_avatar_url, initiator_by_username, initiator_by_name
FROM
workspace_build_with_user AS workspace_builds
WHERE
job_id = $1
LIMIT
1
`
func (q *sqlQuerier) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (WorkspaceBuild, error) {
row := q.db.QueryRowContext(ctx, getWorkspaceBuildByJobID, jobID)
var i WorkspaceBuild
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.WorkspaceID,
&i.TemplateVersionID,
&i.BuildNumber,
&i.Transition,
&i.InitiatorID,
&i.JobID,
&i.Deadline,
&i.Reason,
&i.DailyCost,
&i.MaxDeadline,
&i.TemplateVersionPresetID,
&i.HasAITask,
&i.HasExternalAgent,
&i.NotifiedAutostopDeadline,
&i.InitiatorByAvatarUrl,
&i.InitiatorByUsername,
&i.InitiatorByName,
)
return i, err
}
const getWorkspaceBuildByWorkspaceIDAndBuildNumber = `-- name: GetWorkspaceBuildByWorkspaceIDAndBuildNumber :one
SELECT
id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, notified_autostop_deadline, initiator_by_avatar_url, initiator_by_username, initiator_by_name
FROM
workspace_build_with_user AS workspace_builds
WHERE
workspace_id = $1
AND build_number = $2
`
type GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams struct {
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
BuildNumber int32 `db:"build_number" json:"build_number"`
}
func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (WorkspaceBuild, error) {
row := q.db.QueryRowContext(ctx, getWorkspaceBuildByWorkspaceIDAndBuildNumber, arg.WorkspaceID, arg.BuildNumber)
var i WorkspaceBuild
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.WorkspaceID,
&i.TemplateVersionID,
&i.BuildNumber,
&i.Transition,
&i.InitiatorID,
&i.JobID,
&i.Deadline,
&i.Reason,
&i.DailyCost,
&i.MaxDeadline,
&i.TemplateVersionPresetID,
&i.HasAITask,
&i.HasExternalAgent,
&i.NotifiedAutostopDeadline,
&i.InitiatorByAvatarUrl,
&i.InitiatorByUsername,
&i.InitiatorByName,
)
return i, err
}
const getWorkspaceBuildMetricsByResourceID = `-- name: GetWorkspaceBuildMetricsByResourceID :one
SELECT
wb.created_at,
wb.transition,
t.name AS template_name,
o.name AS organization_name,
(w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0') AS is_prebuild,
-- All agents must have ready_at set (terminal startup state)
COUNT(*) FILTER (WHERE wa.ready_at IS NULL) = 0 AS all_agents_ready,
-- Latest ready_at across all agents (for duration calculation)
MAX(wa.ready_at)::timestamptz AS last_agent_ready_at,
-- Worst status: error > timeout > ready
CASE
WHEN bool_or(wa.lifecycle_state = 'start_error') THEN 'error'
WHEN bool_or(wa.lifecycle_state = 'start_timeout') THEN 'timeout'
ELSE 'success'
END AS worst_status
FROM workspace_builds wb
JOIN workspaces w ON wb.workspace_id = w.id
JOIN templates t ON w.template_id = t.id
JOIN organizations o ON t.organization_id = o.id
JOIN workspace_resources wr ON wr.job_id = wb.job_id
JOIN workspace_agents wa ON wa.resource_id = wr.id AND wa.parent_id IS NULL
WHERE wb.job_id = (SELECT job_id FROM workspace_resources WHERE workspace_resources.id = $1)
GROUP BY wb.created_at, wb.transition, t.name, o.name, w.owner_id
`
type GetWorkspaceBuildMetricsByResourceIDRow struct {
CreatedAt time.Time `db:"created_at" json:"created_at"`
Transition WorkspaceTransition `db:"transition" json:"transition"`
TemplateName string `db:"template_name" json:"template_name"`
OrganizationName string `db:"organization_name" json:"organization_name"`
IsPrebuild bool `db:"is_prebuild" json:"is_prebuild"`
AllAgentsReady bool `db:"all_agents_ready" json:"all_agents_ready"`
LastAgentReadyAt time.Time `db:"last_agent_ready_at" json:"last_agent_ready_at"`
WorstStatus string `db:"worst_status" json:"worst_status"`
}
// Returns build metadata for e2e workspace build duration metrics.
// Also checks if all agents are ready and returns the worst status.
func (q *sqlQuerier) GetWorkspaceBuildMetricsByResourceID(ctx context.Context, id uuid.UUID) (GetWorkspaceBuildMetricsByResourceIDRow, error) {
row := q.db.QueryRowContext(ctx, getWorkspaceBuildMetricsByResourceID, id)
var i GetWorkspaceBuildMetricsByResourceIDRow
err := row.Scan(
&i.CreatedAt,
&i.Transition,
&i.TemplateName,
&i.OrganizationName,
&i.IsPrebuild,
&i.AllAgentsReady,
&i.LastAgentReadyAt,
&i.WorstStatus,
)
return i, err
}
const getWorkspaceBuildProvisionerStateByID = `-- name: GetWorkspaceBuildProvisionerStateByID :one
SELECT
workspace_builds.provisioner_state,
templates.id AS template_id,
templates.organization_id AS template_organization_id,
templates.user_acl,
templates.group_acl
FROM
workspace_builds
INNER JOIN
workspaces ON workspaces.id = workspace_builds.workspace_id
INNER JOIN
templates ON templates.id = workspaces.template_id
WHERE
workspace_builds.id = $1
`
type GetWorkspaceBuildProvisionerStateByIDRow struct {
ProvisionerState []byte `db:"provisioner_state" json:"provisioner_state"`
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
TemplateOrganizationID uuid.UUID `db:"template_organization_id" json:"template_organization_id"`
UserACL TemplateACL `db:"user_acl" json:"user_acl"`
GroupACL TemplateACL `db:"group_acl" json:"group_acl"`
}
// Fetches the provisioner state of a workspace build, joined through to the
// template so that dbauthz can enforce policy.ActionUpdate on the template.
// Provisioner state contains sensitive Terraform state and should only be
// accessible to template administrators.
func (q *sqlQuerier) GetWorkspaceBuildProvisionerStateByID(ctx context.Context, workspaceBuildID uuid.UUID) (GetWorkspaceBuildProvisionerStateByIDRow, error) {
row := q.db.QueryRowContext(ctx, getWorkspaceBuildProvisionerStateByID, workspaceBuildID)
var i GetWorkspaceBuildProvisionerStateByIDRow
err := row.Scan(
&i.ProvisionerState,
&i.TemplateID,
&i.TemplateOrganizationID,
&i.UserACL,
&i.GroupACL,
)
return i, err
}
const getWorkspaceBuildStatsByTemplates = `-- name: GetWorkspaceBuildStatsByTemplates :many
SELECT
w.template_id,
t.name AS template_name,
t.display_name AS template_display_name,
t.organization_id AS template_organization_id,
COUNT(*) AS total_builds,
COUNT(CASE WHEN pj.job_status = 'failed' THEN 1 END) AS failed_builds
FROM
workspace_build_with_user AS wb
JOIN
workspaces AS w ON
wb.workspace_id = w.id
JOIN
provisioner_jobs AS pj ON
wb.job_id = pj.id
JOIN
templates AS t ON
w.template_id = t.id
WHERE
wb.created_at >= $1
AND pj.completed_at IS NOT NULL
GROUP BY
w.template_id, template_name, template_display_name, template_organization_id
ORDER BY
template_name ASC
`
type GetWorkspaceBuildStatsByTemplatesRow struct {
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
TemplateName string `db:"template_name" json:"template_name"`
TemplateDisplayName string `db:"template_display_name" json:"template_display_name"`
TemplateOrganizationID uuid.UUID `db:"template_organization_id" json:"template_organization_id"`
TotalBuilds int64 `db:"total_builds" json:"total_builds"`
FailedBuilds int64 `db:"failed_builds" json:"failed_builds"`
}
func (q *sqlQuerier) GetWorkspaceBuildStatsByTemplates(ctx context.Context, since time.Time) ([]GetWorkspaceBuildStatsByTemplatesRow, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceBuildStatsByTemplates, since)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetWorkspaceBuildStatsByTemplatesRow
for rows.Next() {
var i GetWorkspaceBuildStatsByTemplatesRow
if err := rows.Scan(
&i.TemplateID,
&i.TemplateName,
&i.TemplateDisplayName,
&i.TemplateOrganizationID,
&i.TotalBuilds,
&i.FailedBuilds,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceBuildsByWorkspaceID = `-- name: GetWorkspaceBuildsByWorkspaceID :many
SELECT
id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, notified_autostop_deadline, initiator_by_avatar_url, initiator_by_username, initiator_by_name
FROM
workspace_build_with_user AS workspace_builds
WHERE
workspace_builds.workspace_id = $1
AND workspace_builds.created_at > $2
AND CASE
-- This allows using the last element on a page as effectively a cursor.
-- This is an important option for scripts that need to paginate without
-- duplicating or missing data.
WHEN $3 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN (
-- The pagination cursor is the last ID of the previous page.
-- The query is ordered by the build_number field, so select all
-- rows after the cursor.
build_number > (
SELECT
build_number
FROM
workspace_builds
WHERE
id = $3
)
)
ELSE true
END
ORDER BY
build_number desc OFFSET $4
LIMIT
-- A null limit means "no limit", so 0 means return all
NULLIF($5 :: int, 0)
`
type GetWorkspaceBuildsByWorkspaceIDParams struct {
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
Since time.Time `db:"since" json:"since"`
AfterID uuid.UUID `db:"after_id" json:"after_id"`
OffsetOpt int32 `db:"offset_opt" json:"offset_opt"`
LimitOpt int32 `db:"limit_opt" json:"limit_opt"`
}
func (q *sqlQuerier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg GetWorkspaceBuildsByWorkspaceIDParams) ([]WorkspaceBuild, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceBuildsByWorkspaceID,
arg.WorkspaceID,
arg.Since,
arg.AfterID,
arg.OffsetOpt,
arg.LimitOpt,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceBuild
for rows.Next() {
var i WorkspaceBuild
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.WorkspaceID,
&i.TemplateVersionID,
&i.BuildNumber,
&i.Transition,
&i.InitiatorID,
&i.JobID,
&i.Deadline,
&i.Reason,
&i.DailyCost,
&i.MaxDeadline,
&i.TemplateVersionPresetID,
&i.HasAITask,
&i.HasExternalAgent,
&i.NotifiedAutostopDeadline,
&i.InitiatorByAvatarUrl,
&i.InitiatorByUsername,
&i.InitiatorByName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceBuildsCreatedAfter = `-- name: GetWorkspaceBuildsCreatedAfter :many
SELECT id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, notified_autostop_deadline, initiator_by_avatar_url, initiator_by_username, initiator_by_name FROM workspace_build_with_user WHERE created_at > $1
`
func (q *sqlQuerier) GetWorkspaceBuildsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceBuild, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceBuildsCreatedAfter, createdAt)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceBuild
for rows.Next() {
var i WorkspaceBuild
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.WorkspaceID,
&i.TemplateVersionID,
&i.BuildNumber,
&i.Transition,
&i.InitiatorID,
&i.JobID,
&i.Deadline,
&i.Reason,
&i.DailyCost,
&i.MaxDeadline,
&i.TemplateVersionPresetID,
&i.HasAITask,
&i.HasExternalAgent,
&i.NotifiedAutostopDeadline,
&i.InitiatorByAvatarUrl,
&i.InitiatorByUsername,
&i.InitiatorByName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertWorkspaceBuild = `-- name: InsertWorkspaceBuild :exec
INSERT INTO
workspace_builds (
id,
created_at,
updated_at,
workspace_id,
template_version_id,
"build_number",
transition,
initiator_id,
job_id,
provisioner_state,
deadline,
max_deadline,
reason,
template_version_preset_id
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
`
type InsertWorkspaceBuildParams struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
BuildNumber int32 `db:"build_number" json:"build_number"`
Transition WorkspaceTransition `db:"transition" json:"transition"`
InitiatorID uuid.UUID `db:"initiator_id" json:"initiator_id"`
JobID uuid.UUID `db:"job_id" json:"job_id"`
ProvisionerState []byte `db:"provisioner_state" json:"provisioner_state"`
Deadline time.Time `db:"deadline" json:"deadline"`
MaxDeadline time.Time `db:"max_deadline" json:"max_deadline"`
Reason BuildReason `db:"reason" json:"reason"`
TemplateVersionPresetID uuid.NullUUID `db:"template_version_preset_id" json:"template_version_preset_id"`
}
func (q *sqlQuerier) InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) error {
_, err := q.db.ExecContext(ctx, insertWorkspaceBuild,
arg.ID,
arg.CreatedAt,
arg.UpdatedAt,
arg.WorkspaceID,
arg.TemplateVersionID,
arg.BuildNumber,
arg.Transition,
arg.InitiatorID,
arg.JobID,
arg.ProvisionerState,
arg.Deadline,
arg.MaxDeadline,
arg.Reason,
arg.TemplateVersionPresetID,
)
return err
}
const updateWorkspaceBuildCostByID = `-- name: UpdateWorkspaceBuildCostByID :exec
UPDATE
workspace_builds
SET
daily_cost = $2
WHERE
id = $1
`
type UpdateWorkspaceBuildCostByIDParams struct {
ID uuid.UUID `db:"id" json:"id"`
DailyCost int32 `db:"daily_cost" json:"daily_cost"`
}
func (q *sqlQuerier) UpdateWorkspaceBuildCostByID(ctx context.Context, arg UpdateWorkspaceBuildCostByIDParams) error {
_, err := q.db.ExecContext(ctx, updateWorkspaceBuildCostByID, arg.ID, arg.DailyCost)
return err
}
const updateWorkspaceBuildDeadlineByID = `-- name: UpdateWorkspaceBuildDeadlineByID :exec
UPDATE
workspace_builds
SET
deadline = $1::timestamptz,
max_deadline = $2::timestamptz,
updated_at = $3::timestamptz
FROM
workspaces
WHERE
workspace_builds.id = $4::uuid
AND workspace_builds.workspace_id = workspaces.id
-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
-- are managed by the reconciliation loop, not the lifecycle executor which handles
-- deadline and max_deadline
AND workspaces.owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID
`
type UpdateWorkspaceBuildDeadlineByIDParams struct {
Deadline time.Time `db:"deadline" json:"deadline"`
MaxDeadline time.Time `db:"max_deadline" json:"max_deadline"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateWorkspaceBuildDeadlineByID(ctx context.Context, arg UpdateWorkspaceBuildDeadlineByIDParams) error {
_, err := q.db.ExecContext(ctx, updateWorkspaceBuildDeadlineByID,
arg.Deadline,
arg.MaxDeadline,
arg.UpdatedAt,
arg.ID,
)
return err
}
const updateWorkspaceBuildFlagsByID = `-- name: UpdateWorkspaceBuildFlagsByID :exec
UPDATE
workspace_builds
SET
has_ai_task = $1,
has_external_agent = $2,
updated_at = $3::timestamptz
WHERE id = $4::uuid
`
type UpdateWorkspaceBuildFlagsByIDParams struct {
HasAITask sql.NullBool `db:"has_ai_task" json:"has_ai_task"`
HasExternalAgent sql.NullBool `db:"has_external_agent" json:"has_external_agent"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateWorkspaceBuildFlagsByID(ctx context.Context, arg UpdateWorkspaceBuildFlagsByIDParams) error {
_, err := q.db.ExecContext(ctx, updateWorkspaceBuildFlagsByID,
arg.HasAITask,
arg.HasExternalAgent,
arg.UpdatedAt,
arg.ID,
)
return err
}
const updateWorkspaceBuildNotifiedAutostopDeadline = `-- name: UpdateWorkspaceBuildNotifiedAutostopDeadline :exec
UPDATE
workspace_builds
SET
notified_autostop_deadline = $1::timestamptz,
updated_at = $2::timestamptz
WHERE id = $3::uuid
`
type UpdateWorkspaceBuildNotifiedAutostopDeadlineParams struct {
NotifiedAutostopDeadline time.Time `db:"notified_autostop_deadline" json:"notified_autostop_deadline"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
ID uuid.UUID `db:"id" json:"id"`
}
// Stamps the deadline value that an autostop reminder was last sent for. Once
// this equals the build's deadline the reminder is considered handled and the
// lifecycle executor will not send another for this deadline, which makes the
// reminder idempotent and HA-safe. It re-arms automatically when the deadline
// changes (e.g. an activity bump).
func (q *sqlQuerier) UpdateWorkspaceBuildNotifiedAutostopDeadline(ctx context.Context, arg UpdateWorkspaceBuildNotifiedAutostopDeadlineParams) error {
_, err := q.db.ExecContext(ctx, updateWorkspaceBuildNotifiedAutostopDeadline, arg.NotifiedAutostopDeadline, arg.UpdatedAt, arg.ID)
return err
}
const updateWorkspaceBuildProvisionerStateByID = `-- name: UpdateWorkspaceBuildProvisionerStateByID :exec
UPDATE
workspace_builds
SET
provisioner_state = $1::bytea,
updated_at = $2::timestamptz
WHERE id = $3::uuid
`
type UpdateWorkspaceBuildProvisionerStateByIDParams struct {
ProvisionerState []byte `db:"provisioner_state" json:"provisioner_state"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateWorkspaceBuildProvisionerStateByID(ctx context.Context, arg UpdateWorkspaceBuildProvisionerStateByIDParams) error {
_, err := q.db.ExecContext(ctx, updateWorkspaceBuildProvisionerStateByID, arg.ProvisionerState, arg.UpdatedAt, arg.ID)
return err
}
const getWorkspaceModulesByJobID = `-- name: GetWorkspaceModulesByJobID :many
SELECT
id, job_id, transition, source, version, key, created_at
FROM
workspace_modules
WHERE
job_id = $1
`
func (q *sqlQuerier) GetWorkspaceModulesByJobID(ctx context.Context, jobID uuid.UUID) ([]WorkspaceModule, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceModulesByJobID, jobID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceModule
for rows.Next() {
var i WorkspaceModule
if err := rows.Scan(
&i.ID,
&i.JobID,
&i.Transition,
&i.Source,
&i.Version,
&i.Key,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceModulesCreatedAfter = `-- name: GetWorkspaceModulesCreatedAfter :many
SELECT id, job_id, transition, source, version, key, created_at FROM workspace_modules WHERE created_at > $1
`
func (q *sqlQuerier) GetWorkspaceModulesCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceModule, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceModulesCreatedAfter, createdAt)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceModule
for rows.Next() {
var i WorkspaceModule
if err := rows.Scan(
&i.ID,
&i.JobID,
&i.Transition,
&i.Source,
&i.Version,
&i.Key,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertWorkspaceModule = `-- name: InsertWorkspaceModule :one
INSERT INTO
workspace_modules (id, job_id, transition, source, version, key, created_at)
VALUES
($1, $2, $3, $4, $5, $6, $7) RETURNING id, job_id, transition, source, version, key, created_at
`
type InsertWorkspaceModuleParams struct {
ID uuid.UUID `db:"id" json:"id"`
JobID uuid.UUID `db:"job_id" json:"job_id"`
Transition WorkspaceTransition `db:"transition" json:"transition"`
Source string `db:"source" json:"source"`
Version string `db:"version" json:"version"`
Key string `db:"key" json:"key"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
func (q *sqlQuerier) InsertWorkspaceModule(ctx context.Context, arg InsertWorkspaceModuleParams) (WorkspaceModule, error) {
row := q.db.QueryRowContext(ctx, insertWorkspaceModule,
arg.ID,
arg.JobID,
arg.Transition,
arg.Source,
arg.Version,
arg.Key,
arg.CreatedAt,
)
var i WorkspaceModule
err := row.Scan(
&i.ID,
&i.JobID,
&i.Transition,
&i.Source,
&i.Version,
&i.Key,
&i.CreatedAt,
)
return i, err
}
const getWorkspaceResourceByID = `-- name: GetWorkspaceResourceByID :one
SELECT
id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost, module_path
FROM
workspace_resources
WHERE
id = $1
`
func (q *sqlQuerier) GetWorkspaceResourceByID(ctx context.Context, id uuid.UUID) (WorkspaceResource, error) {
row := q.db.QueryRowContext(ctx, getWorkspaceResourceByID, id)
var i WorkspaceResource
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.JobID,
&i.Transition,
&i.Type,
&i.Name,
&i.Hide,
&i.Icon,
&i.InstanceType,
&i.DailyCost,
&i.ModulePath,
)
return i, err
}
const getWorkspaceResourceMetadataByResourceIDs = `-- name: GetWorkspaceResourceMetadataByResourceIDs :many
SELECT
workspace_resource_id, key, value, sensitive, id
FROM
workspace_resource_metadata
WHERE
workspace_resource_id = ANY($1 :: uuid [ ]) ORDER BY id ASC
`
func (q *sqlQuerier) GetWorkspaceResourceMetadataByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceResourceMetadatum, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceResourceMetadataByResourceIDs, pq.Array(ids))
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceResourceMetadatum
for rows.Next() {
var i WorkspaceResourceMetadatum
if err := rows.Scan(
&i.WorkspaceResourceID,
&i.Key,
&i.Value,
&i.Sensitive,
&i.ID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceResourceMetadataCreatedAfter = `-- name: GetWorkspaceResourceMetadataCreatedAfter :many
SELECT workspace_resource_id, key, value, sensitive, id FROM workspace_resource_metadata WHERE workspace_resource_id = ANY(
SELECT id FROM workspace_resources WHERE created_at > $1
)
`
func (q *sqlQuerier) GetWorkspaceResourceMetadataCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceResourceMetadatum, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceResourceMetadataCreatedAfter, createdAt)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceResourceMetadatum
for rows.Next() {
var i WorkspaceResourceMetadatum
if err := rows.Scan(
&i.WorkspaceResourceID,
&i.Key,
&i.Value,
&i.Sensitive,
&i.ID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceResourcesByJobID = `-- name: GetWorkspaceResourcesByJobID :many
SELECT
id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost, module_path
FROM
workspace_resources
WHERE
job_id = $1
`
func (q *sqlQuerier) GetWorkspaceResourcesByJobID(ctx context.Context, jobID uuid.UUID) ([]WorkspaceResource, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceResourcesByJobID, jobID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceResource
for rows.Next() {
var i WorkspaceResource
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.JobID,
&i.Transition,
&i.Type,
&i.Name,
&i.Hide,
&i.Icon,
&i.InstanceType,
&i.DailyCost,
&i.ModulePath,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceResourcesByJobIDs = `-- name: GetWorkspaceResourcesByJobIDs :many
SELECT
id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost, module_path
FROM
workspace_resources
WHERE
job_id = ANY($1 :: uuid [ ])
`
func (q *sqlQuerier) GetWorkspaceResourcesByJobIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceResource, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceResourcesByJobIDs, pq.Array(ids))
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceResource
for rows.Next() {
var i WorkspaceResource
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.JobID,
&i.Transition,
&i.Type,
&i.Name,
&i.Hide,
&i.Icon,
&i.InstanceType,
&i.DailyCost,
&i.ModulePath,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceResourcesCreatedAfter = `-- name: GetWorkspaceResourcesCreatedAfter :many
SELECT id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost, module_path FROM workspace_resources WHERE created_at > $1
`
func (q *sqlQuerier) GetWorkspaceResourcesCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceResource, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceResourcesCreatedAfter, createdAt)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceResource
for rows.Next() {
var i WorkspaceResource
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.JobID,
&i.Transition,
&i.Type,
&i.Name,
&i.Hide,
&i.Icon,
&i.InstanceType,
&i.DailyCost,
&i.ModulePath,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertWorkspaceResource = `-- name: InsertWorkspaceResource :one
INSERT INTO
workspace_resources (id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost, module_path)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost, module_path
`
type InsertWorkspaceResourceParams struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
JobID uuid.UUID `db:"job_id" json:"job_id"`
Transition WorkspaceTransition `db:"transition" json:"transition"`
Type string `db:"type" json:"type"`
Name string `db:"name" json:"name"`
Hide bool `db:"hide" json:"hide"`
Icon string `db:"icon" json:"icon"`
InstanceType sql.NullString `db:"instance_type" json:"instance_type"`
DailyCost int32 `db:"daily_cost" json:"daily_cost"`
ModulePath sql.NullString `db:"module_path" json:"module_path"`
}
func (q *sqlQuerier) InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error) {
row := q.db.QueryRowContext(ctx, insertWorkspaceResource,
arg.ID,
arg.CreatedAt,
arg.JobID,
arg.Transition,
arg.Type,
arg.Name,
arg.Hide,
arg.Icon,
arg.InstanceType,
arg.DailyCost,
arg.ModulePath,
)
var i WorkspaceResource
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.JobID,
&i.Transition,
&i.Type,
&i.Name,
&i.Hide,
&i.Icon,
&i.InstanceType,
&i.DailyCost,
&i.ModulePath,
)
return i, err
}
const insertWorkspaceResourceMetadata = `-- name: InsertWorkspaceResourceMetadata :many
INSERT INTO
workspace_resource_metadata
SELECT
$1 :: uuid AS workspace_resource_id,
unnest($2 :: text [ ]) AS key,
unnest($3 :: text [ ]) AS value,
unnest($4 :: boolean [ ]) AS sensitive RETURNING workspace_resource_id, key, value, sensitive, id
`
type InsertWorkspaceResourceMetadataParams struct {
WorkspaceResourceID uuid.UUID `db:"workspace_resource_id" json:"workspace_resource_id"`
Key []string `db:"key" json:"key"`
Value []string `db:"value" json:"value"`
Sensitive []bool `db:"sensitive" json:"sensitive"`
}
func (q *sqlQuerier) InsertWorkspaceResourceMetadata(ctx context.Context, arg InsertWorkspaceResourceMetadataParams) ([]WorkspaceResourceMetadatum, error) {
rows, err := q.db.QueryContext(ctx, insertWorkspaceResourceMetadata,
arg.WorkspaceResourceID,
pq.Array(arg.Key),
pq.Array(arg.Value),
pq.Array(arg.Sensitive),
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceResourceMetadatum
for rows.Next() {
var i WorkspaceResourceMetadatum
if err := rows.Scan(
&i.WorkspaceResourceID,
&i.Key,
&i.Value,
&i.Sensitive,
&i.ID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const batchUpdateWorkspaceLastUsedAt = `-- name: BatchUpdateWorkspaceLastUsedAt :exec
UPDATE
workspaces
SET
last_used_at = $1
WHERE
id = ANY($2 :: uuid[])
AND
-- Do not overwrite with older data
last_used_at < $1
`
type BatchUpdateWorkspaceLastUsedAtParams struct {
LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"`
IDs []uuid.UUID `db:"ids" json:"ids"`
}
func (q *sqlQuerier) BatchUpdateWorkspaceLastUsedAt(ctx context.Context, arg BatchUpdateWorkspaceLastUsedAtParams) error {
_, err := q.db.ExecContext(ctx, batchUpdateWorkspaceLastUsedAt, arg.LastUsedAt, pq.Array(arg.IDs))
return err
}
const batchUpdateWorkspaceNextStartAt = `-- name: BatchUpdateWorkspaceNextStartAt :exec
UPDATE
workspaces
SET
next_start_at = CASE
WHEN batch.next_start_at = '0001-01-01 00:00:00+00'::timestamptz THEN NULL
ELSE batch.next_start_at
END
FROM (
SELECT
unnest($1::uuid[]) AS id,
unnest($2::timestamptz[]) AS next_start_at
) AS batch
WHERE
workspaces.id = batch.id
`
type BatchUpdateWorkspaceNextStartAtParams struct {
IDs []uuid.UUID `db:"ids" json:"ids"`
NextStartAts []time.Time `db:"next_start_ats" json:"next_start_ats"`
}
func (q *sqlQuerier) BatchUpdateWorkspaceNextStartAt(ctx context.Context, arg BatchUpdateWorkspaceNextStartAtParams) error {
_, err := q.db.ExecContext(ctx, batchUpdateWorkspaceNextStartAt, pq.Array(arg.IDs), pq.Array(arg.NextStartAts))
return err
}
const deleteWorkspaceACLByID = `-- name: DeleteWorkspaceACLByID :exec
UPDATE
workspaces
SET
group_acl = '{}'::json,
user_acl = '{}'::json
WHERE
id = $1
`
func (q *sqlQuerier) DeleteWorkspaceACLByID(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteWorkspaceACLByID, id)
return err
}
const deleteWorkspaceACLsByOrganization = `-- name: DeleteWorkspaceACLsByOrganization :exec
UPDATE
workspaces
SET
group_acl = '{}'::jsonb,
user_acl = '{}'::jsonb
WHERE
organization_id = $1
AND (
NOT $2::boolean
OR owner_id NOT IN (
SELECT id FROM users WHERE is_service_account = true
)
)
`
type DeleteWorkspaceACLsByOrganizationParams struct {
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
ExcludeServiceAccounts bool `db:"exclude_service_accounts" json:"exclude_service_accounts"`
}
func (q *sqlQuerier) DeleteWorkspaceACLsByOrganization(ctx context.Context, arg DeleteWorkspaceACLsByOrganizationParams) error {
_, err := q.db.ExecContext(ctx, deleteWorkspaceACLsByOrganization, arg.OrganizationID, arg.ExcludeServiceAccounts)
return err
}
const favoriteWorkspace = `-- name: FavoriteWorkspace :exec
UPDATE workspaces SET favorite = true WHERE id = $1
`
func (q *sqlQuerier) FavoriteWorkspace(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, favoriteWorkspace, id)
return err
}
const getDeploymentWorkspaceStats = `-- name: GetDeploymentWorkspaceStats :one
WITH workspaces_with_jobs AS (
SELECT
latest_build.transition, latest_build.provisioner_job_id, latest_build.started_at, latest_build.updated_at, latest_build.canceled_at, latest_build.completed_at, latest_build.error FROM workspaces
LEFT JOIN LATERAL (
SELECT
workspace_builds.transition,
provisioner_jobs.id AS provisioner_job_id,
provisioner_jobs.started_at,
provisioner_jobs.updated_at,
provisioner_jobs.canceled_at,
provisioner_jobs.completed_at,
provisioner_jobs.error
FROM
workspace_builds
LEFT JOIN
provisioner_jobs
ON
provisioner_jobs.id = workspace_builds.job_id
WHERE
workspace_builds.workspace_id = workspaces.id
ORDER BY
build_number DESC
LIMIT
1
) latest_build ON TRUE WHERE deleted = false
), pending_workspaces AS (
SELECT COUNT(*) AS count FROM workspaces_with_jobs WHERE
started_at IS NULL
), building_workspaces AS (
SELECT COUNT(*) AS count FROM workspaces_with_jobs WHERE
started_at IS NOT NULL AND
canceled_at IS NULL AND
completed_at IS NULL AND
updated_at - INTERVAL '30 seconds' < NOW()
), running_workspaces AS (
SELECT COUNT(*) AS count FROM workspaces_with_jobs WHERE
completed_at IS NOT NULL AND
canceled_at IS NULL AND
error IS NULL AND
transition = 'start'::workspace_transition
), failed_workspaces AS (
SELECT COUNT(*) AS count FROM workspaces_with_jobs WHERE
(canceled_at IS NOT NULL AND
error IS NOT NULL) OR
(completed_at IS NOT NULL AND
error IS NOT NULL)
), stopped_workspaces AS (
SELECT COUNT(*) AS count FROM workspaces_with_jobs WHERE
completed_at IS NOT NULL AND
canceled_at IS NULL AND
error IS NULL AND
transition = 'stop'::workspace_transition
)
SELECT
pending_workspaces.count AS pending_workspaces,
building_workspaces.count AS building_workspaces,
running_workspaces.count AS running_workspaces,
failed_workspaces.count AS failed_workspaces,
stopped_workspaces.count AS stopped_workspaces
FROM pending_workspaces, building_workspaces, running_workspaces, failed_workspaces, stopped_workspaces
`
type GetDeploymentWorkspaceStatsRow struct {
PendingWorkspaces int64 `db:"pending_workspaces" json:"pending_workspaces"`
BuildingWorkspaces int64 `db:"building_workspaces" json:"building_workspaces"`
RunningWorkspaces int64 `db:"running_workspaces" json:"running_workspaces"`
FailedWorkspaces int64 `db:"failed_workspaces" json:"failed_workspaces"`
StoppedWorkspaces int64 `db:"stopped_workspaces" json:"stopped_workspaces"`
}
func (q *sqlQuerier) GetDeploymentWorkspaceStats(ctx context.Context) (GetDeploymentWorkspaceStatsRow, error) {
row := q.db.QueryRowContext(ctx, getDeploymentWorkspaceStats)
var i GetDeploymentWorkspaceStatsRow
err := row.Scan(
&i.PendingWorkspaces,
&i.BuildingWorkspaces,
&i.RunningWorkspaces,
&i.FailedWorkspaces,
&i.StoppedWorkspaces,
)
return i, err
}
const getRegularWorkspaceCreateMetrics = `-- name: GetRegularWorkspaceCreateMetrics :many
WITH first_success_build AS (
-- Earliest successful 'start' build per workspace
SELECT DISTINCT ON (wb.workspace_id)
wb.workspace_id,
wb.template_version_preset_id,
wb.initiator_id
FROM workspace_builds wb
JOIN provisioner_jobs pj ON pj.id = wb.job_id
WHERE
wb.transition = 'start'::workspace_transition
AND pj.job_status = 'succeeded'::provisioner_job_status
ORDER BY wb.workspace_id, wb.build_number, wb.id
)
SELECT
t.name AS template_name,
COALESCE(tvp.name, '') AS preset_name,
o.name AS organization_name,
COUNT(*) AS created_count
FROM first_success_build fsb
JOIN workspaces w ON w.id = fsb.workspace_id
JOIN templates t ON t.id = w.template_id
LEFT JOIN template_version_presets tvp ON tvp.id = fsb.template_version_preset_id
JOIN organizations o ON o.id = w.organization_id
WHERE
NOT t.deleted
-- Exclude workspaces whose first successful start was the prebuilds system user
AND fsb.initiator_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid
GROUP BY t.name, COALESCE(tvp.name, ''), o.name
ORDER BY t.name, preset_name, o.name
`
type GetRegularWorkspaceCreateMetricsRow struct {
TemplateName string `db:"template_name" json:"template_name"`
PresetName string `db:"preset_name" json:"preset_name"`
OrganizationName string `db:"organization_name" json:"organization_name"`
CreatedCount int64 `db:"created_count" json:"created_count"`
}
// Count regular workspaces: only those whose first successful 'start' build
// was not initiated by the prebuild system user.
func (q *sqlQuerier) GetRegularWorkspaceCreateMetrics(ctx context.Context) ([]GetRegularWorkspaceCreateMetricsRow, error) {
rows, err := q.db.QueryContext(ctx, getRegularWorkspaceCreateMetrics)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetRegularWorkspaceCreateMetricsRow
for rows.Next() {
var i GetRegularWorkspaceCreateMetricsRow
if err := rows.Scan(
&i.TemplateName,
&i.PresetName,
&i.OrganizationName,
&i.CreatedCount,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getTemplateRankingSignalsByOwnerID = `-- name: GetTemplateRankingSignalsByOwnerID :many
WITH org_usage AS (
-- Distinct developers with a non-deleted workspace; the prebuilds system
-- user is excluded so unclaimed prebuilds do not inflate popularity.
SELECT
w.template_id,
COUNT(DISTINCT w.owner_id) AS org_devs
FROM
workspaces w
WHERE
w.template_id = ANY($1 :: uuid[])
AND NOT w.deleted
AND w.owner_id != $2 :: uuid
AND CASE
WHEN $3 :: uuid != '00000000-0000-0000-0000-000000000000' :: uuid THEN
w.organization_id = $3
ELSE true
END
GROUP BY
w.template_id
),
user_usage AS (
-- The owner's workspaces used within the lookback window, split into
-- active and recently-deleted counts.
SELECT
w.template_id,
COUNT(*) FILTER (WHERE NOT w.deleted) AS active_count,
COUNT(*) FILTER (WHERE w.deleted) AS deleted_recent_count,
MAX(w.last_used_at) :: timestamptz AS last_used_at
FROM
workspaces w
WHERE
w.owner_id = $4
AND w.template_id = ANY($1 :: uuid[])
AND w.last_used_at > $5 :: timestamptz
AND CASE
WHEN $3 :: uuid != '00000000-0000-0000-0000-000000000000' :: uuid THEN
w.organization_id = $3
ELSE true
END
GROUP BY
w.template_id
)
SELECT
t.template_id :: uuid AS template_id,
COALESCE(u.active_count, 0) :: bigint AS active_count,
COALESCE(u.deleted_recent_count, 0) :: bigint AS deleted_recent_count,
u.last_used_at,
COALESCE(o.org_devs, 0) :: bigint AS org_devs
FROM
unnest($1 :: uuid[]) AS t(template_id)
LEFT JOIN user_usage u ON u.template_id = t.template_id
LEFT JOIN org_usage o ON o.template_id = t.template_id
`
type GetTemplateRankingSignalsByOwnerIDParams struct {
TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
PrebuildsUserID uuid.UUID `db:"prebuilds_user_id" json:"prebuilds_user_id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
LookbackCutoff time.Time `db:"lookback_cutoff" json:"lookback_cutoff"`
}
type GetTemplateRankingSignalsByOwnerIDRow struct {
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
ActiveCount int64 `db:"active_count" json:"active_count"`
DeletedRecentCount int64 `db:"deleted_recent_count" json:"deleted_recent_count"`
LastUsedAt sql.NullTime `db:"last_used_at" json:"last_used_at"`
OrgDevs int64 `db:"org_devs" json:"org_devs"`
}
// GetTemplateRankingSignalsByOwnerID returns raw template-ranking signals for
// one owner: in-window active and recently-deleted workspace counts, the last
// in-window usage, and distinct active developers per template. The affinity
// score is computed in Go (see listtemplates.go) so the ranking policy and
// its confidence thresholds live in one place.
func (q *sqlQuerier) GetTemplateRankingSignalsByOwnerID(ctx context.Context, arg GetTemplateRankingSignalsByOwnerIDParams) ([]GetTemplateRankingSignalsByOwnerIDRow, error) {
rows, err := q.db.QueryContext(ctx, getTemplateRankingSignalsByOwnerID,
pq.Array(arg.TemplateIDs),
arg.PrebuildsUserID,
arg.OrganizationID,
arg.OwnerID,
arg.LookbackCutoff,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetTemplateRankingSignalsByOwnerIDRow
for rows.Next() {
var i GetTemplateRankingSignalsByOwnerIDRow
if err := rows.Scan(
&i.TemplateID,
&i.ActiveCount,
&i.DeletedRecentCount,
&i.LastUsedAt,
&i.OrgDevs,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceACLByID = `-- name: GetWorkspaceACLByID :one
SELECT
group_acl as groups,
user_acl as users
FROM
workspaces
WHERE
id = $1
`
type GetWorkspaceACLByIDRow struct {
Groups WorkspaceACL `db:"groups" json:"groups"`
Users WorkspaceACL `db:"users" json:"users"`
}
func (q *sqlQuerier) GetWorkspaceACLByID(ctx context.Context, id uuid.UUID) (GetWorkspaceACLByIDRow, error) {
row := q.db.QueryRowContext(ctx, getWorkspaceACLByID, id)
var i GetWorkspaceACLByIDRow
err := row.Scan(&i.Groups, &i.Users)
return i, err
}
const getWorkspaceByAgentID = `-- name: GetWorkspaceByAgentID :one
SELECT
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description, task_id, group_acl_display_info, user_acl_display_info
FROM
workspaces_expanded as workspaces
WHERE
workspaces.id = (
SELECT
workspace_id
FROM
workspace_builds
WHERE
workspace_builds.job_id = (
SELECT
job_id
FROM
workspace_resources
WHERE
workspace_resources.id = (
SELECT
resource_id
FROM
workspace_agents
WHERE
workspace_agents.id = $1
)
)
)
`
func (q *sqlQuerier) GetWorkspaceByAgentID(ctx context.Context, agentID uuid.UUID) (Workspace, error) {
row := q.db.QueryRowContext(ctx, getWorkspaceByAgentID, agentID)
var i Workspace
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.OwnerID,
&i.OrganizationID,
&i.TemplateID,
&i.Deleted,
&i.Name,
&i.AutostartSchedule,
&i.Ttl,
&i.LastUsedAt,
&i.DormantAt,
&i.DeletingAt,
&i.AutomaticUpdates,
&i.Favorite,
&i.NextStartAt,
&i.GroupACL,
&i.UserACL,
&i.OwnerAvatarUrl,
&i.OwnerUsername,
&i.OwnerName,
&i.OrganizationName,
&i.OrganizationDisplayName,
&i.OrganizationIcon,
&i.OrganizationDescription,
&i.TemplateName,
&i.TemplateDisplayName,
&i.TemplateIcon,
&i.TemplateDescription,
&i.TaskID,
&i.GroupACLDisplayInfo,
&i.UserACLDisplayInfo,
)
return i, err
}
const getWorkspaceByID = `-- name: GetWorkspaceByID :one
SELECT
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description, task_id, group_acl_display_info, user_acl_display_info
FROM
workspaces_expanded
WHERE
id = $1
LIMIT
1
`
func (q *sqlQuerier) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Workspace, error) {
row := q.db.QueryRowContext(ctx, getWorkspaceByID, id)
var i Workspace
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.OwnerID,
&i.OrganizationID,
&i.TemplateID,
&i.Deleted,
&i.Name,
&i.AutostartSchedule,
&i.Ttl,
&i.LastUsedAt,
&i.DormantAt,
&i.DeletingAt,
&i.AutomaticUpdates,
&i.Favorite,
&i.NextStartAt,
&i.GroupACL,
&i.UserACL,
&i.OwnerAvatarUrl,
&i.OwnerUsername,
&i.OwnerName,
&i.OrganizationName,
&i.OrganizationDisplayName,
&i.OrganizationIcon,
&i.OrganizationDescription,
&i.TemplateName,
&i.TemplateDisplayName,
&i.TemplateIcon,
&i.TemplateDescription,
&i.TaskID,
&i.GroupACLDisplayInfo,
&i.UserACLDisplayInfo,
)
return i, err
}
const getWorkspaceByOwnerIDAndName = `-- name: GetWorkspaceByOwnerIDAndName :one
SELECT
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description, task_id, group_acl_display_info, user_acl_display_info
FROM
workspaces_expanded as workspaces
WHERE
owner_id = $1
AND deleted = $2
AND LOWER("name") = LOWER($3)
ORDER BY created_at DESC
`
type GetWorkspaceByOwnerIDAndNameParams struct {
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
Deleted bool `db:"deleted" json:"deleted"`
Name string `db:"name" json:"name"`
}
func (q *sqlQuerier) GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWorkspaceByOwnerIDAndNameParams) (Workspace, error) {
row := q.db.QueryRowContext(ctx, getWorkspaceByOwnerIDAndName, arg.OwnerID, arg.Deleted, arg.Name)
var i Workspace
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.OwnerID,
&i.OrganizationID,
&i.TemplateID,
&i.Deleted,
&i.Name,
&i.AutostartSchedule,
&i.Ttl,
&i.LastUsedAt,
&i.DormantAt,
&i.DeletingAt,
&i.AutomaticUpdates,
&i.Favorite,
&i.NextStartAt,
&i.GroupACL,
&i.UserACL,
&i.OwnerAvatarUrl,
&i.OwnerUsername,
&i.OwnerName,
&i.OrganizationName,
&i.OrganizationDisplayName,
&i.OrganizationIcon,
&i.OrganizationDescription,
&i.TemplateName,
&i.TemplateDisplayName,
&i.TemplateIcon,
&i.TemplateDescription,
&i.TaskID,
&i.GroupACLDisplayInfo,
&i.UserACLDisplayInfo,
)
return i, err
}
const getWorkspaceByResourceID = `-- name: GetWorkspaceByResourceID :one
SELECT
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description, task_id, group_acl_display_info, user_acl_display_info
FROM
workspaces_expanded as workspaces
WHERE
workspaces.id = (
SELECT
workspace_id
FROM
workspace_builds
WHERE
workspace_builds.job_id = (
SELECT
job_id
FROM
workspace_resources
WHERE
workspace_resources.id = $1
)
)
LIMIT
1
`
func (q *sqlQuerier) GetWorkspaceByResourceID(ctx context.Context, resourceID uuid.UUID) (Workspace, error) {
row := q.db.QueryRowContext(ctx, getWorkspaceByResourceID, resourceID)
var i Workspace
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.OwnerID,
&i.OrganizationID,
&i.TemplateID,
&i.Deleted,
&i.Name,
&i.AutostartSchedule,
&i.Ttl,
&i.LastUsedAt,
&i.DormantAt,
&i.DeletingAt,
&i.AutomaticUpdates,
&i.Favorite,
&i.NextStartAt,
&i.GroupACL,
&i.UserACL,
&i.OwnerAvatarUrl,
&i.OwnerUsername,
&i.OwnerName,
&i.OrganizationName,
&i.OrganizationDisplayName,
&i.OrganizationIcon,
&i.OrganizationDescription,
&i.TemplateName,
&i.TemplateDisplayName,
&i.TemplateIcon,
&i.TemplateDescription,
&i.TaskID,
&i.GroupACLDisplayInfo,
&i.UserACLDisplayInfo,
)
return i, err
}
const getWorkspaceByWorkspaceAppID = `-- name: GetWorkspaceByWorkspaceAppID :one
SELECT
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description, task_id, group_acl_display_info, user_acl_display_info
FROM
workspaces_expanded as workspaces
WHERE
workspaces.id = (
SELECT
workspace_id
FROM
workspace_builds
WHERE
workspace_builds.job_id = (
SELECT
job_id
FROM
workspace_resources
WHERE
workspace_resources.id = (
SELECT
resource_id
FROM
workspace_agents
WHERE
workspace_agents.id = (
SELECT
agent_id
FROM
workspace_apps
WHERE
workspace_apps.id = $1
)
)
)
)
`
func (q *sqlQuerier) GetWorkspaceByWorkspaceAppID(ctx context.Context, workspaceAppID uuid.UUID) (Workspace, error) {
row := q.db.QueryRowContext(ctx, getWorkspaceByWorkspaceAppID, workspaceAppID)
var i Workspace
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.OwnerID,
&i.OrganizationID,
&i.TemplateID,
&i.Deleted,
&i.Name,
&i.AutostartSchedule,
&i.Ttl,
&i.LastUsedAt,
&i.DormantAt,
&i.DeletingAt,
&i.AutomaticUpdates,
&i.Favorite,
&i.NextStartAt,
&i.GroupACL,
&i.UserACL,
&i.OwnerAvatarUrl,
&i.OwnerUsername,
&i.OwnerName,
&i.OrganizationName,
&i.OrganizationDisplayName,
&i.OrganizationIcon,
&i.OrganizationDescription,
&i.TemplateName,
&i.TemplateDisplayName,
&i.TemplateIcon,
&i.TemplateDescription,
&i.TaskID,
&i.GroupACLDisplayInfo,
&i.UserACLDisplayInfo,
)
return i, err
}
const getWorkspaceUniqueOwnerCountByTemplateIDs = `-- name: GetWorkspaceUniqueOwnerCountByTemplateIDs :many
SELECT templates.id AS template_id, COUNT(DISTINCT workspaces.owner_id) AS unique_owners_sum
FROM templates
LEFT JOIN workspaces ON workspaces.template_id = templates.id AND workspaces.deleted = false
WHERE templates.id = ANY($1 :: uuid[])
GROUP BY templates.id
`
type GetWorkspaceUniqueOwnerCountByTemplateIDsRow struct {
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
UniqueOwnersSum int64 `db:"unique_owners_sum" json:"unique_owners_sum"`
}
func (q *sqlQuerier) GetWorkspaceUniqueOwnerCountByTemplateIDs(ctx context.Context, templateIds []uuid.UUID) ([]GetWorkspaceUniqueOwnerCountByTemplateIDsRow, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceUniqueOwnerCountByTemplateIDs, pq.Array(templateIds))
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetWorkspaceUniqueOwnerCountByTemplateIDsRow
for rows.Next() {
var i GetWorkspaceUniqueOwnerCountByTemplateIDsRow
if err := rows.Scan(&i.TemplateID, &i.UniqueOwnersSum); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaces = `-- name: GetWorkspaces :many
WITH
build_params AS (
SELECT
LOWER(unnest($2 :: text[])) AS name,
LOWER(unnest($3 :: text[])) AS value
),
filtered_workspaces AS (
SELECT
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at, workspaces.group_acl, workspaces.user_acl, workspaces.owner_avatar_url, workspaces.owner_username, workspaces.owner_name, workspaces.organization_name, workspaces.organization_display_name, workspaces.organization_icon, workspaces.organization_description, workspaces.template_name, workspaces.template_display_name, workspaces.template_icon, workspaces.template_description, workspaces.task_id, workspaces.group_acl_display_info, workspaces.user_acl_display_info,
latest_build.template_version_id,
latest_build.template_version_name,
latest_build.completed_at as latest_build_completed_at,
latest_build.canceled_at as latest_build_canceled_at,
latest_build.error as latest_build_error,
latest_build.transition as latest_build_transition,
latest_build.job_status as latest_build_status,
latest_build.has_external_agent as latest_build_has_external_agent,
latest_build.provisioner_job_id as latest_build_provisioner_job_id
FROM
workspaces_expanded as workspaces
JOIN
users
ON
workspaces.owner_id = users.id
LEFT JOIN LATERAL (
SELECT
workspace_builds.id,
workspace_builds.transition,
workspace_builds.template_version_id,
workspace_builds.has_ai_task,
workspace_builds.has_external_agent,
template_versions.name AS template_version_name,
provisioner_jobs.id AS provisioner_job_id,
provisioner_jobs.started_at,
provisioner_jobs.updated_at,
provisioner_jobs.canceled_at,
provisioner_jobs.completed_at,
provisioner_jobs.error,
provisioner_jobs.job_status
FROM
workspace_builds
JOIN
provisioner_jobs
ON
provisioner_jobs.id = workspace_builds.job_id
LEFT JOIN
template_versions
ON
template_versions.id = workspace_builds.template_version_id
WHERE
workspace_builds.workspace_id = workspaces.id
ORDER BY
build_number DESC
LIMIT
1
) latest_build ON TRUE
LEFT JOIN LATERAL (
SELECT
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, time_til_autostop_notify, agents_allowed
FROM
templates
WHERE
templates.id = workspaces.template_id
) template ON true
WHERE
-- Optionally include deleted workspaces
workspaces.deleted = $4
AND CASE
WHEN $5 :: text != '' THEN
CASE
-- Some workspace specific status refer to the transition
-- type. By default, the standard provisioner job status
-- search strings are supported.
-- 'running' states
WHEN $5 = 'starting' THEN
latest_build.job_status = 'running'::provisioner_job_status AND
latest_build.transition = 'start'::workspace_transition
WHEN $5 = 'stopping' THEN
latest_build.job_status = 'running'::provisioner_job_status AND
latest_build.transition = 'stop'::workspace_transition
WHEN $5 = 'deleting' THEN
latest_build.job_status = 'running' AND
latest_build.transition = 'delete'::workspace_transition
-- 'succeeded' states
WHEN $5 = 'deleted' THEN
latest_build.job_status = 'succeeded'::provisioner_job_status AND
latest_build.transition = 'delete'::workspace_transition
WHEN $5 = 'stopped' THEN
latest_build.job_status = 'succeeded'::provisioner_job_status AND
latest_build.transition = 'stop'::workspace_transition
WHEN $5 = 'started' THEN
latest_build.job_status = 'succeeded'::provisioner_job_status AND
latest_build.transition = 'start'::workspace_transition
-- Special case where the provisioner status and workspace status
-- differ. A workspace is "running" if the job is "succeeded" and
-- the transition is "start". This is because a workspace starts
-- running when a job is complete.
WHEN $5 = 'running' THEN
latest_build.job_status = 'succeeded'::provisioner_job_status AND
latest_build.transition = 'start'::workspace_transition
WHEN $5 != '' THEN
-- By default just match the job status exactly
latest_build.job_status = $5::provisioner_job_status
ELSE
true
END
ELSE true
END
-- Filter by owner_id
AND CASE
WHEN $6 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
workspaces.owner_id = $6
ELSE true
END
-- Filter by organization_id
AND CASE
WHEN $7 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
workspaces.organization_id = $7
ELSE true
END
-- Filter by build parameter
-- @has_param will match any build that includes the parameter.
AND CASE WHEN array_length($8 :: text[], 1) > 0 THEN
EXISTS (
SELECT
1
FROM
workspace_build_parameters
WHERE
workspace_build_parameters.workspace_build_id = latest_build.id AND
-- ILIKE is case insensitive
workspace_build_parameters.name ILIKE ANY($8)
)
ELSE true
END
-- @param_value will match param name an value.
-- requires 2 arrays, @param_names and @param_values to be passed in.
-- Array index must match between the 2 arrays for name=value
AND CASE WHEN array_length($2 :: text[], 1) > 0 THEN
EXISTS (
SELECT
1
FROM
workspace_build_parameters
INNER JOIN
build_params
ON
LOWER(workspace_build_parameters.name) = build_params.name AND
LOWER(workspace_build_parameters.value) = build_params.value AND
workspace_build_parameters.workspace_build_id = latest_build.id
)
ELSE true
END
-- Filter by owner_name
AND CASE
WHEN $9 :: text != '' THEN
workspaces.owner_id = (SELECT id FROM users WHERE lower(users.username) = lower($9) AND deleted = false)
ELSE true
END
-- Filter by template_name
-- There can be more than 1 template with the same name across organizations.
-- Use the organization filter to restrict to 1 org if needed.
AND CASE
WHEN $10 :: text != '' THEN
workspaces.template_id = ANY(SELECT id FROM templates WHERE lower(name) = lower($10) AND deleted = false)
ELSE true
END
-- Filter by template_ids
AND CASE
WHEN array_length($11 :: uuid[], 1) > 0 THEN
workspaces.template_id = ANY($11)
ELSE true
END
-- Filter by workspace_ids
AND CASE
WHEN array_length($12 :: uuid[], 1) > 0 THEN
workspaces.id = ANY($12)
ELSE true
END
-- Filter by name, matching on substring
AND CASE
WHEN $13 :: text != '' THEN
workspaces.name ILIKE '%' || $13 || '%'
ELSE true
END
-- Filter by agent status
-- has-agent: is only applicable for workspaces in "start" transition. Stopped and deleted workspaces don't have agents.
AND CASE
WHEN array_length($14 :: text[], 1) > 0 THEN
(
SELECT COUNT(*)
FROM
workspace_resources
JOIN
workspace_agents
ON
workspace_agents.resource_id = workspace_resources.id
WHERE
workspace_resources.job_id = latest_build.provisioner_job_id AND
latest_build.transition = 'start'::workspace_transition AND
-- Filter out deleted sub agents.
workspace_agents.deleted = FALSE AND
(
CASE
WHEN workspace_agents.first_connected_at IS NULL THEN
CASE
WHEN workspace_agents.connection_timeout_seconds > 0 AND NOW() - workspace_agents.created_at > workspace_agents.connection_timeout_seconds * INTERVAL '1 second' THEN
'timeout'
ELSE
'connecting'
END
WHEN workspace_agents.disconnected_at > workspace_agents.last_connected_at THEN
'disconnected'
WHEN NOW() - workspace_agents.last_connected_at > INTERVAL '1 second' * $15 :: bigint THEN
'disconnected'
WHEN workspace_agents.last_connected_at IS NOT NULL THEN
'connected'
ELSE
NULL
END
) = ANY($14 :: text[])
) > 0
ELSE true
END
-- Filter by dormant workspaces.
AND CASE
WHEN $16 :: boolean != 'false' THEN
dormant_at IS NOT NULL
ELSE true
END
-- Filter by last_used
AND CASE
WHEN $17 :: timestamp with time zone > '0001-01-01 00:00:00Z' THEN
workspaces.last_used_at <= $17
ELSE true
END
AND CASE
WHEN $18 :: timestamp with time zone > '0001-01-01 00:00:00Z' THEN
workspaces.last_used_at >= $18
ELSE true
END
AND CASE
WHEN $19 :: boolean IS NOT NULL THEN
(latest_build.template_version_id = template.active_version_id) = $19 :: boolean
ELSE true
END
-- Filter by has_ai_task, checks if this is a task workspace.
AND CASE
WHEN $20::boolean IS NOT NULL
THEN $20::boolean = EXISTS (
SELECT
1
FROM
tasks
WHERE
-- Consider all tasks, deleting a task does not turn the
-- workspace into a non-task workspace.
tasks.workspace_id = workspaces.id
)
ELSE true
END
-- Filter by has_external_agent in latest build
AND CASE
WHEN $21 :: boolean IS NOT NULL THEN
latest_build.has_external_agent = $21 :: boolean
ELSE true
END
-- Filter by shared status
AND CASE
WHEN $22 :: boolean IS NOT NULL THEN
(workspaces.user_acl != '{}'::jsonb OR workspaces.group_acl != '{}'::jsonb) = $22 :: boolean
ELSE true
END
-- Filter by shared_with_user_id
AND CASE
WHEN $23 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
workspaces.user_acl ? ($23 :: uuid) :: text
ELSE true
END
-- Filter by shared_with_group_id
AND CASE
WHEN $24 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
workspaces.group_acl ? ($24 :: uuid) :: text
ELSE true
END
-- Authorize Filter clause will be injected below in GetAuthorizedWorkspaces
-- @authorize_filter
), filtered_workspaces_order AS (
SELECT
fw.id, fw.created_at, fw.updated_at, fw.owner_id, fw.organization_id, fw.template_id, fw.deleted, fw.name, fw.autostart_schedule, fw.ttl, fw.last_used_at, fw.dormant_at, fw.deleting_at, fw.automatic_updates, fw.favorite, fw.next_start_at, fw.group_acl, fw.user_acl, fw.owner_avatar_url, fw.owner_username, fw.owner_name, fw.organization_name, fw.organization_display_name, fw.organization_icon, fw.organization_description, fw.template_name, fw.template_display_name, fw.template_icon, fw.template_description, fw.task_id, fw.group_acl_display_info, fw.user_acl_display_info, fw.template_version_id, fw.template_version_name, fw.latest_build_completed_at, fw.latest_build_canceled_at, fw.latest_build_error, fw.latest_build_transition, fw.latest_build_status, fw.latest_build_has_external_agent, fw.latest_build_provisioner_job_id
FROM
filtered_workspaces fw
ORDER BY
-- To ensure that 'favorite' workspaces show up first in the list only for their owner.
CASE WHEN favorite AND owner_username = (SELECT users.username FROM users WHERE users.id = $25) THEN 0 ELSE 1 END ASC,
(latest_build_completed_at IS NOT NULL AND
latest_build_canceled_at IS NULL AND
latest_build_error IS NULL AND
latest_build_transition = 'start'::workspace_transition) DESC,
LOWER(owner_username) ASC,
LOWER(name) ASC
LIMIT
CASE
WHEN $27 :: integer > 0 THEN
$27
END
OFFSET
$26
), filtered_workspaces_order_with_summary AS (
SELECT
fwo.id, fwo.created_at, fwo.updated_at, fwo.owner_id, fwo.organization_id, fwo.template_id, fwo.deleted, fwo.name, fwo.autostart_schedule, fwo.ttl, fwo.last_used_at, fwo.dormant_at, fwo.deleting_at, fwo.automatic_updates, fwo.favorite, fwo.next_start_at, fwo.group_acl, fwo.user_acl, fwo.owner_avatar_url, fwo.owner_username, fwo.owner_name, fwo.organization_name, fwo.organization_display_name, fwo.organization_icon, fwo.organization_description, fwo.template_name, fwo.template_display_name, fwo.template_icon, fwo.template_description, fwo.task_id, fwo.group_acl_display_info, fwo.user_acl_display_info, fwo.template_version_id, fwo.template_version_name, fwo.latest_build_completed_at, fwo.latest_build_canceled_at, fwo.latest_build_error, fwo.latest_build_transition, fwo.latest_build_status, fwo.latest_build_has_external_agent, fwo.latest_build_provisioner_job_id
FROM
filtered_workspaces_order fwo
-- Return a technical summary row with total count of workspaces.
-- It is used to present the correct count if pagination goes beyond the offset.
UNION ALL
SELECT
'00000000-0000-0000-0000-000000000000'::uuid, -- id
'0001-01-01 00:00:00+00'::timestamptz, -- created_at
'0001-01-01 00:00:00+00'::timestamptz, -- updated_at
'00000000-0000-0000-0000-000000000000'::uuid, -- owner_id
'00000000-0000-0000-0000-000000000000'::uuid, -- organization_id
'00000000-0000-0000-0000-000000000000'::uuid, -- template_id
false, -- deleted
'**TECHNICAL_ROW**', -- name
'', -- autostart_schedule
0, -- ttl
'0001-01-01 00:00:00+00'::timestamptz, -- last_used_at
'0001-01-01 00:00:00+00'::timestamptz, -- dormant_at
'0001-01-01 00:00:00+00'::timestamptz, -- deleting_at
'never'::automatic_updates, -- automatic_updates
false, -- favorite
'0001-01-01 00:00:00+00'::timestamptz, -- next_start_at
'{}'::jsonb, -- group_acl
'{}'::jsonb, -- user_acl
'', -- owner_avatar_url
'', -- owner_username
'', -- owner_name
'', -- organization_name
'', -- organization_display_name
'', -- organization_icon
'', -- organization_description
'', -- template_name
'', -- template_display_name
'', -- template_icon
'', -- template_description
'00000000-0000-0000-0000-000000000000'::uuid, -- task_id
'{}'::jsonb, -- group_acl_display_info
'{}'::jsonb, -- user_acl_display_info
-- Extra columns added to ` + "`" + `filtered_workspaces` + "`" + `
'00000000-0000-0000-0000-000000000000'::uuid, -- template_version_id
'', -- template_version_name
'0001-01-01 00:00:00+00'::timestamptz, -- latest_build_completed_at,
'0001-01-01 00:00:00+00'::timestamptz, -- latest_build_canceled_at,
'', -- latest_build_error
'start'::workspace_transition, -- latest_build_transition
'unknown'::provisioner_job_status, -- latest_build_status
false, -- latest_build_has_external_agent
'00000000-0000-0000-0000-000000000000'::uuid -- latest_build_provisioner_job_id
WHERE
$28 :: boolean = true
), total_count AS (
SELECT
count(*) AS count
FROM
filtered_workspaces
)
SELECT
fwos.id, fwos.created_at, fwos.updated_at, fwos.owner_id, fwos.organization_id, fwos.template_id, fwos.deleted, fwos.name, fwos.autostart_schedule, fwos.ttl, fwos.last_used_at, fwos.dormant_at, fwos.deleting_at, fwos.automatic_updates, fwos.favorite, fwos.next_start_at, fwos.group_acl, fwos.user_acl, fwos.owner_avatar_url, fwos.owner_username, fwos.owner_name, fwos.organization_name, fwos.organization_display_name, fwos.organization_icon, fwos.organization_description, fwos.template_name, fwos.template_display_name, fwos.template_icon, fwos.template_description, fwos.task_id, fwos.group_acl_display_info, fwos.user_acl_display_info, fwos.template_version_id, fwos.template_version_name, fwos.latest_build_completed_at, fwos.latest_build_canceled_at, fwos.latest_build_error, fwos.latest_build_transition, fwos.latest_build_status, fwos.latest_build_has_external_agent, fwos.latest_build_provisioner_job_id,
-- agent_metadata expands the response with the requested agent
-- metadata keys for the latest build's agents. The CASE keeps the
-- subquery unevaluated for every caller that does not opt in, and
-- it only runs for the returned page. Each element carries the
-- workspace_agent_id so multi-agent workspaces can map values onto
-- the right agent.
CASE WHEN cardinality($1 :: text[]) > 0 THEN
COALESCE((
SELECT
jsonb_agg(jsonb_build_object(
'workspace_agent_id', workspace_agents.id,
'display_name', workspace_agent_metadata.display_name,
'key', workspace_agent_metadata.key,
-- script is deliberately omitted: it can be long and
-- list consumers want values, not collection commands.
'value', workspace_agent_metadata.value,
'error', workspace_agent_metadata.error,
'timeout', workspace_agent_metadata.timeout,
'interval', workspace_agent_metadata.interval,
-- Rendered explicitly as UTC RFC3339: jsonb renders
-- timestamptz in the session TimeZone, and the year-1
-- default of collected_at renders named zones with
-- LMT second-offsets (even BC), which Go rejects.
'collected_at', to_char(workspace_agent_metadata.collected_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"'),
'display_order', workspace_agent_metadata.display_order
))
FROM
workspace_agents
JOIN
workspace_resources
ON
workspace_resources.id = workspace_agents.resource_id
JOIN
workspace_agent_metadata
ON
workspace_agent_metadata.workspace_agent_id = workspace_agents.id
WHERE
-- The latest build's job was already resolved by the
-- latest_build lateral; resources hang off its job.
workspace_resources.job_id = fwos.latest_build_provisioner_job_id
-- Filter out deleted sub agents.
AND workspace_agents.deleted = FALSE
-- Both sides are lowercased so matching is
-- case-insensitive regardless of how the caller cased
-- the requested keys.
AND LOWER(workspace_agent_metadata.key) = ANY(ARRAY(
SELECT LOWER(key) FROM unnest($1 :: text[]) AS k(key)
))
), '[]'::jsonb)
ELSE
-- Never NULL: lib/pq cannot scan NULL into json.RawMessage.
'[]'::jsonb
END :: jsonb AS agent_metadata,
tc.count
FROM
filtered_workspaces_order_with_summary fwos
CROSS JOIN
total_count tc
`
type GetWorkspacesParams struct {
IncludeAgentMetadata []string `db:"include_agent_metadata" json:"include_agent_metadata"`
ParamNames []string `db:"param_names" json:"param_names"`
ParamValues []string `db:"param_values" json:"param_values"`
Deleted bool `db:"deleted" json:"deleted"`
Status string `db:"status" json:"status"`
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
HasParam []string `db:"has_param" json:"has_param"`
OwnerUsername string `db:"owner_username" json:"owner_username"`
TemplateName string `db:"template_name" json:"template_name"`
TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
WorkspaceIds []uuid.UUID `db:"workspace_ids" json:"workspace_ids"`
Name string `db:"name" json:"name"`
HasAgentStatuses []string `db:"has_agent_statuses" json:"has_agent_statuses"`
AgentInactiveDisconnectTimeoutSeconds int64 `db:"agent_inactive_disconnect_timeout_seconds" json:"agent_inactive_disconnect_timeout_seconds"`
Dormant bool `db:"dormant" json:"dormant"`
LastUsedBefore time.Time `db:"last_used_before" json:"last_used_before"`
LastUsedAfter time.Time `db:"last_used_after" json:"last_used_after"`
UsingActive sql.NullBool `db:"using_active" json:"using_active"`
HasAITask sql.NullBool `db:"has_ai_task" json:"has_ai_task"`
HasExternalAgent sql.NullBool `db:"has_external_agent" json:"has_external_agent"`
Shared sql.NullBool `db:"shared" json:"shared"`
SharedWithUserID uuid.UUID `db:"shared_with_user_id" json:"shared_with_user_id"`
SharedWithGroupID uuid.UUID `db:"shared_with_group_id" json:"shared_with_group_id"`
RequesterID uuid.UUID `db:"requester_id" json:"requester_id"`
Offset int32 `db:"offset_" json:"offset_"`
Limit int32 `db:"limit_" json:"limit_"`
WithSummary bool `db:"with_summary" json:"with_summary"`
}
type GetWorkspacesRow struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
Deleted bool `db:"deleted" json:"deleted"`
Name string `db:"name" json:"name"`
AutostartSchedule sql.NullString `db:"autostart_schedule" json:"autostart_schedule"`
Ttl sql.NullInt64 `db:"ttl" json:"ttl"`
LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"`
DormantAt sql.NullTime `db:"dormant_at" json:"dormant_at"`
DeletingAt sql.NullTime `db:"deleting_at" json:"deleting_at"`
AutomaticUpdates AutomaticUpdates `db:"automatic_updates" json:"automatic_updates"`
Favorite bool `db:"favorite" json:"favorite"`
NextStartAt sql.NullTime `db:"next_start_at" json:"next_start_at"`
GroupACL json.RawMessage `db:"group_acl" json:"group_acl"`
UserACL json.RawMessage `db:"user_acl" json:"user_acl"`
OwnerAvatarUrl string `db:"owner_avatar_url" json:"owner_avatar_url"`
OwnerUsername string `db:"owner_username" json:"owner_username"`
OwnerName string `db:"owner_name" json:"owner_name"`
OrganizationName string `db:"organization_name" json:"organization_name"`
OrganizationDisplayName string `db:"organization_display_name" json:"organization_display_name"`
OrganizationIcon string `db:"organization_icon" json:"organization_icon"`
OrganizationDescription string `db:"organization_description" json:"organization_description"`
TemplateName string `db:"template_name" json:"template_name"`
TemplateDisplayName string `db:"template_display_name" json:"template_display_name"`
TemplateIcon string `db:"template_icon" json:"template_icon"`
TemplateDescription string `db:"template_description" json:"template_description"`
TaskID uuid.NullUUID `db:"task_id" json:"task_id"`
GroupACLDisplayInfo interface{} `db:"group_acl_display_info" json:"group_acl_display_info"`
UserACLDisplayInfo interface{} `db:"user_acl_display_info" json:"user_acl_display_info"`
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
TemplateVersionName sql.NullString `db:"template_version_name" json:"template_version_name"`
LatestBuildCompletedAt sql.NullTime `db:"latest_build_completed_at" json:"latest_build_completed_at"`
LatestBuildCanceledAt sql.NullTime `db:"latest_build_canceled_at" json:"latest_build_canceled_at"`
LatestBuildError sql.NullString `db:"latest_build_error" json:"latest_build_error"`
LatestBuildTransition WorkspaceTransition `db:"latest_build_transition" json:"latest_build_transition"`
LatestBuildStatus ProvisionerJobStatus `db:"latest_build_status" json:"latest_build_status"`
LatestBuildHasExternalAgent sql.NullBool `db:"latest_build_has_external_agent" json:"latest_build_has_external_agent"`
LatestBuildProvisionerJobID uuid.UUID `db:"latest_build_provisioner_job_id" json:"latest_build_provisioner_job_id"`
AgentMetadata json.RawMessage `db:"agent_metadata" json:"agent_metadata"`
Count int64 `db:"count" json:"count"`
}
// build_params is used to filter by build parameters if present.
// It has to be a CTE because the set returning function 'unnest' cannot
// be used in a WHERE clause.
func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) ([]GetWorkspacesRow, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaces,
pq.Array(arg.IncludeAgentMetadata),
pq.Array(arg.ParamNames),
pq.Array(arg.ParamValues),
arg.Deleted,
arg.Status,
arg.OwnerID,
arg.OrganizationID,
pq.Array(arg.HasParam),
arg.OwnerUsername,
arg.TemplateName,
pq.Array(arg.TemplateIDs),
pq.Array(arg.WorkspaceIds),
arg.Name,
pq.Array(arg.HasAgentStatuses),
arg.AgentInactiveDisconnectTimeoutSeconds,
arg.Dormant,
arg.LastUsedBefore,
arg.LastUsedAfter,
arg.UsingActive,
arg.HasAITask,
arg.HasExternalAgent,
arg.Shared,
arg.SharedWithUserID,
arg.SharedWithGroupID,
arg.RequesterID,
arg.Offset,
arg.Limit,
arg.WithSummary,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetWorkspacesRow
for rows.Next() {
var i GetWorkspacesRow
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.OwnerID,
&i.OrganizationID,
&i.TemplateID,
&i.Deleted,
&i.Name,
&i.AutostartSchedule,
&i.Ttl,
&i.LastUsedAt,
&i.DormantAt,
&i.DeletingAt,
&i.AutomaticUpdates,
&i.Favorite,
&i.NextStartAt,
&i.GroupACL,
&i.UserACL,
&i.OwnerAvatarUrl,
&i.OwnerUsername,
&i.OwnerName,
&i.OrganizationName,
&i.OrganizationDisplayName,
&i.OrganizationIcon,
&i.OrganizationDescription,
&i.TemplateName,
&i.TemplateDisplayName,
&i.TemplateIcon,
&i.TemplateDescription,
&i.TaskID,
&i.GroupACLDisplayInfo,
&i.UserACLDisplayInfo,
&i.TemplateVersionID,
&i.TemplateVersionName,
&i.LatestBuildCompletedAt,
&i.LatestBuildCanceledAt,
&i.LatestBuildError,
&i.LatestBuildTransition,
&i.LatestBuildStatus,
&i.LatestBuildHasExternalAgent,
&i.LatestBuildProvisionerJobID,
&i.AgentMetadata,
&i.Count,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspacesAndAgentsByOwnerID = `-- name: GetWorkspacesAndAgentsByOwnerID :many
SELECT
workspaces.id as id,
workspaces.name as name,
job_status,
transition,
(array_agg(ROW(agent_id, agent_name)::agent_id_name_pair) FILTER (WHERE agent_id IS NOT NULL))::agent_id_name_pair[] as agents
FROM workspaces
LEFT JOIN LATERAL (
SELECT
workspace_id,
job_id,
transition,
job_status
FROM workspace_builds
JOIN provisioner_jobs ON provisioner_jobs.id = workspace_builds.job_id
WHERE workspace_builds.workspace_id = workspaces.id
ORDER BY build_number DESC
LIMIT 1
) latest_build ON true
LEFT JOIN LATERAL (
SELECT
workspace_agents.id as agent_id,
workspace_agents.name as agent_name,
job_id
FROM workspace_resources
JOIN workspace_agents ON (
workspace_agents.resource_id = workspace_resources.id
-- Filter out deleted sub agents.
AND workspace_agents.deleted = FALSE
)
WHERE job_id = latest_build.job_id
) resources ON true
WHERE
-- Filter by owner_id
workspaces.owner_id = $1 :: uuid
AND workspaces.deleted = false
-- Authorize Filter clause will be injected below in GetAuthorizedWorkspacesAndAgentsByOwnerID
-- @authorize_filter
GROUP BY workspaces.id, workspaces.name, latest_build.job_status, latest_build.job_id, latest_build.transition
`
type GetWorkspacesAndAgentsByOwnerIDRow struct {
ID uuid.UUID `db:"id" json:"id"`
Name string `db:"name" json:"name"`
JobStatus ProvisionerJobStatus `db:"job_status" json:"job_status"`
Transition WorkspaceTransition `db:"transition" json:"transition"`
Agents []AgentIDNamePair `db:"agents" json:"agents"`
}
func (q *sqlQuerier) GetWorkspacesAndAgentsByOwnerID(ctx context.Context, ownerID uuid.UUID) ([]GetWorkspacesAndAgentsByOwnerIDRow, error) {
rows, err := q.db.QueryContext(ctx, getWorkspacesAndAgentsByOwnerID, ownerID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetWorkspacesAndAgentsByOwnerIDRow
for rows.Next() {
var i GetWorkspacesAndAgentsByOwnerIDRow
if err := rows.Scan(
&i.ID,
&i.Name,
&i.JobStatus,
&i.Transition,
pq.Array(&i.Agents),
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspacesByTemplateID = `-- name: GetWorkspacesByTemplateID :many
SELECT id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl FROM workspaces WHERE template_id = $1 AND deleted = false
`
func (q *sqlQuerier) GetWorkspacesByTemplateID(ctx context.Context, templateID uuid.UUID) ([]WorkspaceTable, error) {
rows, err := q.db.QueryContext(ctx, getWorkspacesByTemplateID, templateID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceTable
for rows.Next() {
var i WorkspaceTable
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.OwnerID,
&i.OrganizationID,
&i.TemplateID,
&i.Deleted,
&i.Name,
&i.AutostartSchedule,
&i.Ttl,
&i.LastUsedAt,
&i.DormantAt,
&i.DeletingAt,
&i.AutomaticUpdates,
&i.Favorite,
&i.NextStartAt,
&i.GroupACL,
&i.UserACL,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspacesEligibleForLifecycleAction = `-- name: GetWorkspacesEligibleForLifecycleAction :many
SELECT
workspaces.id,
workspaces.name,
workspace_builds.template_version_id as build_template_version_id
FROM
workspaces
LEFT JOIN
workspace_builds ON workspace_builds.workspace_id = workspaces.id
INNER JOIN
provisioner_jobs ON workspace_builds.job_id = provisioner_jobs.id
INNER JOIN
templates ON workspaces.template_id = templates.id
INNER JOIN
users ON workspaces.owner_id = users.id
WHERE
workspace_builds.build_number = (
SELECT
MAX(build_number)
FROM
workspace_builds
WHERE
workspace_builds.workspace_id = workspaces.id
) AND
(
-- A workspace may be eligible for autostop if the following are true:
-- * The provisioner job has not failed.
-- * The workspace is not dormant.
-- * The workspace build was a start transition.
-- * The workspace's owner is suspended OR the workspace build deadline has passed.
(
provisioner_jobs.job_status != 'failed'::provisioner_job_status AND
workspaces.dormant_at IS NULL AND
workspace_builds.transition = 'start'::workspace_transition AND (
users.status = 'suspended'::user_status OR (
workspace_builds.deadline != '0001-01-01 00:00:00+00'::timestamptz AND
workspace_builds.deadline < $1 :: timestamptz
)
)
) OR
-- A workspace may be eligible for autostart if the following are true:
-- * The workspace's owner is active.
-- * The provisioner job did not fail.
-- * The workspace build was a stop transition.
-- * The workspace is not dormant
-- * The workspace has an autostart schedule.
-- * It is after the workspace's next start time.
(
users.status = 'active'::user_status AND
provisioner_jobs.job_status != 'failed'::provisioner_job_status AND
workspace_builds.transition = 'stop'::workspace_transition AND
workspaces.dormant_at IS NULL AND
workspaces.autostart_schedule IS NOT NULL AND
(
-- next_start_at might be null in these two scenarios:
-- * A coder instance was updated and we haven't updated next_start_at yet.
-- * A database trigger made it null because of an update to a related column.
--
-- When this occurs, we return the workspace so the Coder server can
-- compute a valid next start at and update it.
workspaces.next_start_at IS NULL OR
workspaces.next_start_at <= $1 :: timestamptz
)
) OR
-- A workspace may be eligible for dormant stop if the following are true:
-- * The workspace is not dormant.
-- * The template has set a time 'til dormant.
-- * The workspace has been unused for longer than the time 'til dormancy.
(
workspaces.dormant_at IS NULL AND
templates.time_til_dormant > 0 AND
($1 :: timestamptz) - workspaces.last_used_at > (INTERVAL '1 millisecond' * (templates.time_til_dormant / 1000000))
) OR
-- A workspace may be eligible for deletion if the following are true:
-- * The workspace is dormant.
-- * The workspace is scheduled to be deleted.
-- * If there was a prior attempt to delete the workspace that failed:
-- * This attempt was at least 24 hours ago.
(
workspaces.dormant_at IS NOT NULL AND
workspaces.deleting_at IS NOT NULL AND
workspaces.deleting_at < $1 :: timestamptz AND
templates.time_til_dormant_autodelete > 0 AND
CASE
WHEN (
workspace_builds.transition = 'delete'::workspace_transition AND
provisioner_jobs.job_status = 'failed'::provisioner_job_status
) THEN (
(
provisioner_jobs.canceled_at IS NOT NULL OR
provisioner_jobs.completed_at IS NOT NULL
) AND (
($1 :: timestamptz) - (CASE
WHEN provisioner_jobs.canceled_at IS NOT NULL THEN provisioner_jobs.canceled_at
ELSE provisioner_jobs.completed_at
END) > INTERVAL '24 hours'
)
)
ELSE true
END
) OR
-- A workspace may be eligible for failed cleanup if the following are true:
-- * The template has a failure ttl set.
-- * The workspace build was a start or stop transition. A failed start
-- is cleaned up by stopping it; a failed stop is retried by issuing
-- another stop.
-- * The provisioner job failed.
-- * The provisioner job had completed.
-- * The provisioner job has been completed for longer than the failure ttl.
(
templates.failure_ttl > 0 AND
(
workspace_builds.transition = 'start'::workspace_transition OR
workspace_builds.transition = 'stop'::workspace_transition
) AND
provisioner_jobs.job_status = 'failed'::provisioner_job_status AND
provisioner_jobs.completed_at IS NOT NULL AND
($1 :: timestamptz) - provisioner_jobs.completed_at > (INTERVAL '1 millisecond' * (templates.failure_ttl / 1000000))
) OR
-- A workspace may be eligible for an autostop reminder if the following are true:
-- * The latest build is a successfully provisioned start build.
-- * The workspace is not dormant and its owner is not suspended.
-- * The build has a deadline in the future (we never remind about a stop already due).
-- * The template opts in (time_til_autostop_notify > 0) and now is within the lead window.
-- * The owner is not active in a way that can keep the workspace
-- alive: either they have not used it within the active threshold
-- (15 minutes), or activity bumps are disabled, or the max_deadline
-- ceiling pins the stop inside the lead window so a bump cannot save it.
-- * A reminder has not yet been sent for THIS deadline.
--
-- NOTE: time_til_autostop_notify has no upper bound. If it exceeds a
-- workspace's remaining lifetime, the notify window already includes "now"
-- at build creation. This arm intentionally still only matches builds whose
-- deadline is in the future (deadline > now) and whose marker has not yet
-- been stamped (notified_autostop_deadline != deadline), so at most ONE
-- reminder is ever produced for a given deadline regardless of how large the
-- field is. The field is stored in nanoseconds, so convert to an interval
-- the same way the dormancy arm does: nanoseconds / 1000000 yields
-- milliseconds.
(
provisioner_jobs.job_status = 'succeeded'::provisioner_job_status AND
workspace_builds.transition = 'start'::workspace_transition AND
workspaces.dormant_at IS NULL AND
users.status != 'suspended'::user_status AND
workspace_builds.deadline != '0001-01-01 00:00:00+00'::timestamptz AND
workspace_builds.deadline > $1::timestamptz AND
templates.time_til_autostop_notify > 0 AND
workspace_builds.deadline <= ($1::timestamptz) + (INTERVAL '1 millisecond' * (templates.time_til_autostop_notify / 1000000)) AND
workspace_builds.notified_autostop_deadline != workspace_builds.deadline AND
-- Keep the reminder unless the user is active AND an activity bump can
-- still move the deadline out of the lead window. This block is the
-- exact complement of the skip-guard in shouldRemindAutostop (Go)
-- (userActive AND bumpEnabled AND NOT maxDeadlineTraps), so the
-- pre-filter and the re-check agree on the boundary.
(
-- Not used within the active threshold (15 minutes). This is the exact
-- complement of the < autostopReminderActiveThreshold guard in
-- shouldRemindAutostop (Go); keep the two in sync.
($1 :: timestamptz) - workspaces.last_used_at >= INTERVAL '15 minutes'
-- ...or activity bumps are disabled (deadline can't move)...
OR templates.activity_bump <= 0
-- ...or the hard max_deadline ceiling is within the lead window, so
-- the workspace will stop regardless of activity.
OR (
workspace_builds.max_deadline != '0001-01-01 00:00:00+00'::timestamptz
AND workspace_builds.max_deadline <= ($1::timestamptz) + (INTERVAL '1 millisecond' * (templates.time_til_autostop_notify / 1000000))
)
)
)
)
AND workspaces.deleted = 'false'
-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
-- should not be considered by the lifecycle executor, as they are handled by the
-- prebuilds reconciliation loop.
AND workspaces.owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID
`
type GetWorkspacesEligibleForLifecycleActionRow struct {
ID uuid.UUID `db:"id" json:"id"`
Name string `db:"name" json:"name"`
BuildTemplateVersionID uuid.NullUUID `db:"build_template_version_id" json:"build_template_version_id"`
}
// Returns workspaces the lifecycle executor must act on this tick. An
// "action" is a state transition (autostart/autostop/dormancy/delete), a
// dormancy mark (which has no build transition), or a one-time autostop
// reminder notification (which only stamps a marker, no transition).
func (q *sqlQuerier) GetWorkspacesEligibleForLifecycleAction(ctx context.Context, now time.Time) ([]GetWorkspacesEligibleForLifecycleActionRow, error) {
rows, err := q.db.QueryContext(ctx, getWorkspacesEligibleForLifecycleAction, now)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetWorkspacesEligibleForLifecycleActionRow
for rows.Next() {
var i GetWorkspacesEligibleForLifecycleActionRow
if err := rows.Scan(&i.ID, &i.Name, &i.BuildTemplateVersionID); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspacesForWorkspaceMetrics = `-- name: GetWorkspacesForWorkspaceMetrics :many
SELECT
u.username as owner_username,
t.name as template_name,
tv.name as template_version_name,
pj.job_status as latest_build_status,
wb.transition as latest_build_transition
FROM workspaces w
JOIN users u ON w.owner_id = u.id
JOIN templates t ON w.template_id = t.id
JOIN workspace_builds wb ON w.id = wb.workspace_id
JOIN provisioner_jobs pj ON wb.job_id = pj.id
LEFT JOIN template_versions tv ON wb.template_version_id = tv.id
WHERE w.deleted = false
AND wb.build_number = (
SELECT MAX(wb2.build_number)
FROM workspace_builds wb2
WHERE wb2.workspace_id = w.id
)
`
type GetWorkspacesForWorkspaceMetricsRow struct {
OwnerUsername string `db:"owner_username" json:"owner_username"`
TemplateName string `db:"template_name" json:"template_name"`
TemplateVersionName sql.NullString `db:"template_version_name" json:"template_version_name"`
LatestBuildStatus ProvisionerJobStatus `db:"latest_build_status" json:"latest_build_status"`
LatestBuildTransition WorkspaceTransition `db:"latest_build_transition" json:"latest_build_transition"`
}
func (q *sqlQuerier) GetWorkspacesForWorkspaceMetrics(ctx context.Context) ([]GetWorkspacesForWorkspaceMetricsRow, error) {
rows, err := q.db.QueryContext(ctx, getWorkspacesForWorkspaceMetrics)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetWorkspacesForWorkspaceMetricsRow
for rows.Next() {
var i GetWorkspacesForWorkspaceMetricsRow
if err := rows.Scan(
&i.OwnerUsername,
&i.TemplateName,
&i.TemplateVersionName,
&i.LatestBuildStatus,
&i.LatestBuildTransition,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertWorkspace = `-- name: InsertWorkspace :one
INSERT INTO
workspaces (
id,
created_at,
updated_at,
owner_id,
organization_id,
template_id,
name,
autostart_schedule,
ttl,
last_used_at,
automatic_updates,
next_start_at
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl
`
type InsertWorkspaceParams struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
Name string `db:"name" json:"name"`
AutostartSchedule sql.NullString `db:"autostart_schedule" json:"autostart_schedule"`
Ttl sql.NullInt64 `db:"ttl" json:"ttl"`
LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"`
AutomaticUpdates AutomaticUpdates `db:"automatic_updates" json:"automatic_updates"`
NextStartAt sql.NullTime `db:"next_start_at" json:"next_start_at"`
}
func (q *sqlQuerier) InsertWorkspace(ctx context.Context, arg InsertWorkspaceParams) (WorkspaceTable, error) {
row := q.db.QueryRowContext(ctx, insertWorkspace,
arg.ID,
arg.CreatedAt,
arg.UpdatedAt,
arg.OwnerID,
arg.OrganizationID,
arg.TemplateID,
arg.Name,
arg.AutostartSchedule,
arg.Ttl,
arg.LastUsedAt,
arg.AutomaticUpdates,
arg.NextStartAt,
)
var i WorkspaceTable
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.OwnerID,
&i.OrganizationID,
&i.TemplateID,
&i.Deleted,
&i.Name,
&i.AutostartSchedule,
&i.Ttl,
&i.LastUsedAt,
&i.DormantAt,
&i.DeletingAt,
&i.AutomaticUpdates,
&i.Favorite,
&i.NextStartAt,
&i.GroupACL,
&i.UserACL,
)
return i, err
}
const unfavoriteWorkspace = `-- name: UnfavoriteWorkspace :exec
UPDATE workspaces SET favorite = false WHERE id = $1
`
func (q *sqlQuerier) UnfavoriteWorkspace(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, unfavoriteWorkspace, id)
return err
}
const updateTemplateWorkspacesLastUsedAt = `-- name: UpdateTemplateWorkspacesLastUsedAt :exec
UPDATE workspaces
SET
last_used_at = $1::timestamptz
WHERE
template_id = $2
`
type UpdateTemplateWorkspacesLastUsedAtParams struct {
LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"`
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
}
func (q *sqlQuerier) UpdateTemplateWorkspacesLastUsedAt(ctx context.Context, arg UpdateTemplateWorkspacesLastUsedAtParams) error {
_, err := q.db.ExecContext(ctx, updateTemplateWorkspacesLastUsedAt, arg.LastUsedAt, arg.TemplateID)
return err
}
const updateWorkspace = `-- name: UpdateWorkspace :one
UPDATE
workspaces
SET
name = $2
WHERE
id = $1
AND deleted = false
RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl
`
type UpdateWorkspaceParams struct {
ID uuid.UUID `db:"id" json:"id"`
Name string `db:"name" json:"name"`
}
func (q *sqlQuerier) UpdateWorkspace(ctx context.Context, arg UpdateWorkspaceParams) (WorkspaceTable, error) {
row := q.db.QueryRowContext(ctx, updateWorkspace, arg.ID, arg.Name)
var i WorkspaceTable
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.OwnerID,
&i.OrganizationID,
&i.TemplateID,
&i.Deleted,
&i.Name,
&i.AutostartSchedule,
&i.Ttl,
&i.LastUsedAt,
&i.DormantAt,
&i.DeletingAt,
&i.AutomaticUpdates,
&i.Favorite,
&i.NextStartAt,
&i.GroupACL,
&i.UserACL,
)
return i, err
}
const updateWorkspaceACLByID = `-- name: UpdateWorkspaceACLByID :exec
UPDATE
workspaces
SET
group_acl = $1,
user_acl = $2
WHERE
id = $3
`
type UpdateWorkspaceACLByIDParams struct {
GroupACL WorkspaceACL `db:"group_acl" json:"group_acl"`
UserACL WorkspaceACL `db:"user_acl" json:"user_acl"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateWorkspaceACLByID(ctx context.Context, arg UpdateWorkspaceACLByIDParams) error {
_, err := q.db.ExecContext(ctx, updateWorkspaceACLByID, arg.GroupACL, arg.UserACL, arg.ID)
return err
}
const updateWorkspaceAutomaticUpdates = `-- name: UpdateWorkspaceAutomaticUpdates :exec
UPDATE
workspaces
SET
automatic_updates = $2
WHERE
id = $1
`
type UpdateWorkspaceAutomaticUpdatesParams struct {
ID uuid.UUID `db:"id" json:"id"`
AutomaticUpdates AutomaticUpdates `db:"automatic_updates" json:"automatic_updates"`
}
func (q *sqlQuerier) UpdateWorkspaceAutomaticUpdates(ctx context.Context, arg UpdateWorkspaceAutomaticUpdatesParams) error {
_, err := q.db.ExecContext(ctx, updateWorkspaceAutomaticUpdates, arg.ID, arg.AutomaticUpdates)
return err
}
const updateWorkspaceAutostart = `-- name: UpdateWorkspaceAutostart :exec
UPDATE
workspaces
SET
autostart_schedule = $2,
next_start_at = $3
WHERE
id = $1
-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
-- are managed by the reconciliation loop, not the lifecycle executor which handles
-- autostart_schedule and next_start_at
AND owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID
`
type UpdateWorkspaceAutostartParams struct {
ID uuid.UUID `db:"id" json:"id"`
AutostartSchedule sql.NullString `db:"autostart_schedule" json:"autostart_schedule"`
NextStartAt sql.NullTime `db:"next_start_at" json:"next_start_at"`
}
func (q *sqlQuerier) UpdateWorkspaceAutostart(ctx context.Context, arg UpdateWorkspaceAutostartParams) error {
_, err := q.db.ExecContext(ctx, updateWorkspaceAutostart, arg.ID, arg.AutostartSchedule, arg.NextStartAt)
return err
}
const updateWorkspaceDeletedByID = `-- name: UpdateWorkspaceDeletedByID :exec
UPDATE
workspaces
SET
deleted = $2
WHERE
id = $1
`
type UpdateWorkspaceDeletedByIDParams struct {
ID uuid.UUID `db:"id" json:"id"`
Deleted bool `db:"deleted" json:"deleted"`
}
func (q *sqlQuerier) UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateWorkspaceDeletedByIDParams) error {
_, err := q.db.ExecContext(ctx, updateWorkspaceDeletedByID, arg.ID, arg.Deleted)
return err
}
const updateWorkspaceDormantDeletingAt = `-- name: UpdateWorkspaceDormantDeletingAt :one
UPDATE
workspaces
SET
dormant_at = $2,
-- When a workspace is active we want to update the last_used_at to avoid the workspace going
-- immediately dormant. If we're transition the workspace to dormant then we leave it alone.
last_used_at = CASE WHEN $2::timestamptz IS NULL THEN
now() at time zone 'utc'
ELSE
last_used_at
END,
-- If dormant_at is null (meaning active) or the template-defined time_til_dormant_autodelete is 0 we should set
-- deleting_at to NULL else set it to the dormant_at + time_til_dormant_autodelete duration.
deleting_at = CASE WHEN $2::timestamptz IS NULL OR templates.time_til_dormant_autodelete = 0 THEN
NULL
ELSE
$2::timestamptz + (INTERVAL '1 millisecond' * (templates.time_til_dormant_autodelete / 1000000))
END
FROM
templates
WHERE
workspaces.id = $1
AND templates.id = workspaces.template_id
-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
-- are managed by the reconciliation loop, not the lifecycle executor which handles
-- dormant_at and deleting_at
AND owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID
RETURNING
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at, workspaces.group_acl, workspaces.user_acl
`
type UpdateWorkspaceDormantDeletingAtParams struct {
ID uuid.UUID `db:"id" json:"id"`
DormantAt sql.NullTime `db:"dormant_at" json:"dormant_at"`
}
func (q *sqlQuerier) UpdateWorkspaceDormantDeletingAt(ctx context.Context, arg UpdateWorkspaceDormantDeletingAtParams) (WorkspaceTable, error) {
row := q.db.QueryRowContext(ctx, updateWorkspaceDormantDeletingAt, arg.ID, arg.DormantAt)
var i WorkspaceTable
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.OwnerID,
&i.OrganizationID,
&i.TemplateID,
&i.Deleted,
&i.Name,
&i.AutostartSchedule,
&i.Ttl,
&i.LastUsedAt,
&i.DormantAt,
&i.DeletingAt,
&i.AutomaticUpdates,
&i.Favorite,
&i.NextStartAt,
&i.GroupACL,
&i.UserACL,
)
return i, err
}
const updateWorkspaceLastUsedAt = `-- name: UpdateWorkspaceLastUsedAt :exec
UPDATE
workspaces
SET
last_used_at = $2
WHERE
id = $1
`
type UpdateWorkspaceLastUsedAtParams struct {
ID uuid.UUID `db:"id" json:"id"`
LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"`
}
func (q *sqlQuerier) UpdateWorkspaceLastUsedAt(ctx context.Context, arg UpdateWorkspaceLastUsedAtParams) error {
_, err := q.db.ExecContext(ctx, updateWorkspaceLastUsedAt, arg.ID, arg.LastUsedAt)
return err
}
const updateWorkspaceNextStartAt = `-- name: UpdateWorkspaceNextStartAt :exec
UPDATE
workspaces
SET
next_start_at = $2
WHERE
id = $1
-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
-- are managed by the reconciliation loop, not the lifecycle executor which handles
-- next_start_at
AND owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID
`
type UpdateWorkspaceNextStartAtParams struct {
ID uuid.UUID `db:"id" json:"id"`
NextStartAt sql.NullTime `db:"next_start_at" json:"next_start_at"`
}
func (q *sqlQuerier) UpdateWorkspaceNextStartAt(ctx context.Context, arg UpdateWorkspaceNextStartAtParams) error {
_, err := q.db.ExecContext(ctx, updateWorkspaceNextStartAt, arg.ID, arg.NextStartAt)
return err
}
const updateWorkspaceTTL = `-- name: UpdateWorkspaceTTL :exec
UPDATE
workspaces
SET
ttl = $2
WHERE
id = $1
-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
-- are managed by the reconciliation loop, not the lifecycle executor which handles
-- ttl
AND owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID
`
type UpdateWorkspaceTTLParams struct {
ID uuid.UUID `db:"id" json:"id"`
Ttl sql.NullInt64 `db:"ttl" json:"ttl"`
}
func (q *sqlQuerier) UpdateWorkspaceTTL(ctx context.Context, arg UpdateWorkspaceTTLParams) error {
_, err := q.db.ExecContext(ctx, updateWorkspaceTTL, arg.ID, arg.Ttl)
return err
}
const updateWorkspacesDormantDeletingAtByTemplateID = `-- name: UpdateWorkspacesDormantDeletingAtByTemplateID :many
UPDATE workspaces
SET
deleting_at = CASE
WHEN $1::bigint = 0 THEN NULL
WHEN $2::timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN ($2::timestamptz) + interval '1 milliseconds' * $1::bigint
ELSE dormant_at + interval '1 milliseconds' * $1::bigint
END,
dormant_at = CASE WHEN $2::timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN $2::timestamptz ELSE dormant_at END
WHERE
template_id = $3
AND dormant_at IS NOT NULL
AND deleted = false
-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
-- should not have their dormant or deleting at set, as these are handled by the
-- prebuilds reconciliation loop.
AND workspaces.owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID
RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl
`
type UpdateWorkspacesDormantDeletingAtByTemplateIDParams struct {
TimeTilDormantAutodeleteMs int64 `db:"time_til_dormant_autodelete_ms" json:"time_til_dormant_autodelete_ms"`
DormantAt time.Time `db:"dormant_at" json:"dormant_at"`
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
}
func (q *sqlQuerier) UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.Context, arg UpdateWorkspacesDormantDeletingAtByTemplateIDParams) ([]WorkspaceTable, error) {
rows, err := q.db.QueryContext(ctx, updateWorkspacesDormantDeletingAtByTemplateID, arg.TimeTilDormantAutodeleteMs, arg.DormantAt, arg.TemplateID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceTable
for rows.Next() {
var i WorkspaceTable
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.OwnerID,
&i.OrganizationID,
&i.TemplateID,
&i.Deleted,
&i.Name,
&i.AutostartSchedule,
&i.Ttl,
&i.LastUsedAt,
&i.DormantAt,
&i.DeletingAt,
&i.AutomaticUpdates,
&i.Favorite,
&i.NextStartAt,
&i.GroupACL,
&i.UserACL,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateWorkspacesTTLByTemplateID = `-- name: UpdateWorkspacesTTLByTemplateID :exec
UPDATE
workspaces
SET
ttl = $2
WHERE
template_id = $1
-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
-- should not have their TTL updated, as they are handled by the prebuilds
-- reconciliation loop.
AND workspaces.owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID
`
type UpdateWorkspacesTTLByTemplateIDParams struct {
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
Ttl sql.NullInt64 `db:"ttl" json:"ttl"`
}
func (q *sqlQuerier) UpdateWorkspacesTTLByTemplateID(ctx context.Context, arg UpdateWorkspacesTTLByTemplateIDParams) error {
_, err := q.db.ExecContext(ctx, updateWorkspacesTTLByTemplateID, arg.TemplateID, arg.Ttl)
return err
}
const getWorkspaceAgentScriptsByAgentIDs = `-- name: GetWorkspaceAgentScriptsByAgentIDs :many
SELECT
DISTINCT ON (workspace_agent_scripts.id) workspace_agent_scripts.workspace_agent_id, workspace_agent_scripts.log_source_id, workspace_agent_scripts.log_path, workspace_agent_scripts.created_at, workspace_agent_scripts.script, workspace_agent_scripts.cron, workspace_agent_scripts.start_blocks_login, workspace_agent_scripts.run_on_start, workspace_agent_scripts.run_on_stop, workspace_agent_scripts.timeout_seconds, workspace_agent_scripts.display_name, workspace_agent_scripts.id,
workspace_agent_script_timings.exit_code,
workspace_agent_script_timings.status
FROM workspace_agent_scripts
LEFT JOIN workspace_agent_script_timings
ON workspace_agent_script_timings.script_id = workspace_agent_scripts.id
WHERE workspace_agent_scripts.workspace_agent_id = ANY($1 :: uuid [ ])
ORDER BY workspace_agent_scripts.id, workspace_agent_script_timings.started_at
DESC NULLS LAST
`
type GetWorkspaceAgentScriptsByAgentIDsRow struct {
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
LogSourceID uuid.UUID `db:"log_source_id" json:"log_source_id"`
LogPath string `db:"log_path" json:"log_path"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
Script string `db:"script" json:"script"`
Cron string `db:"cron" json:"cron"`
StartBlocksLogin bool `db:"start_blocks_login" json:"start_blocks_login"`
RunOnStart bool `db:"run_on_start" json:"run_on_start"`
RunOnStop bool `db:"run_on_stop" json:"run_on_stop"`
TimeoutSeconds int32 `db:"timeout_seconds" json:"timeout_seconds"`
DisplayName string `db:"display_name" json:"display_name"`
ID uuid.UUID `db:"id" json:"id"`
ExitCode sql.NullInt32 `db:"exit_code" json:"exit_code"`
Status NullWorkspaceAgentScriptTimingStatus `db:"status" json:"status"`
}
func (q *sqlQuerier) GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]GetWorkspaceAgentScriptsByAgentIDsRow, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentScriptsByAgentIDs, pq.Array(ids))
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetWorkspaceAgentScriptsByAgentIDsRow
for rows.Next() {
var i GetWorkspaceAgentScriptsByAgentIDsRow
if err := rows.Scan(
&i.WorkspaceAgentID,
&i.LogSourceID,
&i.LogPath,
&i.CreatedAt,
&i.Script,
&i.Cron,
&i.StartBlocksLogin,
&i.RunOnStart,
&i.RunOnStop,
&i.TimeoutSeconds,
&i.DisplayName,
&i.ID,
&i.ExitCode,
&i.Status,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertWorkspaceAgentScripts = `-- name: InsertWorkspaceAgentScripts :many
INSERT INTO
workspace_agent_scripts (workspace_agent_id, created_at, log_source_id, log_path, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds, display_name, id)
SELECT
$1 :: uuid AS workspace_agent_id,
$2 :: timestamptz AS created_at,
unnest($3 :: uuid [ ]) AS log_source_id,
unnest($4 :: text [ ]) AS log_path,
unnest($5 :: text [ ]) AS script,
unnest($6 :: text [ ]) AS cron,
unnest($7 :: boolean [ ]) AS start_blocks_login,
unnest($8 :: boolean [ ]) AS run_on_start,
unnest($9 :: boolean [ ]) AS run_on_stop,
unnest($10 :: integer [ ]) AS timeout_seconds,
unnest($11 :: text [ ]) AS display_name,
unnest($12 :: uuid [ ]) AS id
RETURNING workspace_agent_scripts.workspace_agent_id, workspace_agent_scripts.log_source_id, workspace_agent_scripts.log_path, workspace_agent_scripts.created_at, workspace_agent_scripts.script, workspace_agent_scripts.cron, workspace_agent_scripts.start_blocks_login, workspace_agent_scripts.run_on_start, workspace_agent_scripts.run_on_stop, workspace_agent_scripts.timeout_seconds, workspace_agent_scripts.display_name, workspace_agent_scripts.id
`
type InsertWorkspaceAgentScriptsParams struct {
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
LogSourceID []uuid.UUID `db:"log_source_id" json:"log_source_id"`
LogPath []string `db:"log_path" json:"log_path"`
Script []string `db:"script" json:"script"`
Cron []string `db:"cron" json:"cron"`
StartBlocksLogin []bool `db:"start_blocks_login" json:"start_blocks_login"`
RunOnStart []bool `db:"run_on_start" json:"run_on_start"`
RunOnStop []bool `db:"run_on_stop" json:"run_on_stop"`
TimeoutSeconds []int32 `db:"timeout_seconds" json:"timeout_seconds"`
DisplayName []string `db:"display_name" json:"display_name"`
ID []uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) InsertWorkspaceAgentScripts(ctx context.Context, arg InsertWorkspaceAgentScriptsParams) ([]WorkspaceAgentScript, error) {
rows, err := q.db.QueryContext(ctx, insertWorkspaceAgentScripts,
arg.WorkspaceAgentID,
arg.CreatedAt,
pq.Array(arg.LogSourceID),
pq.Array(arg.LogPath),
pq.Array(arg.Script),
pq.Array(arg.Cron),
pq.Array(arg.StartBlocksLogin),
pq.Array(arg.RunOnStart),
pq.Array(arg.RunOnStop),
pq.Array(arg.TimeoutSeconds),
pq.Array(arg.DisplayName),
pq.Array(arg.ID),
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceAgentScript
for rows.Next() {
var i WorkspaceAgentScript
if err := rows.Scan(
&i.WorkspaceAgentID,
&i.LogSourceID,
&i.LogPath,
&i.CreatedAt,
&i.Script,
&i.Cron,
&i.StartBlocksLogin,
&i.RunOnStart,
&i.RunOnStop,
&i.TimeoutSeconds,
&i.DisplayName,
&i.ID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}