// 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 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 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, ) 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 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, ) 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 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, ) 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 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, ); 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, enabled, base_url, settings, settings_key_id ) VALUES ( $1::uuid, $2::ai_provider_type, $3::text, $4::text, $5::boolean, $6::text, $7::text, $8::text ) RETURNING id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at ` 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"` 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.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, ) return i, err } const updateAIProvider = `-- name: UpdateAIProvider :one UPDATE ai_providers SET display_name = $1::text, enabled = $2::boolean, base_url = $3::text, settings = $4::text, settings_key_id = $5::text, updated_at = NOW() WHERE id = $6::uuid AND deleted = FALSE RETURNING id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at ` type UpdateAIProviderParams struct { DisplayName sql.NullString `db:"display_name" json:"display_name"` 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.DisplayName, 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, ) 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 ` 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, ) 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 countAIBridgeInterceptions = `-- name: CountAIBridgeInterceptions :one SELECT COUNT(*) 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 -- Authorize Filter clause will be injected below in ListAuthorizedAIBridgeInterceptions -- @authorize_filter ` type CountAIBridgeInterceptionsParams 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"` } func (q *sqlQuerier) CountAIBridgeInterceptions(ctx context.Context, arg CountAIBridgeInterceptionsParams) (int64, error) { row := q.db.QueryRowContext(ctx, countAIBridgeInterceptions, arg.StartedAfter, arg.StartedBefore, arg.InitiatorID, arg.Provider, arg.ProviderName, arg.Model, arg.Client, ) var count int64 err := row.Scan(&count) return count, 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 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 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, ) 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 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, ); 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 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, ); 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 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, ); 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 ) VALUES ( $1, $2, $3, $4, $5, $6, COALESCE($7::jsonb, '{}'::jsonb), $8, $9, $10, $11::uuid, $12::uuid, $13, $14 ) 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 ` 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"` } 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, ) 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, ) 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 ) VALUES ( $1, $2, $3, $4, $5, $6, $7, COALESCE($8::jsonb, '{}'::jsonb), $9 ) RETURNING id, interception_id, provider_response_id, input_tokens, output_tokens, metadata, created_at, cache_read_input_tokens, cache_write_input_tokens ` 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"` } 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, ) var i AIBridgeTokenUsage err := row.Scan( &i.ID, &i.InterceptionID, &i.ProviderResponseID, &i.InputTokens, &i.OutputTokens, &i.Metadata, &i.CreatedAt, &i.CacheReadInputTokens, &i.CacheWriteInputTokens, ) return i, err } const insertAIBridgeToolUsage = `-- name: InsertAIBridgeToolUsage :one INSERT INTO aibridge_tool_usages ( id, interception_id, provider_response_id, provider_tool_call_id, tool, server_url, input, injected, invocation_error, metadata, created_at ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, COALESCE($10::jsonb, '{}'::jsonb), $11 ) RETURNING id, interception_id, provider_response_id, server_url, tool, input, injected, invocation_error, metadata, created_at, provider_tool_call_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"` 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.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, ) 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 listAIBridgeInterceptions = `-- name: ListAIBridgeInterceptions :many SELECT 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, visible_users.id, visible_users.username, visible_users.name, visible_users.avatar_url FROM aibridge_interceptions JOIN visible_users ON visible_users.id = aibridge_interceptions.initiator_id 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 -- Cursor pagination AND CASE WHEN $8::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 started_at field, so select all -- rows before the cursor and before the after_id UUID. -- This uses a less than operator because we're sorting DESC. The -- "after_id" terminology comes from our pagination parser in -- coderd. (aibridge_interceptions.started_at, aibridge_interceptions.id) < ( (SELECT started_at FROM aibridge_interceptions WHERE id = $8), $8::uuid ) ) ELSE true END -- Authorize Filter clause will be injected below in ListAuthorizedAIBridgeInterceptions -- @authorize_filter ORDER BY aibridge_interceptions.started_at DESC, aibridge_interceptions.id DESC LIMIT COALESCE(NULLIF($10::integer, 0), 100) OFFSET $9 ` type ListAIBridgeInterceptionsParams 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"` AfterID uuid.UUID `db:"after_id" json:"after_id"` Offset int32 `db:"offset_" json:"offset_"` Limit int32 `db:"limit_" json:"limit_"` } type ListAIBridgeInterceptionsRow struct { AIBridgeInterception AIBridgeInterception `db:"aibridge_interception" json:"aibridge_interception"` VisibleUser VisibleUser `db:"visible_user" json:"visible_user"` } func (q *sqlQuerier) ListAIBridgeInterceptions(ctx context.Context, arg ListAIBridgeInterceptionsParams) ([]ListAIBridgeInterceptionsRow, error) { rows, err := q.db.QueryContext(ctx, listAIBridgeInterceptions, arg.StartedAfter, arg.StartedBefore, arg.InitiatorID, arg.Provider, arg.ProviderName, arg.Model, arg.Client, arg.AfterID, arg.Offset, arg.Limit, ) if err != nil { return nil, err } defer rows.Close() var items []ListAIBridgeInterceptionsRow for rows.Next() { var i ListAIBridgeInterceptionsRow if err := rows.Scan( &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.VisibleUser.ID, &i.VisibleUser.Username, &i.VisibleUser.Name, &i.VisibleUser.AvatarURL, ); 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 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 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 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, ); 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 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 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 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"` } // 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. 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, ); 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 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, ); 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 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, ); 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 WHERE id = $2::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 ` type UpdateAIBridgeInterceptionEndedParams struct { EndedAt time.Time `db:"ended_at" json:"ended_at"` 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.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, ) 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 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 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 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() ` // 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. 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 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 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 getBoundaryLogByID = `-- name: GetBoundaryLogByID :one SELECT id, session_id, sequence_number, captured_at, created_at, proto, method, detail, matched_rule 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, ) return i, err } const getBoundarySessionByID = `-- name: GetBoundarySessionByID :one SELECT id, workspace_agent_id, confined_process_name, started_at, updated_at FROM boundary_sessions WHERE id = $1 ` func (q *sqlQuerier) GetBoundarySessionByID(ctx context.Context, id uuid.UUID) (BoundarySession, error) { row := q.db.QueryRowContext(ctx, getBoundarySessionByID, id) var i BoundarySession err := row.Scan( &i.ID, &i.WorkspaceAgentID, &i.ConfinedProcessName, &i.StartedAt, &i.UpdatedAt, ) return i, err } const insertBoundaryLog = `-- name: InsertBoundaryLog :one INSERT INTO boundary_logs ( id, session_id, sequence_number, captured_at, created_at, proto, method, detail, matched_rule ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9 ) RETURNING id, session_id, sequence_number, captured_at, created_at, proto, method, detail, matched_rule ` type InsertBoundaryLogParams struct { ID uuid.UUID `db:"id" json:"id"` SessionID uuid.UUID `db:"session_id" json:"session_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 sql.NullString `db:"matched_rule" json:"matched_rule"` } func (q *sqlQuerier) InsertBoundaryLog(ctx context.Context, arg InsertBoundaryLogParams) (BoundaryLog, error) { row := q.db.QueryRowContext(ctx, insertBoundaryLog, arg.ID, arg.SessionID, arg.SequenceNumber, arg.CapturedAt, arg.CreatedAt, arg.Proto, arg.Method, arg.Detail, arg.MatchedRule, ) var i BoundaryLog err := row.Scan( &i.ID, &i.SessionID, &i.SequenceNumber, &i.CapturedAt, &i.CreatedAt, &i.Proto, &i.Method, &i.Detail, &i.MatchedRule, ) return i, err } const insertBoundarySession = `-- name: InsertBoundarySession :one INSERT INTO boundary_sessions ( id, workspace_agent_id, confined_process_name, started_at, updated_at ) VALUES ( $1, $2, $3, $4, $5 ) RETURNING id, workspace_agent_id, confined_process_name, started_at, updated_at ` type InsertBoundarySessionParams 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"` } func (q *sqlQuerier) InsertBoundarySession(ctx context.Context, arg InsertBoundarySessionParams) (BoundarySession, error) { row := q.db.QueryRowContext(ctx, insertBoundarySession, arg.ID, arg.WorkspaceAgentID, arg.ConfinedProcessName, arg.StartedAt, arg.UpdatedAt, ) var i BoundarySession err := row.Scan( &i.ID, &i.WorkspaceAgentID, &i.ConfinedProcessName, &i.StartedAt, &i.UpdatedAt, ) return i, err } const listBoundaryLogsBySessionID = `-- name: ListBoundaryLogsBySessionID :many SELECT id, session_id, sequence_number, captured_at, created_at, proto, method, detail, matched_rule 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 optional exclusive sequence number bounds (seq_after, 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, ); 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 deleteOldChatFiles = `-- name: DeleteOldChatFiles :execrows WITH kept_file_ids AS ( -- NOTE: This uses updated_at as a proxy for archive time -- because there is no archived_at column. Correctness -- requires that updated_at is never backdated on archived -- chats. See ArchiveChatByID. SELECT DISTINCT cfl.file_id FROM chat_file_links cfl JOIN chats c ON c.id = cfl.chat_id WHERE c.archived = false OR c.updated_at >= $1::timestamptz ), deletable AS ( SELECT cf.id FROM chat_files cf LEFT JOIN kept_file_ids k ON cf.id = k.file_id WHERE cf.created_at < $1::timestamptz AND k.file_id IS NULL ORDER BY cf.created_at ASC LIMIT $2 ) DELETE FROM chat_files USING deletable WHERE chat_files.id = deletable.id ` type DeleteOldChatFilesParams struct { BeforeTime time.Time `db:"before_time" json:"before_time"` LimitCount int32 `db:"limit_count" json:"limit_count"` } // TODO(cian): Add indexes on chats(archived, updated_at) and // chat_files(created_at) for purge query performance. // See: https://github.com/coder/internal/issues/1438 // Deletes chat files that are older than the given threshold and are // not referenced by any chat that is still active or was archived // within the same threshold window. This covers two cases: // 1. Orphaned files not linked to any chat. // 2. Files whose every referencing chat has been archived for longer // than the retention period. func (q *sqlQuerier) DeleteOldChatFiles(ctx context.Context, arg DeleteOldChatFilesParams) (int64, error) { result, err := q.db.ExecContext(ctx, deleteOldChatFiles, arg.BeforeTime, arg.LimitCount) 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 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 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 getPRInsightsPerModel = `-- name: GetPRInsightsPerModel :many WITH pr_costs AS ( SELECT prc.pr_key, COALESCE(SUM(cc.cost_micros), 0) AS cost_micros FROM ( SELECT DISTINCT COALESCE(NULLIF(cds.url, ''), c.id::text) AS pr_key, related.id AS chat_id FROM chat_diff_statuses cds JOIN chats c ON c.id = cds.chat_id JOIN chats related ON related.id = c.id OR (related.parent_chat_id = c.id AND NOT EXISTS ( SELECT 1 FROM chat_diff_statuses cds2 WHERE cds2.chat_id = related.id AND cds2.pull_request_state IS NOT NULL )) WHERE cds.pull_request_state IS NOT NULL AND c.created_at >= $1::timestamptz AND c.created_at < $2::timestamptz AND ($3::uuid IS NULL OR c.owner_id = $3::uuid) ) prc LEFT JOIN LATERAL ( SELECT COALESCE(SUM(cm.total_cost_micros), 0) AS cost_micros FROM chat_messages cm WHERE cm.chat_id = prc.chat_id AND cm.total_cost_micros IS NOT NULL ) cc ON TRUE GROUP BY prc.pr_key ), deduped AS ( SELECT DISTINCT ON (COALESCE(NULLIF(cds.url, ''), c.id::text)) COALESCE(NULLIF(cds.url, ''), c.id::text) AS pr_key, cds.pull_request_state, cds.additions, cds.deletions, cmc.id AS model_config_id, cmc.display_name, cmc.model, cmc.provider FROM chat_diff_statuses cds JOIN chats c ON c.id = cds.chat_id LEFT JOIN chat_model_configs cmc ON cmc.id = c.last_model_config_id WHERE cds.pull_request_state IS NOT NULL AND c.created_at >= $1::timestamptz AND c.created_at < $2::timestamptz AND ($3::uuid IS NULL OR c.owner_id = $3::uuid) ORDER BY COALESCE(NULLIF(cds.url, ''), c.id::text), c.created_at DESC, c.id DESC ) SELECT d.model_config_id, COALESCE(NULLIF(d.display_name, ''), NULLIF(d.model, ''), 'Unknown')::text AS display_name, COALESCE(d.provider, 'unknown')::text AS provider, COUNT(*)::bigint AS total_prs, COUNT(*) FILTER (WHERE d.pull_request_state = 'merged')::bigint AS merged_prs, COALESCE(SUM(d.additions), 0)::bigint AS total_additions, COALESCE(SUM(d.deletions), 0)::bigint AS total_deletions, COALESCE(SUM(pc.cost_micros), 0)::bigint AS total_cost_micros, COALESCE(SUM(pc.cost_micros) FILTER (WHERE d.pull_request_state = 'merged'), 0)::bigint AS merged_cost_micros FROM deduped d JOIN pr_costs pc ON pc.pr_key = d.pr_key GROUP BY d.model_config_id, d.display_name, d.model, d.provider ORDER BY total_prs DESC ` type GetPRInsightsPerModelParams struct { StartDate time.Time `db:"start_date" json:"start_date"` EndDate time.Time `db:"end_date" json:"end_date"` OwnerID uuid.NullUUID `db:"owner_id" json:"owner_id"` } type GetPRInsightsPerModelRow struct { ModelConfigID uuid.NullUUID `db:"model_config_id" json:"model_config_id"` DisplayName string `db:"display_name" json:"display_name"` Provider string `db:"provider" json:"provider"` TotalPrs int64 `db:"total_prs" json:"total_prs"` MergedPrs int64 `db:"merged_prs" json:"merged_prs"` TotalAdditions int64 `db:"total_additions" json:"total_additions"` TotalDeletions int64 `db:"total_deletions" json:"total_deletions"` TotalCostMicros int64 `db:"total_cost_micros" json:"total_cost_micros"` MergedCostMicros int64 `db:"merged_cost_micros" json:"merged_cost_micros"` } // Returns PR metrics grouped by the model used for each chat. // Uses two CTEs: pr_costs sums cost for the PR-linked chat and its // direct children (that lack their own PR), and deduped picks one row // per PR for state/additions/deletions/model (model comes from the // most recent chat). func (q *sqlQuerier) GetPRInsightsPerModel(ctx context.Context, arg GetPRInsightsPerModelParams) ([]GetPRInsightsPerModelRow, error) { rows, err := q.db.QueryContext(ctx, getPRInsightsPerModel, arg.StartDate, arg.EndDate, arg.OwnerID) if err != nil { return nil, err } defer rows.Close() var items []GetPRInsightsPerModelRow for rows.Next() { var i GetPRInsightsPerModelRow if err := rows.Scan( &i.ModelConfigID, &i.DisplayName, &i.Provider, &i.TotalPrs, &i.MergedPrs, &i.TotalAdditions, &i.TotalDeletions, &i.TotalCostMicros, &i.MergedCostMicros, ); 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 getPRInsightsPullRequests = `-- name: GetPRInsightsPullRequests :many WITH pr_costs AS ( SELECT prc.pr_key, COALESCE(SUM(cc.cost_micros), 0) AS cost_micros FROM ( SELECT DISTINCT COALESCE(NULLIF(cds.url, ''), c.id::text) AS pr_key, related.id AS chat_id FROM chat_diff_statuses cds JOIN chats c ON c.id = cds.chat_id JOIN chats related ON related.id = c.id OR (related.parent_chat_id = c.id AND NOT EXISTS ( SELECT 1 FROM chat_diff_statuses cds2 WHERE cds2.chat_id = related.id AND cds2.pull_request_state IS NOT NULL )) WHERE cds.pull_request_state IS NOT NULL AND c.created_at >= $1::timestamptz AND c.created_at < $2::timestamptz AND ($3::uuid IS NULL OR c.owner_id = $3::uuid) ) prc LEFT JOIN LATERAL ( SELECT COALESCE(SUM(cm.total_cost_micros), 0) AS cost_micros FROM chat_messages cm WHERE cm.chat_id = prc.chat_id AND cm.total_cost_micros IS NOT NULL ) cc ON TRUE GROUP BY prc.pr_key ), deduped AS ( SELECT DISTINCT ON (COALESCE(NULLIF(cds.url, ''), c.id::text)) COALESCE(NULLIF(cds.url, ''), c.id::text) AS pr_key, c.id AS chat_id, cds.pull_request_title AS pr_title, cds.url AS pr_url, cds.pr_number, cds.pull_request_state AS state, cds.pull_request_draft AS draft, cds.additions, cds.deletions, cds.changed_files, cds.commits, cds.approved, cds.changes_requested, cds.reviewer_count, cds.author_login, cds.author_avatar_url, COALESCE(cds.base_branch, '')::text AS base_branch, COALESCE(NULLIF(cmc.display_name, ''), NULLIF(cmc.model, ''), 'Unknown')::text AS model_display_name, c.created_at FROM chat_diff_statuses cds JOIN chats c ON c.id = cds.chat_id LEFT JOIN chat_model_configs cmc ON cmc.id = c.last_model_config_id WHERE cds.pull_request_state IS NOT NULL AND c.created_at >= $1::timestamptz AND c.created_at < $2::timestamptz AND ($3::uuid IS NULL OR c.owner_id = $3::uuid) ORDER BY COALESCE(NULLIF(cds.url, ''), c.id::text), c.created_at DESC, c.id DESC ) SELECT chat_id, pr_title, pr_url, pr_number, state, draft, additions, deletions, changed_files, commits, approved, changes_requested, reviewer_count, author_login, author_avatar_url, base_branch, model_display_name, cost_micros, created_at FROM ( SELECT d.chat_id, d.pr_title, d.pr_url, d.pr_number, d.state, d.draft, d.additions, d.deletions, d.changed_files, d.commits, d.approved, d.changes_requested, d.reviewer_count, d.author_login, d.author_avatar_url, d.base_branch, d.model_display_name, COALESCE(pc.cost_micros, 0)::bigint AS cost_micros, d.created_at FROM deduped d JOIN pr_costs pc ON pc.pr_key = d.pr_key ) sub ORDER BY sub.created_at DESC LIMIT 500 ` type GetPRInsightsPullRequestsParams struct { StartDate time.Time `db:"start_date" json:"start_date"` EndDate time.Time `db:"end_date" json:"end_date"` OwnerID uuid.NullUUID `db:"owner_id" json:"owner_id"` } type GetPRInsightsPullRequestsRow struct { ChatID uuid.UUID `db:"chat_id" json:"chat_id"` PrTitle string `db:"pr_title" json:"pr_title"` PrUrl sql.NullString `db:"pr_url" json:"pr_url"` PrNumber sql.NullInt32 `db:"pr_number" json:"pr_number"` State sql.NullString `db:"state" json:"state"` Draft bool `db:"draft" json:"draft"` Additions int32 `db:"additions" json:"additions"` Deletions int32 `db:"deletions" json:"deletions"` ChangedFiles int32 `db:"changed_files" json:"changed_files"` Commits sql.NullInt32 `db:"commits" json:"commits"` Approved sql.NullBool `db:"approved" json:"approved"` ChangesRequested bool `db:"changes_requested" json:"changes_requested"` ReviewerCount sql.NullInt32 `db:"reviewer_count" json:"reviewer_count"` AuthorLogin sql.NullString `db:"author_login" json:"author_login"` AuthorAvatarUrl sql.NullString `db:"author_avatar_url" json:"author_avatar_url"` BaseBranch string `db:"base_branch" json:"base_branch"` ModelDisplayName string `db:"model_display_name" json:"model_display_name"` CostMicros int64 `db:"cost_micros" json:"cost_micros"` CreatedAt time.Time `db:"created_at" json:"created_at"` } // Returns all individual PR rows with cost for the selected time range. // Uses two CTEs: pr_costs sums cost for the PR-linked chat and its // direct children (that lack their own PR), and deduped picks one row // per PR for metadata. A safety-cap LIMIT guards against unexpectedly // large result sets from direct API callers. func (q *sqlQuerier) GetPRInsightsPullRequests(ctx context.Context, arg GetPRInsightsPullRequestsParams) ([]GetPRInsightsPullRequestsRow, error) { rows, err := q.db.QueryContext(ctx, getPRInsightsPullRequests, arg.StartDate, arg.EndDate, arg.OwnerID) if err != nil { return nil, err } defer rows.Close() var items []GetPRInsightsPullRequestsRow for rows.Next() { var i GetPRInsightsPullRequestsRow if err := rows.Scan( &i.ChatID, &i.PrTitle, &i.PrUrl, &i.PrNumber, &i.State, &i.Draft, &i.Additions, &i.Deletions, &i.ChangedFiles, &i.Commits, &i.Approved, &i.ChangesRequested, &i.ReviewerCount, &i.AuthorLogin, &i.AuthorAvatarUrl, &i.BaseBranch, &i.ModelDisplayName, &i.CostMicros, &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 getPRInsightsSummary = `-- name: GetPRInsightsSummary :one WITH pr_costs AS ( SELECT prc.pr_key, COALESCE(SUM(cc.cost_micros), 0) AS cost_micros FROM ( -- For each PR, include the chat that references it plus any -- direct children (subagents) that do not have their own PR. SELECT DISTINCT COALESCE(NULLIF(cds.url, ''), c.id::text) AS pr_key, related.id AS chat_id FROM chat_diff_statuses cds JOIN chats c ON c.id = cds.chat_id JOIN chats related ON related.id = c.id OR (related.parent_chat_id = c.id AND NOT EXISTS ( SELECT 1 FROM chat_diff_statuses cds2 WHERE cds2.chat_id = related.id AND cds2.pull_request_state IS NOT NULL )) WHERE cds.pull_request_state IS NOT NULL AND c.created_at >= $1::timestamptz AND c.created_at < $2::timestamptz AND ($3::uuid IS NULL OR c.owner_id = $3::uuid) ) prc LEFT JOIN LATERAL ( SELECT COALESCE(SUM(cm.total_cost_micros), 0) AS cost_micros FROM chat_messages cm WHERE cm.chat_id = prc.chat_id AND cm.total_cost_micros IS NOT NULL ) cc ON TRUE GROUP BY prc.pr_key ), deduped AS ( SELECT DISTINCT ON (COALESCE(NULLIF(cds.url, ''), c.id::text)) COALESCE(NULLIF(cds.url, ''), c.id::text) AS pr_key, cds.pull_request_state, cds.additions, cds.deletions FROM chat_diff_statuses cds JOIN chats c ON c.id = cds.chat_id WHERE cds.pull_request_state IS NOT NULL AND c.created_at >= $1::timestamptz AND c.created_at < $2::timestamptz AND ($3::uuid IS NULL OR c.owner_id = $3::uuid) ORDER BY COALESCE(NULLIF(cds.url, ''), c.id::text), c.created_at DESC, c.id DESC ) SELECT COUNT(*)::bigint AS total_prs_created, COUNT(*) FILTER (WHERE d.pull_request_state = 'merged')::bigint AS total_prs_merged, COUNT(*) FILTER (WHERE d.pull_request_state = 'closed')::bigint AS total_prs_closed, COALESCE(SUM(d.additions), 0)::bigint AS total_additions, COALESCE(SUM(d.deletions), 0)::bigint AS total_deletions, COALESCE(SUM(pc.cost_micros), 0)::bigint AS total_cost_micros, COALESCE(SUM(pc.cost_micros) FILTER (WHERE d.pull_request_state = 'merged'), 0)::bigint AS merged_cost_micros FROM deduped d JOIN pr_costs pc ON pc.pr_key = d.pr_key ` type GetPRInsightsSummaryParams struct { StartDate time.Time `db:"start_date" json:"start_date"` EndDate time.Time `db:"end_date" json:"end_date"` OwnerID uuid.NullUUID `db:"owner_id" json:"owner_id"` } type GetPRInsightsSummaryRow struct { TotalPrsCreated int64 `db:"total_prs_created" json:"total_prs_created"` TotalPrsMerged int64 `db:"total_prs_merged" json:"total_prs_merged"` TotalPrsClosed int64 `db:"total_prs_closed" json:"total_prs_closed"` TotalAdditions int64 `db:"total_additions" json:"total_additions"` TotalDeletions int64 `db:"total_deletions" json:"total_deletions"` TotalCostMicros int64 `db:"total_cost_micros" json:"total_cost_micros"` MergedCostMicros int64 `db:"merged_cost_micros" json:"merged_cost_micros"` } // PR Insights queries for the /agents analytics dashboard. // These aggregate data from chat_diff_statuses (PR metadata) joined // with chats and chat_messages (cost) to power the PR Insights view. // // Cost is computed per PR by summing the PR-linked chat's own cost plus // the costs of any direct children (subagents) it spawned that do NOT // have their own PR association. If a child chat has its own // chat_diff_statuses entry (with a non-NULL pull_request_state), its // cost is attributed to that child's PR instead — preventing // double-counting when sibling chats create different PRs. // Subagent trees are at most 2 levels deep (enforced by the // application layer). PR metadata (state, additions, deletions) // comes from the most recent chat via DISTINCT ON so that each PR // is counted exactly once. // Returns aggregate PR metrics for the given date range. // The handler calls this twice (current + previous period) for trends. // Uses two CTEs: pr_costs sums cost for the PR-linked chat and its // direct children (that lack their own PR), and deduped picks one row // per PR for state/additions/deletions. func (q *sqlQuerier) GetPRInsightsSummary(ctx context.Context, arg GetPRInsightsSummaryParams) (GetPRInsightsSummaryRow, error) { row := q.db.QueryRowContext(ctx, getPRInsightsSummary, arg.StartDate, arg.EndDate, arg.OwnerID) var i GetPRInsightsSummaryRow err := row.Scan( &i.TotalPrsCreated, &i.TotalPrsMerged, &i.TotalPrsClosed, &i.TotalAdditions, &i.TotalDeletions, &i.TotalCostMicros, &i.MergedCostMicros, ) return i, err } const getPRInsightsTimeSeries = `-- name: GetPRInsightsTimeSeries :many WITH deduped AS ( SELECT DISTINCT ON (COALESCE(NULLIF(cds.url, ''), c.id::text)) cds.pull_request_state, c.created_at FROM chat_diff_statuses cds JOIN chats c ON c.id = cds.chat_id WHERE cds.pull_request_state IS NOT NULL AND c.created_at >= $1::timestamptz AND c.created_at < $2::timestamptz AND ($3::uuid IS NULL OR c.owner_id = $3::uuid) ORDER BY COALESCE(NULLIF(cds.url, ''), c.id::text), c.created_at DESC, c.id DESC ) SELECT date_trunc('day', created_at)::timestamptz AS date, COUNT(*)::bigint AS prs_created, COUNT(*) FILTER (WHERE pull_request_state = 'merged')::bigint AS prs_merged, COUNT(*) FILTER (WHERE pull_request_state = 'closed')::bigint AS prs_closed FROM deduped GROUP BY date_trunc('day', created_at) ORDER BY date_trunc('day', created_at) ` type GetPRInsightsTimeSeriesParams struct { StartDate time.Time `db:"start_date" json:"start_date"` EndDate time.Time `db:"end_date" json:"end_date"` OwnerID uuid.NullUUID `db:"owner_id" json:"owner_id"` } type GetPRInsightsTimeSeriesRow struct { Date time.Time `db:"date" json:"date"` PrsCreated int64 `db:"prs_created" json:"prs_created"` PrsMerged int64 `db:"prs_merged" json:"prs_merged"` PrsClosed int64 `db:"prs_closed" json:"prs_closed"` } // Returns daily PR counts grouped by state for the chart. // Uses a CTE to deduplicate by PR URL so that multiple chats referencing // the same pull request are only counted once (keeping the most recent chat). func (q *sqlQuerier) GetPRInsightsTimeSeries(ctx context.Context, arg GetPRInsightsTimeSeriesParams) ([]GetPRInsightsTimeSeriesRow, error) { rows, err := q.db.QueryContext(ctx, getPRInsightsTimeSeries, arg.StartDate, arg.EndDate, arg.OwnerID) if err != nil { return nil, err } defer rows.Close() var items []GetPRInsightsTimeSeriesRow for rows.Next() { var i GetPRInsightsTimeSeriesRow if err := rows.Scan( &i.Date, &i.PrsCreated, &i.PrsMerged, &i.PrsClosed, ); 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 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 deleteChatModelConfigsByProvider = `-- name: DeleteChatModelConfigsByProvider :exec UPDATE chat_model_configs SET deleted = TRUE, deleted_at = NOW(), updated_at = NOW() WHERE provider = $1::text AND deleted = FALSE ` func (q *sqlQuerier) DeleteChatModelConfigsByProvider(ctx context.Context, provider string) error { _, err := q.db.ExecContext(ctx, deleteChatModelConfigsByProvider, provider) return err } const getChatModelConfigByID = `-- name: GetChatModelConfigByID :one SELECT id, provider, 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.Provider, &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 id, provider, 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 deleted = FALSE ORDER BY provider ASC, model ASC, updated_at DESC, 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.Provider, &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, provider, 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.Provider, &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.provider, 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.Provider, &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.provider, 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.enabled = TRUE AND cmc.deleted = FALSE AND ap.enabled = TRUE AND ap.deleted = FALSE ORDER BY cmc.provider ASC, cmc.model ASC, cmc.updated_at DESC, cmc.id DESC ` func (q *sqlQuerier) GetEnabledChatModelConfigs(ctx context.Context) ([]ChatModelConfig, error) { rows, err := q.db.QueryContext(ctx, getEnabledChatModelConfigs) 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.Provider, &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 insertChatModelConfig = `-- name: InsertChatModelConfig :one INSERT INTO chat_model_configs ( provider, model, display_name, created_by, updated_by, enabled, is_default, context_limit, compression_threshold, options, ai_provider_id ) VALUES ( $1::text, $2::text, $3::text, $4::uuid, $5::uuid, $6::boolean, $7::boolean, $8::bigint, $9::integer, $10::jsonb, $11::uuid ) RETURNING id, provider, 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 { Provider string `db:"provider" json:"provider"` 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.Provider, 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.Provider, &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 provider = $1::text, model = $2::text, display_name = $3::text, updated_by = $4::uuid, enabled = $5::boolean, is_default = $6::boolean, context_limit = $7::bigint, compression_threshold = $8::integer, options = $9::jsonb, ai_provider_id = $10::uuid, updated_at = NOW() WHERE id = $11::uuid AND deleted = FALSE RETURNING id, provider, 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 { Provider string `db:"provider" json:"provider"` 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.Provider, 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.Provider, &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 acquireChats = `-- name: AcquireChats :many WITH acquired_chats AS ( UPDATE chats SET status = 'running'::chat_status, started_at = $1::timestamptz, heartbeat_at = $1::timestamptz, updated_at = $1::timestamptz, worker_id = $2::uuid WHERE id = ANY( SELECT id FROM chats WHERE status = 'pending'::chat_status AND archived = false ORDER BY updated_at ASC FOR UPDATE SKIP LOCKED LIMIT $3::int ) RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl ), chats_expanded AS ( SELECT acquired_chats.id, acquired_chats.owner_id, acquired_chats.workspace_id, acquired_chats.title, acquired_chats.status, acquired_chats.worker_id, acquired_chats.started_at, acquired_chats.heartbeat_at, acquired_chats.created_at, acquired_chats.updated_at, acquired_chats.parent_chat_id, acquired_chats.root_chat_id, acquired_chats.last_model_config_id, acquired_chats.archived, acquired_chats.last_error, acquired_chats.mode, acquired_chats.mcp_server_ids, acquired_chats.labels, acquired_chats.build_id, acquired_chats.agent_id, acquired_chats.pin_order, acquired_chats.last_read_message_id, acquired_chats.last_injected_context, acquired_chats.dynamic_tools, acquired_chats.organization_id, acquired_chats.plan_mode, acquired_chats.client_type, acquired_chats.last_turn_summary, COALESCE(root.user_acl, acquired_chats.user_acl) AS user_acl, COALESCE(root.group_acl, acquired_chats.group_acl) AS group_acl, owner.username AS owner_username, owner.name AS owner_name FROM acquired_chats LEFT JOIN chats root ON root.id = COALESCE(acquired_chats.root_chat_id, acquired_chats.parent_chat_id) JOIN visible_users owner ON owner.id = acquired_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, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name FROM chats_expanded ` type AcquireChatsParams struct { StartedAt time.Time `db:"started_at" json:"started_at"` WorkerID uuid.UUID `db:"worker_id" json:"worker_id"` NumChats int32 `db:"num_chats" json:"num_chats"` } // Acquires up to @num_chats pending chats for processing. Uses SKIP LOCKED // to prevent multiple replicas from acquiring the same chat. func (q *sqlQuerier) AcquireChats(ctx context.Context, arg AcquireChatsParams) ([]Chat, error) { rows, err := q.db.QueryContext(ctx, acquireChats, arg.StartedAt, arg.WorkerID, arg.NumChats) 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.Archived, &i.LastError, &i.Mode, pq.Array(&i.MCPServerIDs), &i.Labels, &i.BuildID, &i.AgentID, &i.PinOrder, &i.LastReadMessageID, &i.LastInjectedContext, &i.DynamicTools, &i.OrganizationID, &i.PlanMode, &i.ClientType, &i.LastTurnSummary, &i.UserACL, &i.GroupACL, &i.OwnerUsername, &i.OwnerName, ); 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 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, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl ), 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.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.last_injected_context, updated_chats.dynamic_tools, updated_chats.organization_id, updated_chats.plan_mode, updated_chats.client_type, updated_chats.last_turn_summary, 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 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, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name 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.Archived, &i.LastError, &i.Mode, pq.Array(&i.MCPServerIDs), &i.Labels, &i.BuildID, &i.AgentID, &i.PinOrder, &i.LastReadMessageID, &i.LastInjectedContext, &i.DynamicTools, &i.OrganizationID, &i.PlanMode, &i.ClientType, &i.LastTurnSummary, &i.UserACL, &i.GroupACL, &i.OwnerUsername, &i.OwnerName, ); 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', 'pending', 'paused', '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.last_injected_context, c.dynamic_tools, c.organization_id, c.plan_mode, c.client_type, c.last_turn_summary, c.user_acl, c.group_acl ) 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.last_injected_context, a.dynamic_tools, a.organization_id, a.plan_mode, a.client_type, a.last_turn_summary, a.user_acl, a.group_acl, -- 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"` LastInjectedContext pqtype.NullRawMessage `db:"last_injected_context" json:"last_injected_context"` 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"` 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.LastInjectedContext, &i.DynamicTools, &i.OrganizationID, &i.PlanMode, &i.ClientType, &i.LastTurnSummary, &i.UserACL, &i.GroupACL, &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 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 clearChatMessageProviderResponseIDsByChatID = `-- name: ClearChatMessageProviderResponseIDsByChatID :exec UPDATE chat_messages SET provider_response_id = NULL WHERE chat_id = $1::uuid AND deleted = false AND provider_response_id IS NOT NULL ` func (q *sqlQuerier) ClearChatMessageProviderResponseIDsByChatID(ctx context.Context, chatID uuid.UUID) error { _, err := q.db.ExecContext(ctx, clearChatMessageProviderResponseIDsByChatID, chatID) return err } const countEnabledModelsWithoutPricing = `-- name: CountEnabledModelsWithoutPricing :one SELECT COUNT(*)::bigint AS count FROM chat_model_configs WHERE enabled = TRUE AND deleted = FALSE AND ( options->'cost' IS NULL OR options->'cost' = 'null'::jsonb OR ( (options->'cost'->>'input_price_per_million_tokens' IS NULL) AND (options->'cost'->>'output_price_per_million_tokens' IS NULL) ) ) ` // Counts enabled, non-deleted model configs that lack both input and // output pricing in their JSONB options.cost configuration. func (q *sqlQuerier) CountEnabledModelsWithoutPricing(ctx context.Context) (int64, error) { row := q.db.QueryRowContext(ctx, countEnabledModelsWithoutPricing) var count int64 err := row.Scan(&count) return count, 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 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 deleteChatUsageLimitGroupOverride = `-- name: DeleteChatUsageLimitGroupOverride :exec UPDATE groups SET chat_spend_limit_micros = NULL WHERE id = $1::uuid ` func (q *sqlQuerier) DeleteChatUsageLimitGroupOverride(ctx context.Context, groupID uuid.UUID) error { _, err := q.db.ExecContext(ctx, deleteChatUsageLimitGroupOverride, groupID) return err } const deleteChatUsageLimitUserOverride = `-- name: DeleteChatUsageLimitUserOverride :exec UPDATE users SET chat_spend_limit_micros = NULL WHERE id = $1::uuid ` func (q *sqlQuerier) DeleteChatUsageLimitUserOverride(ctx context.Context, userID uuid.UUID) error { _, err := q.db.ExecContext(ctx, deleteChatUsageLimitUserOverride, userID) return err } 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. // Related chat_messages, chat_diff_statuses, and // chat_queued_messages 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 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, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name FROM chats_expanded WHERE agent_id = $1::uuid AND archived = false -- Active statuses only: waiting, pending, running, paused, -- requires_action. -- Excludes completed and error (terminal states). AND status IN ('waiting', 'running', 'paused', 'pending', '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.Archived, &i.LastError, &i.Mode, pq.Array(&i.MCPServerIDs), &i.Labels, &i.BuildID, &i.AgentID, &i.PinOrder, &i.LastReadMessageID, &i.LastInjectedContext, &i.DynamicTools, &i.OrganizationID, &i.PlanMode, &i.ClientType, &i.LastTurnSummary, &i.UserACL, &i.GroupACL, &i.OwnerUsername, &i.OwnerName, ); 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, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name 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.Archived, &i.LastError, &i.Mode, pq.Array(&i.MCPServerIDs), &i.Labels, &i.BuildID, &i.AgentID, &i.PinOrder, &i.LastReadMessageID, &i.LastInjectedContext, &i.DynamicTools, &i.OrganizationID, &i.PlanMode, &i.ClientType, &i.LastTurnSummary, &i.UserACL, &i.GroupACL, &i.OwnerUsername, &i.OwnerName, ) 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, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl 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.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.last_injected_context, locked_chat.dynamic_tools, locked_chat.organization_id, locked_chat.plan_mode, locked_chat.client_type, locked_chat.last_turn_summary, 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 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, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name 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.Archived, &i.LastError, &i.Mode, pq.Array(&i.MCPServerIDs), &i.Labels, &i.BuildID, &i.AgentID, &i.PinOrder, &i.LastReadMessageID, &i.LastInjectedContext, &i.DynamicTools, &i.OrganizationID, &i.PlanMode, &i.ClientType, &i.LastTurnSummary, &i.UserACL, &i.GroupACL, &i.OwnerUsername, &i.OwnerName, ) return i, err } const getChatCostPerChat = `-- name: GetChatCostPerChat :many WITH chat_costs AS ( SELECT COALESCE(c.root_chat_id, c.id) AS root_chat_id, COALESCE(SUM(cm.total_cost_micros), 0)::bigint AS total_cost_micros, COUNT(*) FILTER ( WHERE cm.input_tokens IS NOT NULL OR cm.output_tokens IS NOT NULL OR cm.reasoning_tokens IS NOT NULL OR cm.cache_creation_tokens IS NOT NULL OR cm.cache_read_tokens IS NOT NULL )::bigint AS 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.cache_read_tokens), 0)::bigint AS total_cache_read_tokens, COALESCE(SUM(cm.cache_creation_tokens), 0)::bigint AS total_cache_creation_tokens, COALESCE(SUM(cm.runtime_ms), 0)::bigint AS total_runtime_ms FROM chat_messages cm JOIN chats c ON c.id = cm.chat_id WHERE c.owner_id = $1::uuid AND cm.role = 'assistant' AND cm.created_at >= $2::timestamptz AND cm.created_at < $3::timestamptz GROUP BY COALESCE(c.root_chat_id, c.id) ) SELECT cc.root_chat_id, COALESCE(rc.title, '') AS chat_title, cc.total_cost_micros, cc.message_count, cc.total_input_tokens, cc.total_output_tokens, cc.total_cache_read_tokens, cc.total_cache_creation_tokens, cc.total_runtime_ms FROM chat_costs cc LEFT JOIN chats rc ON rc.id = cc.root_chat_id ORDER BY cc.total_cost_micros DESC ` type GetChatCostPerChatParams struct { OwnerID uuid.UUID `db:"owner_id" json:"owner_id"` StartDate time.Time `db:"start_date" json:"start_date"` EndDate time.Time `db:"end_date" json:"end_date"` } type GetChatCostPerChatRow struct { RootChatID uuid.UUID `db:"root_chat_id" json:"root_chat_id"` ChatTitle string `db:"chat_title" json:"chat_title"` TotalCostMicros int64 `db:"total_cost_micros" json:"total_cost_micros"` MessageCount int64 `db:"message_count" json:"message_count"` TotalInputTokens int64 `db:"total_input_tokens" json:"total_input_tokens"` TotalOutputTokens int64 `db:"total_output_tokens" json:"total_output_tokens"` TotalCacheReadTokens int64 `db:"total_cache_read_tokens" json:"total_cache_read_tokens"` TotalCacheCreationTokens int64 `db:"total_cache_creation_tokens" json:"total_cache_creation_tokens"` TotalRuntimeMs int64 `db:"total_runtime_ms" json:"total_runtime_ms"` } // Per-root-chat cost breakdown for a single user within a date range. // Groups by root_chat_id so forked chats roll up under their root. // Only counts assistant-role messages. func (q *sqlQuerier) GetChatCostPerChat(ctx context.Context, arg GetChatCostPerChatParams) ([]GetChatCostPerChatRow, error) { rows, err := q.db.QueryContext(ctx, getChatCostPerChat, arg.OwnerID, arg.StartDate, arg.EndDate) if err != nil { return nil, err } defer rows.Close() var items []GetChatCostPerChatRow for rows.Next() { var i GetChatCostPerChatRow if err := rows.Scan( &i.RootChatID, &i.ChatTitle, &i.TotalCostMicros, &i.MessageCount, &i.TotalInputTokens, &i.TotalOutputTokens, &i.TotalCacheReadTokens, &i.TotalCacheCreationTokens, &i.TotalRuntimeMs, ); 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 getChatCostPerModel = `-- name: GetChatCostPerModel :many SELECT cmc.id AS model_config_id, cmc.display_name, cmc.provider, cmc.model, COALESCE(SUM(cm.total_cost_micros), 0)::bigint AS total_cost_micros, COUNT(*) FILTER ( WHERE cm.input_tokens IS NOT NULL OR cm.output_tokens IS NOT NULL OR cm.reasoning_tokens IS NOT NULL OR cm.cache_creation_tokens IS NOT NULL OR cm.cache_read_tokens IS NOT NULL )::bigint AS 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.cache_read_tokens), 0)::bigint AS total_cache_read_tokens, COALESCE(SUM(cm.cache_creation_tokens), 0)::bigint AS total_cache_creation_tokens, COALESCE(SUM(cm.runtime_ms), 0)::bigint AS total_runtime_ms FROM chat_messages cm JOIN chats c ON c.id = cm.chat_id JOIN chat_model_configs cmc ON cmc.id = cm.model_config_id WHERE c.owner_id = $1::uuid AND cm.role = 'assistant' AND cm.created_at >= $2::timestamptz AND cm.created_at < $3::timestamptz GROUP BY cmc.id, cmc.display_name, cmc.provider, cmc.model ORDER BY total_cost_micros DESC ` type GetChatCostPerModelParams struct { OwnerID uuid.UUID `db:"owner_id" json:"owner_id"` StartDate time.Time `db:"start_date" json:"start_date"` EndDate time.Time `db:"end_date" json:"end_date"` } type GetChatCostPerModelRow struct { ModelConfigID uuid.UUID `db:"model_config_id" json:"model_config_id"` DisplayName string `db:"display_name" json:"display_name"` Provider string `db:"provider" json:"provider"` Model string `db:"model" json:"model"` TotalCostMicros int64 `db:"total_cost_micros" json:"total_cost_micros"` MessageCount int64 `db:"message_count" json:"message_count"` TotalInputTokens int64 `db:"total_input_tokens" json:"total_input_tokens"` TotalOutputTokens int64 `db:"total_output_tokens" json:"total_output_tokens"` TotalCacheReadTokens int64 `db:"total_cache_read_tokens" json:"total_cache_read_tokens"` TotalCacheCreationTokens int64 `db:"total_cache_creation_tokens" json:"total_cache_creation_tokens"` TotalRuntimeMs int64 `db:"total_runtime_ms" json:"total_runtime_ms"` } // Per-model cost breakdown for a single user within a date range. // Only counts assistant-role messages that have a model_config_id. func (q *sqlQuerier) GetChatCostPerModel(ctx context.Context, arg GetChatCostPerModelParams) ([]GetChatCostPerModelRow, error) { rows, err := q.db.QueryContext(ctx, getChatCostPerModel, arg.OwnerID, arg.StartDate, arg.EndDate) if err != nil { return nil, err } defer rows.Close() var items []GetChatCostPerModelRow for rows.Next() { var i GetChatCostPerModelRow if err := rows.Scan( &i.ModelConfigID, &i.DisplayName, &i.Provider, &i.Model, &i.TotalCostMicros, &i.MessageCount, &i.TotalInputTokens, &i.TotalOutputTokens, &i.TotalCacheReadTokens, &i.TotalCacheCreationTokens, &i.TotalRuntimeMs, ); 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 getChatCostPerUser = `-- name: GetChatCostPerUser :many WITH chat_cost_users AS ( SELECT c.owner_id AS user_id, u.username, u.name, u.avatar_url, COALESCE(SUM(cm.total_cost_micros), 0)::bigint AS total_cost_micros, COUNT(*) FILTER ( WHERE cm.input_tokens IS NOT NULL OR cm.output_tokens IS NOT NULL OR cm.reasoning_tokens IS NOT NULL OR cm.cache_creation_tokens IS NOT NULL OR cm.cache_read_tokens IS NOT NULL )::bigint AS message_count, COUNT(DISTINCT COALESCE(c.root_chat_id, c.id))::bigint AS chat_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.cache_read_tokens), 0)::bigint AS total_cache_read_tokens, COALESCE(SUM(cm.cache_creation_tokens), 0)::bigint AS total_cache_creation_tokens, COALESCE(SUM(cm.runtime_ms), 0)::bigint AS total_runtime_ms FROM chat_messages cm JOIN chats c ON c.id = cm.chat_id JOIN users u ON u.id = c.owner_id WHERE cm.role = 'assistant' AND cm.created_at >= $3::timestamptz AND cm.created_at < $4::timestamptz AND ( $5::text = '' OR u.username ILIKE '%' || $5::text || '%' OR u.name ILIKE '%' || $5::text || '%' ) GROUP BY c.owner_id, u.username, u.name, u.avatar_url ) SELECT user_id, username, name, avatar_url, total_cost_micros, message_count, chat_count, total_input_tokens, total_output_tokens, total_cache_read_tokens, total_cache_creation_tokens, total_runtime_ms, COUNT(*) OVER()::bigint AS total_count FROM chat_cost_users ORDER BY total_cost_micros DESC, username ASC LIMIT $2::int OFFSET $1::int ` type GetChatCostPerUserParams struct { PageOffset int32 `db:"page_offset" json:"page_offset"` PageLimit int32 `db:"page_limit" json:"page_limit"` StartDate time.Time `db:"start_date" json:"start_date"` EndDate time.Time `db:"end_date" json:"end_date"` Username string `db:"username" json:"username"` } type GetChatCostPerUserRow struct { UserID uuid.UUID `db:"user_id" json:"user_id"` Username string `db:"username" json:"username"` Name string `db:"name" json:"name"` AvatarURL string `db:"avatar_url" json:"avatar_url"` TotalCostMicros int64 `db:"total_cost_micros" json:"total_cost_micros"` MessageCount int64 `db:"message_count" json:"message_count"` ChatCount int64 `db:"chat_count" json:"chat_count"` TotalInputTokens int64 `db:"total_input_tokens" json:"total_input_tokens"` TotalOutputTokens int64 `db:"total_output_tokens" json:"total_output_tokens"` TotalCacheReadTokens int64 `db:"total_cache_read_tokens" json:"total_cache_read_tokens"` TotalCacheCreationTokens int64 `db:"total_cache_creation_tokens" json:"total_cache_creation_tokens"` TotalRuntimeMs int64 `db:"total_runtime_ms" json:"total_runtime_ms"` TotalCount int64 `db:"total_count" json:"total_count"` } // Deployment-wide per-user cost rollup within a date range. // Only counts assistant-role messages. func (q *sqlQuerier) GetChatCostPerUser(ctx context.Context, arg GetChatCostPerUserParams) ([]GetChatCostPerUserRow, error) { rows, err := q.db.QueryContext(ctx, getChatCostPerUser, arg.PageOffset, arg.PageLimit, arg.StartDate, arg.EndDate, arg.Username, ) if err != nil { return nil, err } defer rows.Close() var items []GetChatCostPerUserRow for rows.Next() { var i GetChatCostPerUserRow if err := rows.Scan( &i.UserID, &i.Username, &i.Name, &i.AvatarURL, &i.TotalCostMicros, &i.MessageCount, &i.ChatCount, &i.TotalInputTokens, &i.TotalOutputTokens, &i.TotalCacheReadTokens, &i.TotalCacheCreationTokens, &i.TotalRuntimeMs, &i.TotalCount, ); 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 getChatCostSummary = `-- name: GetChatCostSummary :one SELECT COALESCE(SUM(cm.total_cost_micros), 0)::bigint AS total_cost_micros, COUNT(*) FILTER ( WHERE cm.total_cost_micros IS NOT NULL )::bigint AS priced_message_count, COUNT(*) FILTER ( WHERE cm.total_cost_micros IS NULL AND ( cm.input_tokens IS NOT NULL OR cm.output_tokens IS NOT NULL OR cm.reasoning_tokens IS NOT NULL OR cm.cache_creation_tokens IS NOT NULL OR cm.cache_read_tokens IS NOT NULL ) )::bigint AS unpriced_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.cache_read_tokens), 0)::bigint AS total_cache_read_tokens, COALESCE(SUM(cm.cache_creation_tokens), 0)::bigint AS total_cache_creation_tokens, COALESCE(SUM(cm.runtime_ms), 0)::bigint AS total_runtime_ms FROM chat_messages cm JOIN chats c ON c.id = cm.chat_id WHERE c.owner_id = $1::uuid AND cm.role = 'assistant' AND cm.created_at >= $2::timestamptz AND cm.created_at < $3::timestamptz ` type GetChatCostSummaryParams struct { OwnerID uuid.UUID `db:"owner_id" json:"owner_id"` StartDate time.Time `db:"start_date" json:"start_date"` EndDate time.Time `db:"end_date" json:"end_date"` } type GetChatCostSummaryRow struct { TotalCostMicros int64 `db:"total_cost_micros" json:"total_cost_micros"` PricedMessageCount int64 `db:"priced_message_count" json:"priced_message_count"` UnpricedMessageCount int64 `db:"unpriced_message_count" json:"unpriced_message_count"` TotalInputTokens int64 `db:"total_input_tokens" json:"total_input_tokens"` TotalOutputTokens int64 `db:"total_output_tokens" json:"total_output_tokens"` TotalCacheReadTokens int64 `db:"total_cache_read_tokens" json:"total_cache_read_tokens"` TotalCacheCreationTokens int64 `db:"total_cache_creation_tokens" json:"total_cache_creation_tokens"` TotalRuntimeMs int64 `db:"total_runtime_ms" json:"total_runtime_ms"` } // Aggregate cost summary for a single user within a date range. // Only counts assistant-role messages. func (q *sqlQuerier) GetChatCostSummary(ctx context.Context, arg GetChatCostSummaryParams) (GetChatCostSummaryRow, error) { row := q.db.QueryRowContext(ctx, getChatCostSummary, arg.OwnerID, arg.StartDate, arg.EndDate) var i GetChatCostSummaryRow err := row.Scan( &i.TotalCostMicros, &i.PricedMessageCount, &i.UnpricedMessageCount, &i.TotalInputTokens, &i.TotalOutputTokens, &i.TotalCacheReadTokens, &i.TotalCacheCreationTokens, &i.TotalRuntimeMs, ) 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 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, api_key_id 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.APIKeyID, ) 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.total_cost_micros), 0)::bigint AS total_cost_micros, 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"` TotalCostMicros int64 `db:"total_cost_micros" json:"total_cost_micros"` 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.TotalCostMicros, &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, api_key_id FROM chat_messages WHERE chat_id = $1::uuid AND id > $2::bigint AND visibility IN ('user', 'both') AND deleted = false ORDER BY created_at ASC ` type GetChatMessagesByChatIDParams struct { ChatID uuid.UUID `db:"chat_id" json:"chat_id"` AfterID int64 `db:"after_id" json:"after_id"` } 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.APIKeyID, ); 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, api_key_id 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.APIKeyID, ); 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, api_key_id 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.APIKeyID, ); 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 created_at DESC, 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, api_key_id 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 created_at ASC, id ASC ` 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.APIKeyID, ); 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 id, provider, model, context_limit, enabled, is_default FROM chat_model_configs WHERE 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. 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 getChatQueuedMessages = `-- name: GetChatQueuedMessages :many SELECT id, chat_id, content, created_at, model_config_id, api_key_id 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.APIKeyID, ); 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 getChatUsageLimitConfig = `-- name: GetChatUsageLimitConfig :one SELECT id, singleton, enabled, default_limit_micros, period, created_at, updated_at FROM chat_usage_limit_config WHERE singleton = TRUE LIMIT 1 ` func (q *sqlQuerier) GetChatUsageLimitConfig(ctx context.Context) (ChatUsageLimitConfig, error) { row := q.db.QueryRowContext(ctx, getChatUsageLimitConfig) var i ChatUsageLimitConfig err := row.Scan( &i.ID, &i.Singleton, &i.Enabled, &i.DefaultLimitMicros, &i.Period, &i.CreatedAt, &i.UpdatedAt, ) return i, err } const getChatUsageLimitGroupOverride = `-- name: GetChatUsageLimitGroupOverride :one SELECT id AS group_id, chat_spend_limit_micros AS spend_limit_micros FROM groups WHERE id = $1::uuid AND chat_spend_limit_micros IS NOT NULL ` type GetChatUsageLimitGroupOverrideRow struct { GroupID uuid.UUID `db:"group_id" json:"group_id"` SpendLimitMicros sql.NullInt64 `db:"spend_limit_micros" json:"spend_limit_micros"` } func (q *sqlQuerier) GetChatUsageLimitGroupOverride(ctx context.Context, groupID uuid.UUID) (GetChatUsageLimitGroupOverrideRow, error) { row := q.db.QueryRowContext(ctx, getChatUsageLimitGroupOverride, groupID) var i GetChatUsageLimitGroupOverrideRow err := row.Scan(&i.GroupID, &i.SpendLimitMicros) return i, err } const getChatUsageLimitUserOverride = `-- name: GetChatUsageLimitUserOverride :one SELECT id AS user_id, chat_spend_limit_micros AS spend_limit_micros FROM users WHERE id = $1::uuid AND chat_spend_limit_micros IS NOT NULL ` type GetChatUsageLimitUserOverrideRow struct { UserID uuid.UUID `db:"user_id" json:"user_id"` SpendLimitMicros sql.NullInt64 `db:"spend_limit_micros" json:"spend_limit_micros"` } func (q *sqlQuerier) GetChatUsageLimitUserOverride(ctx context.Context, userID uuid.UUID) (GetChatUsageLimitUserOverrideRow, error) { row := q.db.QueryRowContext(ctx, getChatUsageLimitUserOverride, userID) var i GetChatUsageLimitUserOverrideRow err := row.Scan(&i.UserID, &i.SpendLimitMicros) return i, err } 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 getChats = `-- name: GetChats :many WITH cursor_chat AS ( SELECT pin_order, updated_at, id FROM chats WHERE id = $5 ) 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.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.last_injected_context, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.user_acl, chats_expanded.group_acl, chats_expanded.owner_username, chats_expanded.owner_name, 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 CASE WHEN $1::boolean THEN chats_expanded.owner_id = $2::uuid ELSE true END AND CASE WHEN $3::boolean THEN chats_expanded.owner_id != $2::uuid ELSE true END AND CASE WHEN $4 :: boolean IS NULL THEN true ELSE chats_expanded.archived = $4 :: 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 $5 :: 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 $6::jsonb IS NOT NULL THEN chats_expanded.labels @> $6::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 $7::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($7::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 $8 :: text != '' THEN chats_expanded.title ILIKE '%' || $8 || '%' ELSE true END AND CASE WHEN $9::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) ) ) = $9::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($10::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($10::text[]) ) ELSE true END -- Filter by PR number (exact match on chat's diff status). AND CASE WHEN $11::int != 0 THEN EXISTS ( SELECT 1 FROM chat_diff_statuses cds WHERE cds.chat_id = chats_expanded.id AND cds.pr_number = $11 ) ELSE true END -- Filter by repository (substring match on remote origin or PR URL). AND CASE WHEN $12::text != '' THEN EXISTS ( SELECT 1 FROM chat_diff_statuses cds WHERE cds.chat_id = chats_expanded.id AND ( cds.git_remote_origin ILIKE '%' || $12 || '%' OR cds.url ILIKE '%' || $12 || '%' ) ) ELSE true END -- Filter by pull request title (case-insensitive substring). AND CASE WHEN $13::text != '' THEN EXISTS ( SELECT 1 FROM chat_diff_statuses cds WHERE cds.chat_id = chats_expanded.id AND cds.pull_request_title ILIKE '%' || $13 || '%' ) 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 $14 LIMIT -- The chat list is unbounded and expected to grow large. -- Default to 50 to prevent accidental excessively large queries. COALESCE(NULLIF($15 :: int, 0), 50) ` type GetChatsParams struct { OwnedOnly bool `db:"owned_only" json:"owned_only"` ViewerID uuid.UUID `db:"viewer_id" json:"viewer_id"` SharedOnly bool `db:"shared_only" json:"shared_only"` 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"` 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.ViewerID, arg.SharedOnly, arg.Archived, arg.AfterID, arg.LabelFilter, arg.DiffURL, arg.TitleQuery, arg.HasUnread, pq.Array(arg.PullRequestStatuses), arg.PrNumber, arg.RepoQuery, arg.PrTitleQuery, 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.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.LastInjectedContext, &i.Chat.DynamicTools, &i.Chat.OrganizationID, &i.Chat.PlanMode, &i.Chat.ClientType, &i.Chat.LastTurnSummary, &i.Chat.UserACL, &i.Chat.GroupACL, &i.Chat.OwnerUsername, &i.Chat.OwnerName, &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, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name 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.Archived, &i.LastError, &i.Mode, pq.Array(&i.MCPServerIDs), &i.Labels, &i.BuildID, &i.AgentID, &i.PinOrder, &i.LastReadMessageID, &i.LastInjectedContext, &i.DynamicTools, &i.OrganizationID, &i.PlanMode, &i.ClientType, &i.LastTurnSummary, &i.UserACL, &i.GroupACL, &i.OwnerUsername, &i.OwnerName, ); 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, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name 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.Archived, &i.LastError, &i.Mode, pq.Array(&i.MCPServerIDs), &i.Labels, &i.BuildID, &i.AgentID, &i.PinOrder, &i.LastReadMessageID, &i.LastInjectedContext, &i.DynamicTools, &i.OrganizationID, &i.PlanMode, &i.ClientType, &i.LastTurnSummary, &i.UserACL, &i.GroupACL, &i.OwnerUsername, &i.OwnerName, ); 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.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.last_injected_context, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.user_acl, chats_expanded.group_acl, chats_expanded.owner_username, chats_expanded.owner_name, 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.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.LastInjectedContext, &i.Chat.DynamicTools, &i.Chat.OrganizationID, &i.Chat.PlanMode, &i.Chat.ClientType, &i.Chat.LastTurnSummary, &i.Chat.UserACL, &i.Chat.GroupACL, &i.Chat.OwnerUsername, &i.Chat.OwnerName, &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 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, api_key_id FROM chat_messages WHERE chat_id = $1::uuid AND role = $2::chat_message_role AND deleted = false ORDER BY created_at DESC, id DESC LIMIT 1 ` type GetLastChatMessageByRoleParams struct { ChatID uuid.UUID `db:"chat_id" json:"chat_id"` Role ChatMessageRole `db:"role" json:"role"` } 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.APIKeyID, ) 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, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name 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.Archived, &i.LastError, &i.Mode, pq.Array(&i.MCPServerIDs), &i.Labels, &i.BuildID, &i.AgentID, &i.PinOrder, &i.LastReadMessageID, &i.LastInjectedContext, &i.DynamicTools, &i.OrganizationID, &i.PlanMode, &i.ClientType, &i.LastTurnSummary, &i.UserACL, &i.GroupACL, &i.OwnerUsername, &i.OwnerName, ); 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 getUserChatSpendInPeriod = `-- name: GetUserChatSpendInPeriod :one SELECT COALESCE(SUM(cm.total_cost_micros), 0)::bigint AS total_spend_micros FROM chat_messages cm JOIN chats c ON c.id = cm.chat_id WHERE c.owner_id = $1::uuid AND ($2::uuid IS NULL OR c.organization_id = $2::uuid) AND cm.created_at >= $3::timestamptz AND cm.created_at < $4::timestamptz AND cm.total_cost_micros IS NOT NULL ` type GetUserChatSpendInPeriodParams struct { UserID uuid.UUID `db:"user_id" json:"user_id"` OrganizationID uuid.NullUUID `db:"organization_id" json:"organization_id"` StartTime time.Time `db:"start_time" json:"start_time"` EndTime time.Time `db:"end_time" json:"end_time"` } // Returns the total spend for a user in the given period. // When organization_id is NULL, spend across all organizations is // returned (global behavior). Otherwise only spend within the // specified organization is included. func (q *sqlQuerier) GetUserChatSpendInPeriod(ctx context.Context, arg GetUserChatSpendInPeriodParams) (int64, error) { row := q.db.QueryRowContext(ctx, getUserChatSpendInPeriod, arg.UserID, arg.OrganizationID, arg.StartTime, arg.EndTime, ) var total_spend_micros int64 err := row.Scan(&total_spend_micros) return total_spend_micros, err } const getUserGroupSpendLimit = `-- name: GetUserGroupSpendLimit :one SELECT COALESCE(MIN(g.chat_spend_limit_micros), -1)::bigint AS limit_micros FROM groups g JOIN group_members_expanded gme ON gme.group_id = g.id WHERE gme.user_id = $1::uuid AND ($2::uuid IS NULL OR g.organization_id = $2::uuid) AND g.chat_spend_limit_micros IS NOT NULL ` type GetUserGroupSpendLimitParams struct { UserID uuid.UUID `db:"user_id" json:"user_id"` OrganizationID uuid.NullUUID `db:"organization_id" json:"organization_id"` } // Returns the minimum (most restrictive) group limit for a user. // Returns -1 if no group limits match the specified scope. // When organization_id is NULL, groups across all organizations are // considered (global behavior). Otherwise only groups within the // specified organization are considered. func (q *sqlQuerier) GetUserGroupSpendLimit(ctx context.Context, arg GetUserGroupSpendLimitParams) (int64, error) { row := q.db.QueryRowContext(ctx, getUserGroupSpendLimit, arg.UserID, arg.OrganizationID) var limit_micros int64 err := row.Scan(&limit_micros) return limit_micros, err } const insertChat = `-- name: InsertChat :one WITH inserted_chat AS ( INSERT INTO chats ( 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 ( $1::uuid, $2::uuid, $3::uuid, $4::uuid, $5::uuid, $6::uuid, $7::uuid, $8::uuid, $9::text, $10::chat_mode, $11::chat_plan_mode, $12::chat_status, COALESCE($13::uuid[], '{}'::uuid[]), COALESCE($14::jsonb, '{}'::jsonb), $15::jsonb, $16::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, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl ), 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.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.last_injected_context, inserted_chat.dynamic_tools, inserted_chat.organization_id, inserted_chat.plan_mode, inserted_chat.client_type, inserted_chat.last_turn_summary, 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 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, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name FROM chats_expanded ` type InsertChatParams struct { 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.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.Archived, &i.LastError, &i.Mode, pq.Array(&i.MCPServerIDs), &i.Labels, &i.BuildID, &i.AgentID, &i.PinOrder, &i.LastReadMessageID, &i.LastInjectedContext, &i.DynamicTools, &i.OrganizationID, &i.PlanMode, &i.ClientType, &i.LastTurnSummary, &i.UserACL, &i.GroupACL, &i.OwnerUsername, &i.OwnerName, ) return i, err } const insertChatMessages = `-- name: InsertChatMessages :many WITH updated_chat AS ( UPDATE chats SET last_model_config_id = ( SELECT val FROM UNNEST($4::uuid[]) WITH ORDINALITY AS t(val, ord) WHERE val != '00000000-0000-0000-0000-000000000000'::uuid ORDER BY ord DESC LIMIT 1 ) WHERE id = $1::uuid AND EXISTS ( SELECT 1 FROM UNNEST($4::uuid[]) WHERE unnest != '00000000-0000-0000-0000-000000000000'::uuid ) AND chats.last_model_config_id IS DISTINCT FROM ( SELECT val FROM UNNEST($4::uuid[]) WITH ORDINALITY AS t(val, ord) WHERE val != '00000000-0000-0000-0000-000000000000'::uuid ORDER BY ord DESC LIMIT 1 ) ) INSERT INTO chat_messages ( chat_id, created_by, api_key_id, model_config_id, role, content, content_version, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, total_cost_micros, runtime_ms, provider_response_id ) SELECT $1::uuid, NULLIF(UNNEST($2::uuid[]), '00000000-0000-0000-0000-000000000000'::uuid), NULLIF(UNNEST($3::text[]), ''), NULLIF(UNNEST($4::uuid[]), '00000000-0000-0000-0000-000000000000'::uuid), UNNEST($5::chat_message_role[]), UNNEST($6::text[])::jsonb, UNNEST($7::smallint[]), UNNEST($8::chat_message_visibility[]), NULLIF(UNNEST($9::bigint[]), 0), NULLIF(UNNEST($10::bigint[]), 0), NULLIF(UNNEST($11::bigint[]), 0), NULLIF(UNNEST($12::bigint[]), 0), NULLIF(UNNEST($13::bigint[]), 0), NULLIF(UNNEST($14::bigint[]), 0), NULLIF(UNNEST($15::bigint[]), 0), UNNEST($16::boolean[]), NULLIF(UNNEST($17::bigint[]), 0), NULLIF(UNNEST($18::bigint[]), 0), NULLIF(UNNEST($19::text[]), '') 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, api_key_id ` type InsertChatMessagesParams struct { ChatID uuid.UUID `db:"chat_id" json:"chat_id"` CreatedBy []uuid.UUID `db:"created_by" json:"created_by"` APIKeyID []string `db:"api_key_id" json:"api_key_id"` ModelConfigID []uuid.UUID `db:"model_config_id" json:"model_config_id"` Role []ChatMessageRole `db:"role" json:"role"` 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"` TotalCostMicros []int64 `db:"total_cost_micros" json:"total_cost_micros"` RuntimeMs []int64 `db:"runtime_ms" json:"runtime_ms"` ProviderResponseID []string `db:"provider_response_id" json:"provider_response_id"` } func (q *sqlQuerier) InsertChatMessages(ctx context.Context, arg InsertChatMessagesParams) ([]ChatMessage, error) { rows, err := q.db.QueryContext(ctx, insertChatMessages, arg.ChatID, pq.Array(arg.CreatedBy), pq.Array(arg.APIKeyID), pq.Array(arg.ModelConfigID), pq.Array(arg.Role), 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.TotalCostMicros), pq.Array(arg.RuntimeMs), pq.Array(arg.ProviderResponseID), ) 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.APIKeyID, ); 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, api_key_id) VALUES ( $1, $2, $3::uuid, $4::text ) RETURNING id, chat_id, content, created_at, model_config_id, api_key_id ` 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"` APIKeyID sql.NullString `db:"api_key_id" json:"api_key_id"` } func (q *sqlQuerier) InsertChatQueuedMessage(ctx context.Context, arg InsertChatQueuedMessageParams) (ChatQueuedMessage, error) { row := q.db.QueryRowContext(ctx, insertChatQueuedMessage, arg.ChatID, arg.Content, arg.ModelConfigID, arg.APIKeyID, ) var i ChatQueuedMessage err := row.Scan( &i.ID, &i.ChatID, &i.Content, &i.CreatedAt, &i.ModelConfigID, &i.APIKeyID, ) return i, err } const linkChatFiles = `-- name: LinkChatFiles :one WITH current AS ( SELECT COUNT(*) AS cnt FROM chat_file_links WHERE chat_id = $1::uuid ), new_links AS ( SELECT $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 LinkChatFilesParams 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"` } // LinkChatFiles inserts file associations into the chat_file_links // join table with deduplication (ON CONFLICT DO NOTHING). The INSERT // is conditional: it only proceeds when the total number of links // (existing + genuinely new) does not exceed max_file_links. Returns // the number of genuinely new file IDs that were NOT inserted due to // the cap. A return value of 0 means all files were linked (or were // already linked). A positive value means the cap blocked that many // new links. func (q *sqlQuerier) LinkChatFiles(ctx context.Context, arg LinkChatFilesParams) (int32, error) { row := q.db.QueryRowContext(ctx, linkChatFiles, 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 listChatUsageLimitGroupOverrides = `-- name: ListChatUsageLimitGroupOverrides :many SELECT g.id AS group_id, g.name AS group_name, g.display_name AS group_display_name, g.avatar_url AS group_avatar_url, g.chat_spend_limit_micros AS spend_limit_micros, (SELECT COUNT(*) FROM group_members_expanded gme WHERE gme.group_id = g.id AND gme.user_is_system = FALSE) AS member_count FROM groups g WHERE g.chat_spend_limit_micros IS NOT NULL ORDER BY g.name ASC ` type ListChatUsageLimitGroupOverridesRow struct { GroupID uuid.UUID `db:"group_id" json:"group_id"` GroupName string `db:"group_name" json:"group_name"` GroupDisplayName string `db:"group_display_name" json:"group_display_name"` GroupAvatarUrl string `db:"group_avatar_url" json:"group_avatar_url"` SpendLimitMicros sql.NullInt64 `db:"spend_limit_micros" json:"spend_limit_micros"` MemberCount int64 `db:"member_count" json:"member_count"` } func (q *sqlQuerier) ListChatUsageLimitGroupOverrides(ctx context.Context) ([]ListChatUsageLimitGroupOverridesRow, error) { rows, err := q.db.QueryContext(ctx, listChatUsageLimitGroupOverrides) if err != nil { return nil, err } defer rows.Close() var items []ListChatUsageLimitGroupOverridesRow for rows.Next() { var i ListChatUsageLimitGroupOverridesRow if err := rows.Scan( &i.GroupID, &i.GroupName, &i.GroupDisplayName, &i.GroupAvatarUrl, &i.SpendLimitMicros, &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 listChatUsageLimitOverrides = `-- name: ListChatUsageLimitOverrides :many SELECT u.id AS user_id, u.username, u.name, u.avatar_url, u.chat_spend_limit_micros AS spend_limit_micros FROM users u WHERE u.chat_spend_limit_micros IS NOT NULL ORDER BY u.username ASC ` type ListChatUsageLimitOverridesRow struct { UserID uuid.UUID `db:"user_id" json:"user_id"` Username string `db:"username" json:"username"` Name string `db:"name" json:"name"` AvatarURL string `db:"avatar_url" json:"avatar_url"` SpendLimitMicros sql.NullInt64 `db:"spend_limit_micros" json:"spend_limit_micros"` } func (q *sqlQuerier) ListChatUsageLimitOverrides(ctx context.Context) ([]ListChatUsageLimitOverridesRow, error) { rows, err := q.db.QueryContext(ctx, listChatUsageLimitOverrides) if err != nil { return nil, err } defer rows.Close() var items []ListChatUsageLimitOverridesRow for rows.Next() { var i ListChatUsageLimitOverridesRow if err := rows.Scan( &i.UserID, &i.Username, &i.Name, &i.AvatarURL, &i.SpendLimitMicros, ); 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, api_key_id ` 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.APIKeyID, ) 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 resolveUserChatSpendLimit = `-- name: ResolveUserChatSpendLimit :one SELECT CASE WHEN NOT cfg.enabled THEN -1 WHEN u.chat_spend_limit_micros IS NOT NULL THEN u.chat_spend_limit_micros WHEN gl.limit_micros IS NOT NULL THEN gl.limit_micros ELSE cfg.default_limit_micros END::bigint AS effective_limit_micros, CASE WHEN NOT cfg.enabled THEN 'disabled' WHEN u.chat_spend_limit_micros IS NOT NULL THEN 'user' WHEN gl.limit_micros IS NOT NULL THEN 'group' ELSE 'default' END AS limit_source FROM chat_usage_limit_config cfg CROSS JOIN users u LEFT JOIN LATERAL ( SELECT MIN(g.chat_spend_limit_micros) AS limit_micros FROM groups g JOIN group_members_expanded gme ON gme.group_id = g.id WHERE gme.user_id = $1::uuid AND ($2::uuid IS NULL OR g.organization_id = $2::uuid) AND g.chat_spend_limit_micros IS NOT NULL ) gl ON TRUE WHERE u.id = $1::uuid LIMIT 1 ` type ResolveUserChatSpendLimitParams struct { UserID uuid.UUID `db:"user_id" json:"user_id"` OrganizationID uuid.NullUUID `db:"organization_id" json:"organization_id"` } type ResolveUserChatSpendLimitRow struct { EffectiveLimitMicros int64 `db:"effective_limit_micros" json:"effective_limit_micros"` LimitSource string `db:"limit_source" json:"limit_source"` } // Resolves the effective spend limit for a user using the hierarchy: // 1. Individual user override (highest priority, applies globally across // all organizations since it lives on the users table) // 2. Minimum group limit across the user's groups // 3. Global default from config // // Returns -1 if limits are not enabled. // When organization_id is NULL, groups across all organizations are // considered (global behavior). Otherwise only groups within the // specified organization are considered. // limit_source indicates which tier won: 'user', 'group', 'default', // or 'disabled'. func (q *sqlQuerier) ResolveUserChatSpendLimit(ctx context.Context, arg ResolveUserChatSpendLimitParams) (ResolveUserChatSpendLimitRow, error) { row := q.db.QueryRowContext(ctx, resolveUserChatSpendLimit, arg.UserID, arg.OrganizationID) var i ResolveUserChatSpendLimitRow err := row.Scan(&i.EffectiveLimitMicros, &i.LimitSource) return i, 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, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl ), 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.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.last_injected_context, updated_chats.dynamic_tools, updated_chats.organization_id, updated_chats.plan_mode, updated_chats.client_type, updated_chats.last_turn_summary, 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 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, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name FROM chats_expanded ORDER BY (chats_expanded.id = $1::uuid) DESC, chats_expanded.created_at ASC, chats_expanded.id ASC ` // Unarchives a chat (and its children). Stale file references are // handled automatically by FK cascades on chat_file_links: when // dbpurge deletes a chat_files row, the corresponding // chat_file_links rows are cascade-deleted by PostgreSQL. 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.Archived, &i.LastError, &i.Mode, pq.Array(&i.MCPServerIDs), &i.Labels, &i.BuildID, &i.AgentID, &i.PinOrder, &i.LastReadMessageID, &i.LastInjectedContext, &i.DynamicTools, &i.OrganizationID, &i.PlanMode, &i.ClientType, &i.LastTurnSummary, &i.UserACL, &i.GroupACL, &i.OwnerUsername, &i.OwnerName, ); 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, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl ), 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.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.last_injected_context, updated_chat.dynamic_tools, updated_chat.organization_id, updated_chat.plan_mode, updated_chat.client_type, updated_chat.last_turn_summary, 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 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, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name 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.Archived, &i.LastError, &i.Mode, pq.Array(&i.MCPServerIDs), &i.Labels, &i.BuildID, &i.AgentID, &i.PinOrder, &i.LastReadMessageID, &i.LastInjectedContext, &i.DynamicTools, &i.OrganizationID, &i.PlanMode, &i.ClientType, &i.LastTurnSummary, &i.UserACL, &i.GroupACL, &i.OwnerUsername, &i.OwnerName, ) 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, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl ), 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.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.last_injected_context, updated_chat.dynamic_tools, updated_chat.organization_id, updated_chat.plan_mode, updated_chat.client_type, updated_chat.last_turn_summary, 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 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, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name 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.Archived, &i.LastError, &i.Mode, pq.Array(&i.MCPServerIDs), &i.Labels, &i.BuildID, &i.AgentID, &i.PinOrder, &i.LastReadMessageID, &i.LastInjectedContext, &i.DynamicTools, &i.OrganizationID, &i.PlanMode, &i.ClientType, &i.LastTurnSummary, &i.UserACL, &i.GroupACL, &i.OwnerUsername, &i.OwnerName, ) 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, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl ), 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.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.last_injected_context, updated_chat.dynamic_tools, updated_chat.organization_id, updated_chat.plan_mode, updated_chat.client_type, updated_chat.last_turn_summary, 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 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, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name 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.Archived, &i.LastError, &i.Mode, pq.Array(&i.MCPServerIDs), &i.Labels, &i.BuildID, &i.AgentID, &i.PinOrder, &i.LastReadMessageID, &i.LastInjectedContext, &i.DynamicTools, &i.OrganizationID, &i.PlanMode, &i.ClientType, &i.LastTurnSummary, &i.UserACL, &i.GroupACL, &i.OwnerUsername, &i.OwnerName, ) return i, err } const updateChatLastInjectedContext = `-- name: UpdateChatLastInjectedContext :one WITH updated_chat AS ( UPDATE chats SET last_injected_context = $1::jsonb 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, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl ), 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.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.last_injected_context, updated_chat.dynamic_tools, updated_chat.organization_id, updated_chat.plan_mode, updated_chat.client_type, updated_chat.last_turn_summary, 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 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, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name FROM chats_expanded ` type UpdateChatLastInjectedContextParams struct { LastInjectedContext pqtype.NullRawMessage `db:"last_injected_context" json:"last_injected_context"` ID uuid.UUID `db:"id" json:"id"` } // Updates the cached injected context parts (AGENTS.md + // skills) on the chat row. Called only when context changes // (first workspace attach or agent change). updated_at is // intentionally not touched to avoid reordering the chat list. func (q *sqlQuerier) UpdateChatLastInjectedContext(ctx context.Context, arg UpdateChatLastInjectedContextParams) (Chat, error) { row := q.db.QueryRowContext(ctx, updateChatLastInjectedContext, arg.LastInjectedContext, 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.Archived, &i.LastError, &i.Mode, pq.Array(&i.MCPServerIDs), &i.Labels, &i.BuildID, &i.AgentID, &i.PinOrder, &i.LastReadMessageID, &i.LastInjectedContext, &i.DynamicTools, &i.OrganizationID, &i.PlanMode, &i.ClientType, &i.LastTurnSummary, &i.UserACL, &i.GroupACL, &i.OwnerUsername, &i.OwnerName, ) 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, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl ), 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.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.last_injected_context, updated_chat.dynamic_tools, updated_chat.organization_id, updated_chat.plan_mode, updated_chat.client_type, updated_chat.last_turn_summary, 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 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, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name 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.Archived, &i.LastError, &i.Mode, pq.Array(&i.MCPServerIDs), &i.Labels, &i.BuildID, &i.AgentID, &i.PinOrder, &i.LastReadMessageID, &i.LastInjectedContext, &i.DynamicTools, &i.OrganizationID, &i.PlanMode, &i.ClientType, &i.LastTurnSummary, &i.UserACL, &i.GroupACL, &i.OwnerUsername, &i.OwnerName, ) 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 updated_at = $3::timestamptz ` type UpdateChatLastTurnSummaryParams struct { LastTurnSummary sql.NullString `db:"last_turn_summary" json:"last_turn_summary"` ID uuid.UUID `db:"id" json:"id"` ExpectedUpdatedAt time.Time `db:"expected_updated_at" json:"expected_updated_at"` } // 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 relies on // every new-turn query, such as UpdateChatStatus and AcquireChats, bumping // updated_at. Future chat-field updates that do not bump updated_at can let // stale summaries persist. If this query ever bumps updated_at, later // goroutine summary writes will be rejected as stale. // 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.ExpectedUpdatedAt) 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, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl ), 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.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.last_injected_context, updated_chat.dynamic_tools, updated_chat.organization_id, updated_chat.plan_mode, updated_chat.client_type, updated_chat.last_turn_summary, 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 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, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name 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.Archived, &i.LastError, &i.Mode, pq.Array(&i.MCPServerIDs), &i.Labels, &i.BuildID, &i.AgentID, &i.PinOrder, &i.LastReadMessageID, &i.LastInjectedContext, &i.DynamicTools, &i.OrganizationID, &i.PlanMode, &i.ClientType, &i.LastTurnSummary, &i.UserACL, &i.GroupACL, &i.OwnerUsername, &i.OwnerName, ) return i, err } const updateChatMessageByID = `-- name: UpdateChatMessageByID :one UPDATE chat_messages SET model_config_id = COALESCE($1::uuid, model_config_id), content = $2::jsonb WHERE id = $3::bigint 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, api_key_id ` type UpdateChatMessageByIDParams struct { ModelConfigID uuid.NullUUID `db:"model_config_id" json:"model_config_id"` Content pqtype.NullRawMessage `db:"content" json:"content"` ID int64 `db:"id" json:"id"` } func (q *sqlQuerier) UpdateChatMessageByID(ctx context.Context, arg UpdateChatMessageByIDParams) (ChatMessage, error) { row := q.db.QueryRowContext(ctx, updateChatMessageByID, arg.ModelConfigID, arg.Content, arg.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.APIKeyID, ) 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, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl ), 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.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.last_injected_context, updated_chat.dynamic_tools, updated_chat.organization_id, updated_chat.plan_mode, updated_chat.client_type, updated_chat.last_turn_summary, 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 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, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name 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.Archived, &i.LastError, &i.Mode, pq.Array(&i.MCPServerIDs), &i.Labels, &i.BuildID, &i.AgentID, &i.PinOrder, &i.LastReadMessageID, &i.LastInjectedContext, &i.DynamicTools, &i.OrganizationID, &i.PlanMode, &i.ClientType, &i.LastTurnSummary, &i.UserACL, &i.GroupACL, &i.OwnerUsername, &i.OwnerName, ) 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, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl ), 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.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.last_injected_context, updated_chat.dynamic_tools, updated_chat.organization_id, updated_chat.plan_mode, updated_chat.client_type, updated_chat.last_turn_summary, 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 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, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name 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.Archived, &i.LastError, &i.Mode, pq.Array(&i.MCPServerIDs), &i.Labels, &i.BuildID, &i.AgentID, &i.PinOrder, &i.LastReadMessageID, &i.LastInjectedContext, &i.DynamicTools, &i.OrganizationID, &i.PlanMode, &i.ClientType, &i.LastTurnSummary, &i.UserACL, &i.GroupACL, &i.OwnerUsername, &i.OwnerName, ) return i, err } const updateChatStatusPreserveUpdatedAt = `-- name: UpdateChatStatusPreserveUpdatedAt :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 = $6::timestamptz WHERE id = $7::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, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl ), 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.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.last_injected_context, updated_chat.dynamic_tools, updated_chat.organization_id, updated_chat.plan_mode, updated_chat.client_type, updated_chat.last_turn_summary, 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 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, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name FROM chats_expanded ` type UpdateChatStatusPreserveUpdatedAtParams 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"` UpdatedAt time.Time `db:"updated_at" json:"updated_at"` ID uuid.UUID `db:"id" json:"id"` } func (q *sqlQuerier) UpdateChatStatusPreserveUpdatedAt(ctx context.Context, arg UpdateChatStatusPreserveUpdatedAtParams) (Chat, error) { row := q.db.QueryRowContext(ctx, updateChatStatusPreserveUpdatedAt, arg.Status, arg.WorkerID, arg.StartedAt, arg.HeartbeatAt, arg.LastError, arg.UpdatedAt, 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.Archived, &i.LastError, &i.Mode, pq.Array(&i.MCPServerIDs), &i.Labels, &i.BuildID, &i.AgentID, &i.PinOrder, &i.LastReadMessageID, &i.LastInjectedContext, &i.DynamicTools, &i.OrganizationID, &i.PlanMode, &i.ClientType, &i.LastTurnSummary, &i.UserACL, &i.GroupACL, &i.OwnerUsername, &i.OwnerName, ) return i, err } 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, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl ), 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.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.last_injected_context, updated_chat.dynamic_tools, updated_chat.organization_id, updated_chat.plan_mode, updated_chat.client_type, updated_chat.last_turn_summary, 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 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, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name 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.Archived, &i.LastError, &i.Mode, pq.Array(&i.MCPServerIDs), &i.Labels, &i.BuildID, &i.AgentID, &i.PinOrder, &i.LastReadMessageID, &i.LastInjectedContext, &i.DynamicTools, &i.OrganizationID, &i.PlanMode, &i.ClientType, &i.LastTurnSummary, &i.UserACL, &i.GroupACL, &i.OwnerUsername, &i.OwnerName, ) return i, err } const updateChatWorkspaceBinding = `-- name: UpdateChatWorkspaceBinding :one WITH updated_chat AS ( UPDATE chats SET workspace_id = $1::uuid, build_id = $2::uuid, agent_id = $3::uuid, updated_at = NOW() WHERE id = $4::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, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl ), 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.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.last_injected_context, updated_chat.dynamic_tools, updated_chat.organization_id, updated_chat.plan_mode, updated_chat.client_type, updated_chat.last_turn_summary, 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 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, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name FROM chats_expanded ` type UpdateChatWorkspaceBindingParams struct { 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"` ID uuid.UUID `db:"id" json:"id"` } func (q *sqlQuerier) UpdateChatWorkspaceBinding(ctx context.Context, arg UpdateChatWorkspaceBindingParams) (Chat, error) { row := q.db.QueryRowContext(ctx, updateChatWorkspaceBinding, arg.WorkspaceID, 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.Archived, &i.LastError, &i.Mode, pq.Array(&i.MCPServerIDs), &i.Labels, &i.BuildID, &i.AgentID, &i.PinOrder, &i.LastReadMessageID, &i.LastInjectedContext, &i.DynamicTools, &i.OrganizationID, &i.PlanMode, &i.ClientType, &i.LastTurnSummary, &i.UserACL, &i.GroupACL, &i.OwnerUsername, &i.OwnerName, ) 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 upsertChatUsageLimitConfig = `-- name: UpsertChatUsageLimitConfig :one INSERT INTO chat_usage_limit_config (singleton, enabled, default_limit_micros, period, updated_at) VALUES (TRUE, $1::boolean, $2::bigint, $3::text, NOW()) ON CONFLICT (singleton) DO UPDATE SET enabled = EXCLUDED.enabled, default_limit_micros = EXCLUDED.default_limit_micros, period = EXCLUDED.period, updated_at = NOW() RETURNING id, singleton, enabled, default_limit_micros, period, created_at, updated_at ` type UpsertChatUsageLimitConfigParams struct { Enabled bool `db:"enabled" json:"enabled"` DefaultLimitMicros int64 `db:"default_limit_micros" json:"default_limit_micros"` Period string `db:"period" json:"period"` } func (q *sqlQuerier) UpsertChatUsageLimitConfig(ctx context.Context, arg UpsertChatUsageLimitConfigParams) (ChatUsageLimitConfig, error) { row := q.db.QueryRowContext(ctx, upsertChatUsageLimitConfig, arg.Enabled, arg.DefaultLimitMicros, arg.Period) var i ChatUsageLimitConfig err := row.Scan( &i.ID, &i.Singleton, &i.Enabled, &i.DefaultLimitMicros, &i.Period, &i.CreatedAt, &i.UpdatedAt, ) return i, err } const upsertChatUsageLimitGroupOverride = `-- name: UpsertChatUsageLimitGroupOverride :one UPDATE groups SET chat_spend_limit_micros = $1::bigint WHERE id = $2::uuid RETURNING id AS group_id, name, display_name, avatar_url, chat_spend_limit_micros AS spend_limit_micros ` type UpsertChatUsageLimitGroupOverrideParams struct { SpendLimitMicros int64 `db:"spend_limit_micros" json:"spend_limit_micros"` GroupID uuid.UUID `db:"group_id" json:"group_id"` } type UpsertChatUsageLimitGroupOverrideRow struct { GroupID uuid.UUID `db:"group_id" json:"group_id"` Name string `db:"name" json:"name"` DisplayName string `db:"display_name" json:"display_name"` AvatarURL string `db:"avatar_url" json:"avatar_url"` SpendLimitMicros sql.NullInt64 `db:"spend_limit_micros" json:"spend_limit_micros"` } func (q *sqlQuerier) UpsertChatUsageLimitGroupOverride(ctx context.Context, arg UpsertChatUsageLimitGroupOverrideParams) (UpsertChatUsageLimitGroupOverrideRow, error) { row := q.db.QueryRowContext(ctx, upsertChatUsageLimitGroupOverride, arg.SpendLimitMicros, arg.GroupID) var i UpsertChatUsageLimitGroupOverrideRow err := row.Scan( &i.GroupID, &i.Name, &i.DisplayName, &i.AvatarURL, &i.SpendLimitMicros, ) return i, err } const upsertChatUsageLimitUserOverride = `-- name: UpsertChatUsageLimitUserOverride :one UPDATE users SET chat_spend_limit_micros = $1::bigint WHERE id = $2::uuid RETURNING id AS user_id, username, name, avatar_url, chat_spend_limit_micros AS spend_limit_micros ` type UpsertChatUsageLimitUserOverrideParams struct { SpendLimitMicros int64 `db:"spend_limit_micros" json:"spend_limit_micros"` UserID uuid.UUID `db:"user_id" json:"user_id"` } type UpsertChatUsageLimitUserOverrideRow struct { UserID uuid.UUID `db:"user_id" json:"user_id"` Username string `db:"username" json:"username"` Name string `db:"name" json:"name"` AvatarURL string `db:"avatar_url" json:"avatar_url"` SpendLimitMicros sql.NullInt64 `db:"spend_limit_micros" json:"spend_limit_micros"` } func (q *sqlQuerier) UpsertChatUsageLimitUserOverride(ctx context.Context, arg UpsertChatUsageLimitUserOverrideParams) (UpsertChatUsageLimitUserOverrideRow, error) { row := q.db.QueryRowContext(ctx, upsertChatUsageLimitUserOverride, arg.SpendLimitMicros, arg.UserID) var i UpsertChatUsageLimitUserOverrideRow err := row.Scan( &i.UserID, &i.Username, &i.Name, &i.AvatarURL, &i.SpendLimitMicros, ) return i, 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 web events, since we don't know their close time. "type" NOT IN ('workspace_app', 'port_forwarding') 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 web events, since we don't know their close time. "type" NOT IN ('workspace_app', 'port_forwarding') 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 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, ) return i, err } const insertGitSSHKey = `-- name: InsertGitSSHKey :one INSERT INTO gitsshkeys ( user_id, created_at, updated_at, private_key, public_key ) VALUES ($1, $2, $3, $4, $5) RETURNING user_id, created_at, updated_at, private_key, public_key ` 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"` 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.PublicKey, ) var i GitSSHKey err := row.Scan( &i.UserID, &i.CreatedAt, &i.UpdatedAt, &i.PrivateKey, &i.PublicKey, ) return i, err } const updateGitSSHKey = `-- name: UpdateGitSSHKey :one UPDATE gitsshkeys SET updated_at = $2, private_key = $3, public_key = $4 WHERE user_id = $1 RETURNING user_id, created_at, updated_at, private_key, public_key ` 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"` 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.PublicKey, ) var i GitSSHKey err := row.Scan( &i.UserID, &i.CreatedAt, &i.UpdatedAt, &i.PrivateKey, &i.PublicKey, ) 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 or username AND CASE WHEN $3 :: text != '' THEN ( user_email ILIKE concat('%', $3, '%') OR user_username 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 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($5 :: user_status[]) > 0 THEN user_status = ANY($5 :: 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($6 :: text[]) > 0 AND 'member' != ANY($6 :: text[]) THEN user_rbac_roles && $6 :: text[] ELSE true END -- Filter by last_seen AND CASE WHEN $7 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN user_last_seen_at <= $7 ELSE true END AND CASE WHEN $8 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN user_last_seen_at >= $8 ELSE true END -- Filter by created_at AND CASE WHEN $9 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN user_created_at <= $9 ELSE true END AND CASE WHEN $10 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN user_created_at >= $10 ELSE true END -- Filter by system type AND CASE WHEN $11::bool THEN TRUE ELSE user_is_system = false END -- Filter by github.com user ID AND CASE WHEN $12 :: bigint != 0 THEN user_github_com_user_id = $12 ELSE true END -- Filter by login_type AND CASE WHEN cardinality($13 :: login_type[]) > 0 THEN user_login_type = ANY($13 :: login_type[]) ELSE true END -- Filter by service account. AND CASE WHEN $14 :: boolean IS NOT NULL THEN user_is_service_account = $14 :: 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 $15 LIMIT -- A null limit means "no limit", so 0 means return all NULLIF($16 :: 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"` 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, 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 name = $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 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 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, ); 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 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, ); 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 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, ) 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 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, ) 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 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, ); 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 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, ); 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 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, ) 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 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, ); 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_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::boolean, $23::boolean, $24::boolean, $25::boolean, $26::uuid, $27::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 ` 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"` 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.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, ) 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_scopes = $13::text, api_key_header = $14::text, api_key_value = $15::text, api_key_value_key_id = $16::text, custom_headers = $17::text, custom_headers_key_id = $18::text, tool_allow_list = $19::text[], tool_deny_list = $20::text[], availability = $21::text, enabled = $22::boolean, model_intent = $23::boolean, allow_in_plan_mode = $24::boolean, forward_coder_headers = $25::boolean, updated_by = $26::uuid, updated_at = NOW() WHERE id = $27::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 ` 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"` 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.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, ) 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, 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 ` 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, ) 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 USING oauth2_provider_app_secrets WHERE oauth2_provider_app_secrets.id = oauth2_provider_app_tokens.app_secret_id AND oauth2_provider_app_secrets.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"` } 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 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, ) 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 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, ) 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 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, ) 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 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, ) 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_app_secrets ON oauth2_provider_app_secrets.id = oauth2_provider_app_tokens.app_secret_id INNER JOIN oauth2_provider_apps ON oauth2_provider_apps.id = oauth2_provider_app_secrets.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"` } 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 sql.NullString `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 ) VALUES( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12 ) 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 ` 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"` } 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, ) 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, ) 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_secret_id, api_key_id, user_id, audience ) VALUES( $1, $2, $3, $4, $5, $6, $7, $8, $9 ) RETURNING id, created_at, expires_at, hash_prefix, refresh_hash, app_secret_id, api_key_id, audience, user_id ` 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"` AppSecretID uuid.UUID `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"` } 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.AppSecretID, arg.APIKeyID, arg.UserID, arg.Audience, ) var i OAuth2ProviderAppToken err := row.Scan( &i.ID, &i.CreatedAt, &i.ExpiresAt, &i.HashPrefix, &i.RefreshHash, &i.AppSecretID, &i.APIKeyID, &i.Audience, &i.UserID, ) 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 sql.NullString `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 sql.NullString `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 or username AND CASE WHEN $3 :: text != '' THEN ( users.email ILIKE concat('%', $3, '%') OR users.username 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 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($5 :: user_status[]) > 0 THEN users.status = ANY($5 :: 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($6 :: text[]) > 0 AND 'member' != ANY($6 :: text[]) THEN users.rbac_roles && $6 :: text[] ELSE true END -- Filter by last_seen AND CASE WHEN $7 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN users.last_seen_at <= $7 ELSE true END AND CASE WHEN $8 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN users.last_seen_at >= $8 ELSE true END -- Filter by created_at (user creation date, not date added to org) AND CASE WHEN $9 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN users.created_at <= $9 ELSE true END AND CASE WHEN $10 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN users.created_at >= $10 ELSE true END -- Filter by system type AND CASE WHEN $11::bool THEN TRUE ELSE users.is_system = false END -- Filter by github.com user ID AND CASE WHEN $12 :: bigint != 0 THEN users.github_com_user_id = $12 ELSE true END -- Filter by login_type AND CASE WHEN cardinality($13 :: login_type[]) > 0 THEN users.login_type = ANY($13 :: login_type[]) ELSE true END -- Filter by service account. AND CASE WHEN $14 :: boolean IS NOT NULL THEN users.is_service_account = $14 :: 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 $15 LIMIT -- A null limit means "no limit", so 0 means return all NULLIF($16 :: 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"` 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, 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 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, ) 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 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, ) 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 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, ) 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 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, ); 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 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, ); 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) 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) RETURNING id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, shareable_workspace_owners ` 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"` } 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, ) 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, ) return i, err } const updateOrganization = `-- name: UpdateOrganization :one UPDATE organizations SET updated_at = $1, name = $2, display_name = $3, description = $4, icon = $5 WHERE id = $6 RETURNING id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, shareable_workspace_owners ` 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"` 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, 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, ) 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 ` 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, ) 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 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" 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, ) 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" 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, ); 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" ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id, created_at, started_at, stopped_at, updated_at, hostname, region_id, relay_address, database_latency, version, error, "primary" ` 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"` } 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, ) 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, ) 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 WHERE id = $1 RETURNING id, created_at, started_at, stopped_at, updated_at, hostname, region_id, relay_address, database_latency, version, error, "primary" ` 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"` } 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, ) 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, ) 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 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 getChatTemplateAllowlist = `-- name: GetChatTemplateAllowlist :one SELECT COALESCE((SELECT value FROM site_configs WHERE key = 'agents_template_allowlist'), '') :: text AS template_allowlist ` // GetChatTemplateAllowlist returns the JSON-encoded template allowlist. // Returns an empty string when no allowlist has been configured (all templates allowed). func (q *sqlQuerier) GetChatTemplateAllowlist(ctx context.Context) (string, error) { row := q.db.QueryRowContext(ctx, getChatTemplateAllowlist) var template_allowlist string err := row.Scan(&template_allowlist) return template_allowlist, 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(¬ifications_settings) return notifications_settings, 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 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 upsertChatTemplateAllowlist = `-- name: UpsertChatTemplateAllowlist :exec INSERT INTO site_configs (key, value) VALUES ('agents_template_allowlist', $1) ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'agents_template_allowlist' ` func (q *sqlQuerier) UpsertChatTemplateAllowlist(ctx context.Context, templateAllowlist string) error { _, err := q.db.ExecContext(ctx, upsertChatTemplateAllowlist, templateAllowlist) 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 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, 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.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, 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.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, 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.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.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 author_id AND CASE WHEN $10 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN t.created_by = $10 ELSE true END -- Filter by author_username AND CASE WHEN $11 :: text != '' THEN t.created_by = (SELECT id FROM users WHERE lower(users.username) = lower($11) AND deleted = false) ELSE true END -- Filter by has_external_agent in latest version AND CASE WHEN $12 :: boolean IS NOT NULL THEN tv.has_external_agent = $12 :: 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"` 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.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.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 ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) ` 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"` } 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, ) 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 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"` } 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, ) 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 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"` } 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, ) 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 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 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 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 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 createUserSecret = `-- name: CreateUserSecret :one INSERT INTO user_secrets ( id, user_id, name, description, value, value_key_id, env_name, file_path ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8 ) RETURNING id, user_id, name, description, value, env_name, file_path, created_at, updated_at, value_key_id ` 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"` } 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, ) 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, ) 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 ` 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, ) 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 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, ) 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 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, ) 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, 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"` 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.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 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, ); 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, updated_at = CURRENT_TIMESTAMP WHERE user_id = $10 AND name = $11 RETURNING id, user_id, name, description, value, env_name, file_path, created_at, updated_at, value_key_id ` 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"` 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.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, ) 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 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, -- 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. unnest( array_append( roles, CASE WHEN users.is_service_account THEN 'organization-service-account' ELSE 'organization-member' END ) ) 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 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. 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 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 or username AND CASE WHEN $2 :: text != '' THEN ( email ILIKE concat('%', $2, '%') OR username 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 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($4 :: user_status[]) > 0 THEN status = ANY($4 :: 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($5 :: text[]) > 0 AND 'member' != ANY($5 :: text[]) THEN rbac_roles && $5 :: text[] ELSE true END -- Filter by last_seen AND CASE WHEN $6 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN last_seen_at <= $6 ELSE true END AND CASE WHEN $7 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN last_seen_at >= $7 ELSE true END -- Filter by created_at AND CASE WHEN $8 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN created_at <= $8 ELSE true END AND CASE WHEN $9 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN created_at >= $9 ELSE true END -- Filter by system type AND CASE WHEN $10::bool THEN TRUE ELSE is_system = false END -- Filter by github.com user ID AND CASE WHEN $11 :: bigint != 0 THEN github_com_user_id = $11 ELSE true END -- Filter by login_type AND CASE WHEN cardinality($12 :: login_type[]) > 0 THEN login_type = ANY($12 :: login_type[]) ELSE true END -- Filter by service account. AND CASE WHEN $13 :: boolean IS NOT NULL THEN is_service_account = $13 :: 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 $14 LIMIT -- A null limit means "no limit", so 0 means return all NULLIF($15 :: 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"` 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, 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 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 UPDATE workspace_agents SET deleted = TRUE WHERE id = $1 AND parent_id IS NOT NULL AND deleted = FALSE ` 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.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.InitiatorByAvatarUrl, &i.WorkspaceBuild.InitiatorByUsername, &i.WorkspaceBuild.InitiatorByName, &i.TaskID, ) return i, err } 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 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 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 ) ` 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. 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 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 ) ` // 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. 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 ` 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 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 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.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.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, 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.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, 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.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, 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.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, 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.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, 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.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, 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.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, 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.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 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 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($1 :: text[])) AS name, LOWER(unnest($2 :: 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 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 FROM templates WHERE templates.id = workspaces.template_id ) template ON true WHERE -- Optionally include deleted workspaces workspaces.deleted = $3 AND CASE WHEN $4 :: 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 $4 = 'starting' THEN latest_build.job_status = 'running'::provisioner_job_status AND latest_build.transition = 'start'::workspace_transition WHEN $4 = 'stopping' THEN latest_build.job_status = 'running'::provisioner_job_status AND latest_build.transition = 'stop'::workspace_transition WHEN $4 = 'deleting' THEN latest_build.job_status = 'running' AND latest_build.transition = 'delete'::workspace_transition -- 'succeeded' states WHEN $4 = 'deleted' THEN latest_build.job_status = 'succeeded'::provisioner_job_status AND latest_build.transition = 'delete'::workspace_transition WHEN $4 = 'stopped' THEN latest_build.job_status = 'succeeded'::provisioner_job_status AND latest_build.transition = 'stop'::workspace_transition WHEN $4 = '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 $4 = 'running' THEN latest_build.job_status = 'succeeded'::provisioner_job_status AND latest_build.transition = 'start'::workspace_transition WHEN $4 != '' THEN -- By default just match the job status exactly latest_build.job_status = $4::provisioner_job_status ELSE true END ELSE true END -- Filter by owner_id AND CASE WHEN $5 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN workspaces.owner_id = $5 ELSE true END -- Filter by organization_id AND CASE WHEN $6 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN workspaces.organization_id = $6 ELSE true END -- Filter by build parameter -- @has_param will match any build that includes the parameter. AND CASE WHEN array_length($7 :: 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($7) ) 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($1 :: 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 $8 :: text != '' THEN workspaces.owner_id = (SELECT id FROM users WHERE lower(users.username) = lower($8) 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 $9 :: text != '' THEN workspaces.template_id = ANY(SELECT id FROM templates WHERE lower(name) = lower($9) AND deleted = false) ELSE true END -- Filter by template_ids AND CASE WHEN array_length($10 :: uuid[], 1) > 0 THEN workspaces.template_id = ANY($10) ELSE true END -- Filter by workspace_ids AND CASE WHEN array_length($11 :: uuid[], 1) > 0 THEN workspaces.id = ANY($11) ELSE true END -- Filter by name, matching on substring AND CASE WHEN $12 :: text != '' THEN workspaces.name ILIKE '%' || $12 || '%' 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($13 :: 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' * $14 :: bigint THEN 'disconnected' WHEN workspace_agents.last_connected_at IS NOT NULL THEN 'connected' ELSE NULL END ) = ANY($13 :: text[]) ) > 0 ELSE true END -- Filter by dormant workspaces. AND CASE WHEN $15 :: boolean != 'false' THEN dormant_at IS NOT NULL ELSE true END -- Filter by last_used AND CASE WHEN $16 :: timestamp with time zone > '0001-01-01 00:00:00Z' THEN workspaces.last_used_at <= $16 ELSE true END 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 :: boolean IS NOT NULL THEN (latest_build.template_version_id = template.active_version_id) = $18 :: boolean ELSE true END -- Filter by has_ai_task, checks if this is a task workspace. AND CASE WHEN $19::boolean IS NOT NULL THEN $19::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 $20 :: boolean IS NOT NULL THEN latest_build.has_external_agent = $20 :: boolean ELSE true END -- Filter by shared status AND CASE WHEN $21 :: boolean IS NOT NULL THEN (workspaces.user_acl != '{}'::jsonb OR workspaces.group_acl != '{}'::jsonb) = $21 :: boolean ELSE true END -- Filter by shared_with_user_id AND CASE WHEN $22 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN workspaces.user_acl ? ($22 :: uuid) :: text ELSE true END -- Filter by shared_with_group_id AND CASE WHEN $23 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN workspaces.group_acl ? ($23 :: 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 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 = $24) 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 $26 :: integer > 0 THEN $26 END OFFSET $25 ), 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 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 WHERE $27 :: 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, tc.count FROM filtered_workspaces_order_with_summary fwos CROSS JOIN total_count tc ` type GetWorkspacesParams struct { 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"` 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.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.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 getWorkspacesEligibleForTransition = `-- name: GetWorkspacesEligibleForTransition :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 stop if the following are true: -- * The template has a failure ttl set. -- * The workspace build was a start transition. -- * 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 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)) ) ) 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 GetWorkspacesEligibleForTransitionRow 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"` } func (q *sqlQuerier) GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]GetWorkspacesEligibleForTransitionRow, error) { rows, err := q.db.QueryContext(ctx, getWorkspacesEligibleForTransition, now) if err != nil { return nil, err } defer rows.Close() var items []GetWorkspacesEligibleForTransitionRow for rows.Next() { var i GetWorkspacesEligibleForTransitionRow 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 }