mirror of
https://github.com/coder/coder.git
synced 2026-06-06 14:38:23 +00:00
37372 lines
1.2 MiB
Plaintext
Generated
37372 lines
1.2 MiB
Plaintext
Generated
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.31.1
|
|
|
|
package database
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/lib/pq"
|
|
"github.com/sqlc-dev/pqtype"
|
|
)
|
|
|
|
const activityBumpWorkspace = `-- name: ActivityBumpWorkspace :exec
|
|
WITH latest AS (
|
|
SELECT
|
|
workspace_builds.id::uuid AS build_id,
|
|
workspace_builds.deadline::timestamp with time zone AS build_deadline,
|
|
workspace_builds.max_deadline::timestamp with time zone AS build_max_deadline,
|
|
workspace_builds.transition AS build_transition,
|
|
provisioner_jobs.completed_at::timestamp with time zone AS job_completed_at,
|
|
templates.activity_bump AS activity_bump,
|
|
(
|
|
CASE
|
|
-- If the extension would push us over the next_autostart
|
|
-- interval, then extend the deadline by the full TTL (NOT
|
|
-- activity bump) from the autostart time. This will essentially
|
|
-- be as if the workspace auto started at the given time and the
|
|
-- original TTL was applied.
|
|
--
|
|
-- Sadly we can't define 'activity_bump_interval' above since
|
|
-- it won't be available for this CASE statement, so we have to
|
|
-- copy the cast twice.
|
|
WHEN NOW() + (templates.activity_bump / 1000 / 1000 / 1000 || ' seconds')::interval > $1 :: timestamptz
|
|
-- If the autostart is behind now(), then the
|
|
-- autostart schedule is either the 0 time and not provided,
|
|
-- or it was the autostart in the past, which is no longer
|
|
-- relevant. If autostart is > 0 and in the past, then
|
|
-- that is a mistake by the caller.
|
|
AND $1 > NOW()
|
|
THEN
|
|
-- Extend to the autostart, then add the activity bump
|
|
(($1 :: timestamptz) - NOW()) + CASE
|
|
WHEN templates.allow_user_autostop
|
|
THEN (workspaces.ttl / 1000 / 1000 / 1000 || ' seconds')::interval
|
|
ELSE (templates.default_ttl / 1000 / 1000 / 1000 || ' seconds')::interval
|
|
END
|
|
|
|
-- Default to the activity bump duration.
|
|
ELSE
|
|
(templates.activity_bump / 1000 / 1000 / 1000 || ' seconds')::interval
|
|
END
|
|
) AS ttl_interval
|
|
FROM workspace_builds
|
|
JOIN provisioner_jobs
|
|
ON provisioner_jobs.id = workspace_builds.job_id
|
|
JOIN workspaces
|
|
ON workspaces.id = workspace_builds.workspace_id
|
|
JOIN templates
|
|
ON templates.id = workspaces.template_id
|
|
WHERE
|
|
workspace_builds.workspace_id = $2::uuid
|
|
-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
|
|
-- are managed by the reconciliation loop and not subject to activity bumping
|
|
AND workspaces.owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID
|
|
ORDER BY workspace_builds.build_number DESC
|
|
LIMIT 1
|
|
)
|
|
UPDATE
|
|
workspace_builds wb
|
|
SET
|
|
updated_at = NOW(),
|
|
deadline = CASE
|
|
WHEN l.build_max_deadline = '0001-01-01 00:00:00+00'
|
|
-- Never reduce the deadline from activity.
|
|
THEN GREATEST(wb.deadline, NOW() + l.ttl_interval)
|
|
ELSE LEAST(GREATEST(wb.deadline, NOW() + l.ttl_interval), l.build_max_deadline)
|
|
END
|
|
FROM latest l
|
|
WHERE wb.id = l.build_id
|
|
AND l.job_completed_at IS NOT NULL
|
|
AND l.activity_bump > 0
|
|
AND l.build_transition = 'start'
|
|
AND l.ttl_interval > '0 seconds'::interval
|
|
AND l.build_deadline != '0001-01-01 00:00:00+00'
|
|
AND l.build_deadline - (l.ttl_interval * 0.95) < NOW()
|
|
`
|
|
|
|
type ActivityBumpWorkspaceParams struct {
|
|
NextAutostart time.Time `db:"next_autostart" json:"next_autostart"`
|
|
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
|
|
}
|
|
|
|
// Bumps the workspace deadline by the template's configured "activity_bump"
|
|
// duration (default 1h). If the workspace bump will cross an autostart
|
|
// threshold, then the bump is autostart + TTL. This is the deadline behavior if
|
|
// the workspace was to autostart from a stopped state.
|
|
//
|
|
// Max deadline is respected, and the deadline will never be bumped past it.
|
|
// The deadline will never decrease.
|
|
// We only bump if the template has an activity bump duration set.
|
|
// We only bump if the raw interval is positive and non-zero.
|
|
// We only bump if workspace shutdown is manual.
|
|
// We only bump when 5% of the deadline has elapsed.
|
|
func (q *sqlQuerier) ActivityBumpWorkspace(ctx context.Context, arg ActivityBumpWorkspaceParams) error {
|
|
_, err := q.db.ExecContext(ctx, activityBumpWorkspace, arg.NextAutostart, arg.WorkspaceID)
|
|
return err
|
|
}
|
|
|
|
const deleteAIGatewayKey = `-- name: DeleteAIGatewayKey :one
|
|
DELETE FROM ai_gateway_keys WHERE id = $1
|
|
RETURNING id, name, secret_prefix, created_at, last_used_at
|
|
`
|
|
|
|
type DeleteAIGatewayKeyRow struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
Name string `db:"name" json:"name"`
|
|
SecretPrefix string `db:"secret_prefix" json:"secret_prefix"`
|
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
|
LastUsedAt sql.NullTime `db:"last_used_at" json:"last_used_at"`
|
|
}
|
|
|
|
func (q *sqlQuerier) DeleteAIGatewayKey(ctx context.Context, id uuid.UUID) (DeleteAIGatewayKeyRow, error) {
|
|
row := q.db.QueryRowContext(ctx, deleteAIGatewayKey, id)
|
|
var i DeleteAIGatewayKeyRow
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.SecretPrefix,
|
|
&i.CreatedAt,
|
|
&i.LastUsedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const insertAIGatewayKey = `-- name: InsertAIGatewayKey :one
|
|
INSERT INTO ai_gateway_keys (id, name, secret_prefix, hashed_secret, created_at)
|
|
VALUES ($1, $4, $2, $3, NOW())
|
|
RETURNING id, name, secret_prefix, created_at
|
|
`
|
|
|
|
type InsertAIGatewayKeyParams struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
SecretPrefix string `db:"secret_prefix" json:"secret_prefix"`
|
|
HashedSecret []byte `db:"hashed_secret" json:"hashed_secret"`
|
|
Name string `db:"name" json:"name"`
|
|
}
|
|
|
|
type InsertAIGatewayKeyRow struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
Name string `db:"name" json:"name"`
|
|
SecretPrefix string `db:"secret_prefix" json:"secret_prefix"`
|
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
|
}
|
|
|
|
func (q *sqlQuerier) InsertAIGatewayKey(ctx context.Context, arg InsertAIGatewayKeyParams) (InsertAIGatewayKeyRow, error) {
|
|
row := q.db.QueryRowContext(ctx, insertAIGatewayKey,
|
|
arg.ID,
|
|
arg.SecretPrefix,
|
|
arg.HashedSecret,
|
|
arg.Name,
|
|
)
|
|
var i InsertAIGatewayKeyRow
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.SecretPrefix,
|
|
&i.CreatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const listAIGatewayKeys = `-- name: ListAIGatewayKeys :many
|
|
SELECT id, name, secret_prefix, created_at, last_used_at
|
|
FROM ai_gateway_keys
|
|
ORDER BY created_at ASC
|
|
`
|
|
|
|
type ListAIGatewayKeysRow struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
Name string `db:"name" json:"name"`
|
|
SecretPrefix string `db:"secret_prefix" json:"secret_prefix"`
|
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
|
LastUsedAt sql.NullTime `db:"last_used_at" json:"last_used_at"`
|
|
}
|
|
|
|
func (q *sqlQuerier) ListAIGatewayKeys(ctx context.Context) ([]ListAIGatewayKeysRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, listAIGatewayKeys)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListAIGatewayKeysRow
|
|
for rows.Next() {
|
|
var i ListAIGatewayKeysRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.SecretPrefix,
|
|
&i.CreatedAt,
|
|
&i.LastUsedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
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,
|
|
-- BYOK records its hint at the start of the interception.
|
|
-- Centralized uses key failover, so its hint is only known
|
|
-- at end-of-interception.
|
|
credential_hint = CASE
|
|
WHEN credential_kind = 'centralized' THEN $2::text
|
|
ELSE credential_hint
|
|
END
|
|
WHERE
|
|
id = $3::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"`
|
|
CredentialHint string `db:"credential_hint" json:"credential_hint"`
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateAIBridgeInterceptionEnded(ctx context.Context, arg UpdateAIBridgeInterceptionEndedParams) (AIBridgeInterception, error) {
|
|
row := q.db.QueryRowContext(ctx, updateAIBridgeInterceptionEnded, arg.EndedAt, arg.CredentialHint, arg.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 deleteUserAIBudgetOverride = `-- name: DeleteUserAIBudgetOverride :one
|
|
DELETE FROM user_ai_budget_overrides WHERE user_id = $1 RETURNING user_id, group_id, spend_limit_micros, created_at, updated_at
|
|
`
|
|
|
|
func (q *sqlQuerier) DeleteUserAIBudgetOverride(ctx context.Context, userID uuid.UUID) (UserAiBudgetOverride, error) {
|
|
row := q.db.QueryRowContext(ctx, deleteUserAIBudgetOverride, userID)
|
|
var i UserAiBudgetOverride
|
|
err := row.Scan(
|
|
&i.UserID,
|
|
&i.GroupID,
|
|
&i.SpendLimitMicros,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const 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 getUserAIBudgetOverride = `-- name: GetUserAIBudgetOverride :one
|
|
SELECT user_id, group_id, spend_limit_micros, created_at, updated_at
|
|
FROM user_ai_budget_overrides
|
|
WHERE user_id = $1
|
|
`
|
|
|
|
func (q *sqlQuerier) GetUserAIBudgetOverride(ctx context.Context, userID uuid.UUID) (UserAiBudgetOverride, error) {
|
|
row := q.db.QueryRowContext(ctx, getUserAIBudgetOverride, userID)
|
|
var i UserAiBudgetOverride
|
|
err := row.Scan(
|
|
&i.UserID,
|
|
&i.GroupID,
|
|
&i.SpendLimitMicros,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const 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 upsertUserAIBudgetOverride = `-- name: UpsertUserAIBudgetOverride :one
|
|
INSERT INTO user_ai_budget_overrides (user_id, group_id, spend_limit_micros)
|
|
VALUES ($1, $2, $3)
|
|
ON CONFLICT (user_id) DO UPDATE SET
|
|
group_id = EXCLUDED.group_id,
|
|
spend_limit_micros = EXCLUDED.spend_limit_micros,
|
|
updated_at = NOW()
|
|
RETURNING user_id, group_id, spend_limit_micros, created_at, updated_at
|
|
`
|
|
|
|
type UpsertUserAIBudgetOverrideParams struct {
|
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
|
GroupID uuid.UUID `db:"group_id" json:"group_id"`
|
|
SpendLimitMicros int64 `db:"spend_limit_micros" json:"spend_limit_micros"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpsertUserAIBudgetOverride(ctx context.Context, arg UpsertUserAIBudgetOverrideParams) (UserAiBudgetOverride, error) {
|
|
row := q.db.QueryRowContext(ctx, upsertUserAIBudgetOverride, arg.UserID, arg.GroupID, arg.SpendLimitMicros)
|
|
var i UserAiBudgetOverride
|
|
err := row.Scan(
|
|
&i.UserID,
|
|
&i.GroupID,
|
|
&i.SpendLimitMicros,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getActiveAISeatCount = `-- name: GetActiveAISeatCount :one
|
|
SELECT
|
|
COUNT(*)
|
|
FROM
|
|
ai_seat_state ais
|
|
JOIN
|
|
users u
|
|
ON
|
|
ais.user_id = u.id
|
|
WHERE
|
|
u.status = 'active'::user_status
|
|
AND u.deleted = false
|
|
AND u.is_system = false
|
|
`
|
|
|
|
func (q *sqlQuerier) GetActiveAISeatCount(ctx context.Context) (int64, error) {
|
|
row := q.db.QueryRowContext(ctx, getActiveAISeatCount)
|
|
var count int64
|
|
err := row.Scan(&count)
|
|
return count, err
|
|
}
|
|
|
|
const upsertAISeatState = `-- name: UpsertAISeatState :one
|
|
INSERT INTO ai_seat_state (
|
|
user_id,
|
|
first_used_at,
|
|
last_used_at,
|
|
last_event_type,
|
|
last_event_description,
|
|
updated_at
|
|
)
|
|
VALUES
|
|
($1, $2, $2, $3, $4, $2)
|
|
ON CONFLICT (user_id) DO UPDATE
|
|
SET
|
|
last_used_at = EXCLUDED.last_used_at,
|
|
last_event_type = EXCLUDED.last_event_type,
|
|
last_event_description = EXCLUDED.last_event_description,
|
|
updated_at = EXCLUDED.updated_at
|
|
RETURNING
|
|
-- Postgres vodoo to know if a row was inserted.
|
|
(xmax = 0)::boolean AS is_new
|
|
`
|
|
|
|
type UpsertAISeatStateParams struct {
|
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
|
FirstUsedAt time.Time `db:"first_used_at" json:"first_used_at"`
|
|
LastEventType AiSeatUsageReason `db:"last_event_type" json:"last_event_type"`
|
|
LastEventDescription string `db:"last_event_description" json:"last_event_description"`
|
|
}
|
|
|
|
// Returns true if a new rows was inserted, false otherwise.
|
|
func (q *sqlQuerier) UpsertAISeatState(ctx context.Context, arg UpsertAISeatStateParams) (bool, error) {
|
|
row := q.db.QueryRowContext(ctx, upsertAISeatState,
|
|
arg.UserID,
|
|
arg.FirstUsedAt,
|
|
arg.LastEventType,
|
|
arg.LastEventDescription,
|
|
)
|
|
var is_new bool
|
|
err := row.Scan(&is_new)
|
|
return is_new, err
|
|
}
|
|
|
|
const getUserAISeatStates = `-- name: GetUserAISeatStates :many
|
|
SELECT
|
|
ais.user_id
|
|
FROM
|
|
ai_seat_state ais
|
|
JOIN
|
|
users u
|
|
ON
|
|
ais.user_id = u.id
|
|
WHERE
|
|
ais.user_id = ANY($1::uuid[])
|
|
AND u.status = 'active'::user_status
|
|
AND u.deleted = false
|
|
AND u.is_system = false
|
|
`
|
|
|
|
// Returns user IDs from the provided list that are consuming an AI seat.
|
|
// Filters to active, non-deleted, non-system users to match the canonical
|
|
// seat count query (GetActiveAISeatCount).
|
|
func (q *sqlQuerier) GetUserAISeatStates(ctx context.Context, userIds []uuid.UUID) ([]uuid.UUID, error) {
|
|
rows, err := q.db.QueryContext(ctx, getUserAISeatStates, pq.Array(userIds))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []uuid.UUID
|
|
for rows.Next() {
|
|
var user_id uuid.UUID
|
|
if err := rows.Scan(&user_id); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, user_id)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const deleteAPIKeyByID = `-- name: DeleteAPIKeyByID :exec
|
|
DELETE FROM
|
|
api_keys
|
|
WHERE
|
|
id = $1
|
|
`
|
|
|
|
func (q *sqlQuerier) DeleteAPIKeyByID(ctx context.Context, id string) error {
|
|
_, err := q.db.ExecContext(ctx, deleteAPIKeyByID, id)
|
|
return err
|
|
}
|
|
|
|
const deleteAPIKeysByUserID = `-- name: DeleteAPIKeysByUserID :exec
|
|
DELETE FROM
|
|
api_keys
|
|
WHERE
|
|
user_id = $1
|
|
`
|
|
|
|
func (q *sqlQuerier) DeleteAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error {
|
|
_, err := q.db.ExecContext(ctx, deleteAPIKeysByUserID, userID)
|
|
return err
|
|
}
|
|
|
|
const deleteApplicationConnectAPIKeysByUserID = `-- name: DeleteApplicationConnectAPIKeysByUserID :exec
|
|
DELETE FROM
|
|
api_keys
|
|
WHERE
|
|
user_id = $1 AND
|
|
'coder:application_connect'::api_key_scope = ANY(scopes)
|
|
`
|
|
|
|
func (q *sqlQuerier) DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error {
|
|
_, err := q.db.ExecContext(ctx, deleteApplicationConnectAPIKeysByUserID, userID)
|
|
return err
|
|
}
|
|
|
|
const deleteExpiredAPIKeys = `-- name: DeleteExpiredAPIKeys :execrows
|
|
WITH expired_keys AS (
|
|
SELECT id
|
|
FROM api_keys
|
|
-- expired keys only
|
|
WHERE expires_at < $1::timestamptz
|
|
LIMIT $2
|
|
)
|
|
DELETE FROM
|
|
api_keys
|
|
USING
|
|
expired_keys
|
|
WHERE
|
|
api_keys.id = expired_keys.id
|
|
`
|
|
|
|
type DeleteExpiredAPIKeysParams struct {
|
|
Before time.Time `db:"before" json:"before"`
|
|
LimitCount int32 `db:"limit_count" json:"limit_count"`
|
|
}
|
|
|
|
func (q *sqlQuerier) DeleteExpiredAPIKeys(ctx context.Context, arg DeleteExpiredAPIKeysParams) (int64, error) {
|
|
result, err := q.db.ExecContext(ctx, deleteExpiredAPIKeys, arg.Before, arg.LimitCount)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected()
|
|
}
|
|
|
|
const expirePrebuildsAPIKeys = `-- name: ExpirePrebuildsAPIKeys :exec
|
|
WITH unexpired_prebuilds_workspace_session_tokens AS (
|
|
SELECT id, SUBSTRING(token_name FROM 38 FOR 36)::uuid AS workspace_id
|
|
FROM api_keys
|
|
WHERE user_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid
|
|
AND expires_at > $1::timestamptz
|
|
AND token_name SIMILAR TO 'c42fdf75-3097-471c-8c33-fb52454d81c0_[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}_session_token'
|
|
),
|
|
stale_prebuilds_workspace_session_tokens AS (
|
|
SELECT upwst.id
|
|
FROM unexpired_prebuilds_workspace_session_tokens upwst
|
|
LEFT JOIN workspaces w
|
|
ON w.id = upwst.workspace_id
|
|
WHERE w.owner_id <> 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid
|
|
),
|
|
unnamed_prebuilds_api_keys AS (
|
|
SELECT id
|
|
FROM api_keys
|
|
WHERE user_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid
|
|
AND token_name = ''
|
|
AND expires_at > $1::timestamptz
|
|
)
|
|
UPDATE api_keys
|
|
SET expires_at = $1::timestamptz
|
|
WHERE id IN (
|
|
SELECT id FROM stale_prebuilds_workspace_session_tokens
|
|
UNION
|
|
SELECT id FROM unnamed_prebuilds_api_keys
|
|
)
|
|
`
|
|
|
|
// Firstly, collect api_keys owned by the prebuilds user that correlate
|
|
// to workspaces no longer owned by the prebuilds user.
|
|
// Next, collect api_keys that belong to the prebuilds user but have no token name.
|
|
// These were most likely created via 'coder login' as the prebuilds user.
|
|
func (q *sqlQuerier) ExpirePrebuildsAPIKeys(ctx context.Context, now time.Time) error {
|
|
_, err := q.db.ExecContext(ctx, expirePrebuildsAPIKeys, now)
|
|
return err
|
|
}
|
|
|
|
const getAPIKeyByID = `-- name: GetAPIKeyByID :one
|
|
SELECT
|
|
id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, token_name, scopes, allow_list
|
|
FROM
|
|
api_keys
|
|
WHERE
|
|
id = $1
|
|
LIMIT
|
|
1
|
|
`
|
|
|
|
func (q *sqlQuerier) GetAPIKeyByID(ctx context.Context, id string) (APIKey, error) {
|
|
row := q.db.QueryRowContext(ctx, getAPIKeyByID, id)
|
|
var i APIKey
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.HashedSecret,
|
|
&i.UserID,
|
|
&i.LastUsed,
|
|
&i.ExpiresAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.LoginType,
|
|
&i.LifetimeSeconds,
|
|
&i.IPAddress,
|
|
&i.TokenName,
|
|
&i.Scopes,
|
|
&i.AllowList,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getAPIKeyByName = `-- name: GetAPIKeyByName :one
|
|
SELECT
|
|
id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, token_name, scopes, allow_list
|
|
FROM
|
|
api_keys
|
|
WHERE
|
|
user_id = $1 AND
|
|
token_name = $2 AND
|
|
token_name != ''
|
|
LIMIT
|
|
1
|
|
`
|
|
|
|
type GetAPIKeyByNameParams struct {
|
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
|
TokenName string `db:"token_name" json:"token_name"`
|
|
}
|
|
|
|
// there is no unique constraint on empty token names
|
|
func (q *sqlQuerier) GetAPIKeyByName(ctx context.Context, arg GetAPIKeyByNameParams) (APIKey, error) {
|
|
row := q.db.QueryRowContext(ctx, getAPIKeyByName, arg.UserID, arg.TokenName)
|
|
var i APIKey
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.HashedSecret,
|
|
&i.UserID,
|
|
&i.LastUsed,
|
|
&i.ExpiresAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.LoginType,
|
|
&i.LifetimeSeconds,
|
|
&i.IPAddress,
|
|
&i.TokenName,
|
|
&i.Scopes,
|
|
&i.AllowList,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getAPIKeysByLoginType = `-- name: GetAPIKeysByLoginType :many
|
|
SELECT id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, token_name, scopes, allow_list FROM api_keys WHERE login_type = $1
|
|
AND ($2::bool OR expires_at > now())
|
|
`
|
|
|
|
type GetAPIKeysByLoginTypeParams struct {
|
|
LoginType LoginType `db:"login_type" json:"login_type"`
|
|
IncludeExpired bool `db:"include_expired" json:"include_expired"`
|
|
}
|
|
|
|
func (q *sqlQuerier) GetAPIKeysByLoginType(ctx context.Context, arg GetAPIKeysByLoginTypeParams) ([]APIKey, error) {
|
|
rows, err := q.db.QueryContext(ctx, getAPIKeysByLoginType, arg.LoginType, arg.IncludeExpired)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []APIKey
|
|
for rows.Next() {
|
|
var i APIKey
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.HashedSecret,
|
|
&i.UserID,
|
|
&i.LastUsed,
|
|
&i.ExpiresAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.LoginType,
|
|
&i.LifetimeSeconds,
|
|
&i.IPAddress,
|
|
&i.TokenName,
|
|
&i.Scopes,
|
|
&i.AllowList,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getAPIKeysByUserID = `-- name: GetAPIKeysByUserID :many
|
|
SELECT id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, token_name, scopes, allow_list FROM api_keys WHERE login_type = $1 AND user_id = $2
|
|
AND ($3::bool OR expires_at > now())
|
|
`
|
|
|
|
type GetAPIKeysByUserIDParams struct {
|
|
LoginType LoginType `db:"login_type" json:"login_type"`
|
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
|
IncludeExpired bool `db:"include_expired" json:"include_expired"`
|
|
}
|
|
|
|
func (q *sqlQuerier) GetAPIKeysByUserID(ctx context.Context, arg GetAPIKeysByUserIDParams) ([]APIKey, error) {
|
|
rows, err := q.db.QueryContext(ctx, getAPIKeysByUserID, arg.LoginType, arg.UserID, arg.IncludeExpired)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []APIKey
|
|
for rows.Next() {
|
|
var i APIKey
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.HashedSecret,
|
|
&i.UserID,
|
|
&i.LastUsed,
|
|
&i.ExpiresAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.LoginType,
|
|
&i.LifetimeSeconds,
|
|
&i.IPAddress,
|
|
&i.TokenName,
|
|
&i.Scopes,
|
|
&i.AllowList,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getAPIKeysLastUsedAfter = `-- name: GetAPIKeysLastUsedAfter :many
|
|
SELECT id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, token_name, scopes, allow_list FROM api_keys WHERE last_used > $1
|
|
`
|
|
|
|
func (q *sqlQuerier) GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.Time) ([]APIKey, error) {
|
|
rows, err := q.db.QueryContext(ctx, getAPIKeysLastUsedAfter, lastUsed)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []APIKey
|
|
for rows.Next() {
|
|
var i APIKey
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.HashedSecret,
|
|
&i.UserID,
|
|
&i.LastUsed,
|
|
&i.ExpiresAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.LoginType,
|
|
&i.LifetimeSeconds,
|
|
&i.IPAddress,
|
|
&i.TokenName,
|
|
&i.Scopes,
|
|
&i.AllowList,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const 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, owner_id 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,
|
|
&i.OwnerID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const insertBoundaryLogs = `-- name: InsertBoundaryLogs :many
|
|
INSERT INTO boundary_logs (
|
|
id,
|
|
session_id,
|
|
sequence_number,
|
|
captured_at,
|
|
created_at,
|
|
proto,
|
|
method,
|
|
detail,
|
|
matched_rule
|
|
)
|
|
SELECT
|
|
unnest($1 :: uuid[]),
|
|
$2 :: uuid,
|
|
unnest($3 :: int[]),
|
|
unnest($4 :: timestamptz[]),
|
|
unnest($5 :: timestamptz[]),
|
|
unnest($6 :: text[]),
|
|
unnest($7 :: text[]),
|
|
unnest($8 :: text[]),
|
|
unnest($9 :: text[])
|
|
RETURNING id, session_id, sequence_number, captured_at, created_at, proto, method, detail, matched_rule
|
|
`
|
|
|
|
type InsertBoundaryLogsParams 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 []string `db:"matched_rule" json:"matched_rule"`
|
|
}
|
|
|
|
func (q *sqlQuerier) InsertBoundaryLogs(ctx context.Context, arg InsertBoundaryLogsParams) ([]BoundaryLog, error) {
|
|
rows, err := q.db.QueryContext(ctx, insertBoundaryLogs,
|
|
pq.Array(arg.ID),
|
|
arg.SessionID,
|
|
pq.Array(arg.SequenceNumber),
|
|
pq.Array(arg.CapturedAt),
|
|
pq.Array(arg.CreatedAt),
|
|
pq.Array(arg.Proto),
|
|
pq.Array(arg.Method),
|
|
pq.Array(arg.Detail),
|
|
pq.Array(arg.MatchedRule),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []BoundaryLog
|
|
for rows.Next() {
|
|
var i BoundaryLog
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.SessionID,
|
|
&i.SequenceNumber,
|
|
&i.CapturedAt,
|
|
&i.CreatedAt,
|
|
&i.Proto,
|
|
&i.Method,
|
|
&i.Detail,
|
|
&i.MatchedRule,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const insertBoundarySession = `-- name: InsertBoundarySession :one
|
|
INSERT INTO boundary_sessions (
|
|
id,
|
|
workspace_agent_id,
|
|
owner_id,
|
|
confined_process_name,
|
|
started_at,
|
|
updated_at
|
|
) VALUES (
|
|
$1,
|
|
$2,
|
|
$3,
|
|
$4,
|
|
$5,
|
|
$6
|
|
) RETURNING id, workspace_agent_id, confined_process_name, started_at, updated_at, owner_id
|
|
`
|
|
|
|
type InsertBoundarySessionParams struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
|
|
OwnerID uuid.NullUUID `db:"owner_id" json:"owner_id"`
|
|
ConfinedProcessName string `db:"confined_process_name" json:"confined_process_name"`
|
|
StartedAt time.Time `db:"started_at" json:"started_at"`
|
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
|
}
|
|
|
|
func (q *sqlQuerier) InsertBoundarySession(ctx context.Context, arg InsertBoundarySessionParams) (BoundarySession, error) {
|
|
row := q.db.QueryRowContext(ctx, insertBoundarySession,
|
|
arg.ID,
|
|
arg.WorkspaceAgentID,
|
|
arg.OwnerID,
|
|
arg.ConfinedProcessName,
|
|
arg.StartedAt,
|
|
arg.UpdatedAt,
|
|
)
|
|
var i BoundarySession
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceAgentID,
|
|
&i.ConfinedProcessName,
|
|
&i.StartedAt,
|
|
&i.UpdatedAt,
|
|
&i.OwnerID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const listBoundaryLogsBySessionID = `-- name: ListBoundaryLogsBySessionID :many
|
|
SELECT id, session_id, sequence_number, captured_at, created_at, proto, method, detail, matched_rule
|
|
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 clearChatGoalByID = `-- name: ClearChatGoalByID :one
|
|
UPDATE
|
|
chat_goals
|
|
SET
|
|
status = 'cleared',
|
|
completion_summary = NULL,
|
|
completed_by_user_id = NULL,
|
|
completed_by_agent = FALSE,
|
|
completed_at = NULL,
|
|
updated_at = NOW(),
|
|
cleared_at = NOW()
|
|
WHERE
|
|
root_chat_id = $1::uuid
|
|
AND id = $2::uuid
|
|
AND status IN ('active', 'paused', 'complete')
|
|
RETURNING id, goal_order, root_chat_id, created_from_chat_id, created_from_message_id, objective, status, completion_summary, created_by_user_id, completed_by_user_id, completed_by_agent, created_at, updated_at, completed_at, cleared_at, replaced_at
|
|
`
|
|
|
|
type ClearChatGoalByIDParams struct {
|
|
RootChatID uuid.UUID `db:"root_chat_id" json:"root_chat_id"`
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
}
|
|
|
|
func (q *sqlQuerier) ClearChatGoalByID(ctx context.Context, arg ClearChatGoalByIDParams) (ChatGoal, error) {
|
|
row := q.db.QueryRowContext(ctx, clearChatGoalByID, arg.RootChatID, arg.ID)
|
|
var i ChatGoal
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.GoalOrder,
|
|
&i.RootChatID,
|
|
&i.CreatedFromChatID,
|
|
&i.CreatedFromMessageID,
|
|
&i.Objective,
|
|
&i.Status,
|
|
&i.CompletionSummary,
|
|
&i.CreatedByUserID,
|
|
&i.CompletedByUserID,
|
|
&i.CompletedByAgent,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.CompletedAt,
|
|
&i.ClearedAt,
|
|
&i.ReplacedAt,
|
|
)
|
|
return i, 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 completeChatGoalByID = `-- name: CompleteChatGoalByID :one
|
|
UPDATE
|
|
chat_goals
|
|
SET
|
|
status = 'complete',
|
|
completion_summary = $1::text,
|
|
completed_by_user_id = $2::uuid,
|
|
completed_by_agent = $3::bool,
|
|
updated_at = NOW(),
|
|
completed_at = NOW()
|
|
WHERE
|
|
root_chat_id = $4::uuid
|
|
AND id = $5::uuid
|
|
AND status = 'active'
|
|
RETURNING id, goal_order, root_chat_id, created_from_chat_id, created_from_message_id, objective, status, completion_summary, created_by_user_id, completed_by_user_id, completed_by_agent, created_at, updated_at, completed_at, cleared_at, replaced_at
|
|
`
|
|
|
|
type CompleteChatGoalByIDParams struct {
|
|
CompletionSummary sql.NullString `db:"completion_summary" json:"completion_summary"`
|
|
CompletedByUserID uuid.NullUUID `db:"completed_by_user_id" json:"completed_by_user_id"`
|
|
CompletedByAgent bool `db:"completed_by_agent" json:"completed_by_agent"`
|
|
RootChatID uuid.UUID `db:"root_chat_id" json:"root_chat_id"`
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
}
|
|
|
|
func (q *sqlQuerier) CompleteChatGoalByID(ctx context.Context, arg CompleteChatGoalByIDParams) (ChatGoal, error) {
|
|
row := q.db.QueryRowContext(ctx, completeChatGoalByID,
|
|
arg.CompletionSummary,
|
|
arg.CompletedByUserID,
|
|
arg.CompletedByAgent,
|
|
arg.RootChatID,
|
|
arg.ID,
|
|
)
|
|
var i ChatGoal
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.GoalOrder,
|
|
&i.RootChatID,
|
|
&i.CreatedFromChatID,
|
|
&i.CreatedFromMessageID,
|
|
&i.Objective,
|
|
&i.Status,
|
|
&i.CompletionSummary,
|
|
&i.CreatedByUserID,
|
|
&i.CompletedByUserID,
|
|
&i.CompletedByAgent,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.CompletedAt,
|
|
&i.ClearedAt,
|
|
&i.ReplacedAt,
|
|
)
|
|
return i, 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 getChatGoalMessageIDsByMessageIDs = `-- name: GetChatGoalMessageIDsByMessageIDs :many
|
|
SELECT DISTINCT
|
|
created_from_message_id::bigint AS message_id
|
|
FROM
|
|
chat_goals
|
|
WHERE
|
|
created_from_message_id = ANY($1::bigint[])
|
|
`
|
|
|
|
func (q *sqlQuerier) GetChatGoalMessageIDsByMessageIDs(ctx context.Context, messageIds []int64) ([]int64, error) {
|
|
rows, err := q.db.QueryContext(ctx, getChatGoalMessageIDsByMessageIDs, pq.Array(messageIds))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []int64
|
|
for rows.Next() {
|
|
var message_id int64
|
|
if err := rows.Scan(&message_id); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, message_id)
|
|
}
|
|
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 getCurrentChatGoalByRootChatID = `-- name: GetCurrentChatGoalByRootChatID :one
|
|
SELECT
|
|
chat_goals.id, chat_goals.goal_order, chat_goals.root_chat_id, chat_goals.created_from_chat_id, chat_goals.created_from_message_id, chat_goals.objective, chat_goals.status, chat_goals.completion_summary, chat_goals.created_by_user_id, chat_goals.completed_by_user_id, chat_goals.completed_by_agent, chat_goals.created_at, chat_goals.updated_at, chat_goals.completed_at, chat_goals.cleared_at, chat_goals.replaced_at
|
|
FROM
|
|
chat_goals
|
|
WHERE
|
|
id = (
|
|
SELECT
|
|
id
|
|
FROM
|
|
chat_goals
|
|
WHERE
|
|
root_chat_id = $1::uuid
|
|
ORDER BY
|
|
created_at DESC,
|
|
goal_order DESC
|
|
LIMIT 1
|
|
)
|
|
AND status IN ('active', 'paused', 'complete')
|
|
`
|
|
|
|
func (q *sqlQuerier) GetCurrentChatGoalByRootChatID(ctx context.Context, rootChatID uuid.UUID) (ChatGoal, error) {
|
|
row := q.db.QueryRowContext(ctx, getCurrentChatGoalByRootChatID, rootChatID)
|
|
var i ChatGoal
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.GoalOrder,
|
|
&i.RootChatID,
|
|
&i.CreatedFromChatID,
|
|
&i.CreatedFromMessageID,
|
|
&i.Objective,
|
|
&i.Status,
|
|
&i.CompletionSummary,
|
|
&i.CreatedByUserID,
|
|
&i.CompletedByUserID,
|
|
&i.CompletedByAgent,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.CompletedAt,
|
|
&i.ClearedAt,
|
|
&i.ReplacedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getCurrentChatGoalsByRootChatIDs = `-- name: GetCurrentChatGoalsByRootChatIDs :many
|
|
WITH latest_goal_ids AS (
|
|
SELECT DISTINCT ON (root_chat_id)
|
|
id
|
|
FROM
|
|
chat_goals
|
|
WHERE
|
|
root_chat_id = ANY($1::uuid[])
|
|
ORDER BY
|
|
root_chat_id,
|
|
created_at DESC,
|
|
goal_order DESC
|
|
)
|
|
SELECT
|
|
chat_goals.id, chat_goals.goal_order, chat_goals.root_chat_id, chat_goals.created_from_chat_id, chat_goals.created_from_message_id, chat_goals.objective, chat_goals.status, chat_goals.completion_summary, chat_goals.created_by_user_id, chat_goals.completed_by_user_id, chat_goals.completed_by_agent, chat_goals.created_at, chat_goals.updated_at, chat_goals.completed_at, chat_goals.cleared_at, chat_goals.replaced_at
|
|
FROM
|
|
chat_goals
|
|
JOIN latest_goal_ids ON latest_goal_ids.id = chat_goals.id
|
|
WHERE
|
|
chat_goals.status IN ('active', 'paused', 'complete')
|
|
`
|
|
|
|
func (q *sqlQuerier) GetCurrentChatGoalsByRootChatIDs(ctx context.Context, rootChatIds []uuid.UUID) ([]ChatGoal, error) {
|
|
rows, err := q.db.QueryContext(ctx, getCurrentChatGoalsByRootChatIDs, pq.Array(rootChatIds))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ChatGoal
|
|
for rows.Next() {
|
|
var i ChatGoal
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.GoalOrder,
|
|
&i.RootChatID,
|
|
&i.CreatedFromChatID,
|
|
&i.CreatedFromMessageID,
|
|
&i.Objective,
|
|
&i.Status,
|
|
&i.CompletionSummary,
|
|
&i.CreatedByUserID,
|
|
&i.CompletedByUserID,
|
|
&i.CompletedByAgent,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.CompletedAt,
|
|
&i.ClearedAt,
|
|
&i.ReplacedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(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 insertActiveChatGoal = `-- name: InsertActiveChatGoal :one
|
|
INSERT INTO chat_goals (
|
|
root_chat_id,
|
|
created_from_chat_id,
|
|
created_from_message_id,
|
|
objective,
|
|
status,
|
|
created_by_user_id
|
|
) VALUES (
|
|
$1::uuid,
|
|
$2::uuid,
|
|
$3::bigint,
|
|
$4::text,
|
|
'active',
|
|
$5::uuid
|
|
)
|
|
RETURNING id, goal_order, root_chat_id, created_from_chat_id, created_from_message_id, objective, status, completion_summary, created_by_user_id, completed_by_user_id, completed_by_agent, created_at, updated_at, completed_at, cleared_at, replaced_at
|
|
`
|
|
|
|
type InsertActiveChatGoalParams struct {
|
|
RootChatID uuid.UUID `db:"root_chat_id" json:"root_chat_id"`
|
|
CreatedFromChatID uuid.NullUUID `db:"created_from_chat_id" json:"created_from_chat_id"`
|
|
CreatedFromMessageID sql.NullInt64 `db:"created_from_message_id" json:"created_from_message_id"`
|
|
Objective string `db:"objective" json:"objective"`
|
|
CreatedByUserID uuid.UUID `db:"created_by_user_id" json:"created_by_user_id"`
|
|
}
|
|
|
|
func (q *sqlQuerier) InsertActiveChatGoal(ctx context.Context, arg InsertActiveChatGoalParams) (ChatGoal, error) {
|
|
row := q.db.QueryRowContext(ctx, insertActiveChatGoal,
|
|
arg.RootChatID,
|
|
arg.CreatedFromChatID,
|
|
arg.CreatedFromMessageID,
|
|
arg.Objective,
|
|
arg.CreatedByUserID,
|
|
)
|
|
var i ChatGoal
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.GoalOrder,
|
|
&i.RootChatID,
|
|
&i.CreatedFromChatID,
|
|
&i.CreatedFromMessageID,
|
|
&i.Objective,
|
|
&i.Status,
|
|
&i.CompletionSummary,
|
|
&i.CreatedByUserID,
|
|
&i.CompletedByUserID,
|
|
&i.CompletedByAgent,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.CompletedAt,
|
|
&i.ClearedAt,
|
|
&i.ReplacedAt,
|
|
)
|
|
return i, 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 markCurrentChatGoalReplacedByRootChatID = `-- name: MarkCurrentChatGoalReplacedByRootChatID :many
|
|
UPDATE
|
|
chat_goals
|
|
SET
|
|
status = 'replaced',
|
|
updated_at = NOW(),
|
|
replaced_at = NOW()
|
|
WHERE
|
|
root_chat_id = $1::uuid
|
|
AND status IN ('active', 'paused')
|
|
RETURNING id, goal_order, root_chat_id, created_from_chat_id, created_from_message_id, objective, status, completion_summary, created_by_user_id, completed_by_user_id, completed_by_agent, created_at, updated_at, completed_at, cleared_at, replaced_at
|
|
`
|
|
|
|
func (q *sqlQuerier) MarkCurrentChatGoalReplacedByRootChatID(ctx context.Context, rootChatID uuid.UUID) ([]ChatGoal, error) {
|
|
rows, err := q.db.QueryContext(ctx, markCurrentChatGoalReplacedByRootChatID, rootChatID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ChatGoal
|
|
for rows.Next() {
|
|
var i ChatGoal
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.GoalOrder,
|
|
&i.RootChatID,
|
|
&i.CreatedFromChatID,
|
|
&i.CreatedFromMessageID,
|
|
&i.Objective,
|
|
&i.Status,
|
|
&i.CompletionSummary,
|
|
&i.CreatedByUserID,
|
|
&i.CompletedByUserID,
|
|
&i.CompletedByAgent,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.CompletedAt,
|
|
&i.ClearedAt,
|
|
&i.ReplacedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const pauseChatGoalByID = `-- name: PauseChatGoalByID :one
|
|
UPDATE
|
|
chat_goals
|
|
SET
|
|
status = 'paused',
|
|
updated_at = NOW()
|
|
WHERE
|
|
root_chat_id = $1::uuid
|
|
AND id = $2::uuid
|
|
AND status = 'active'
|
|
RETURNING id, goal_order, root_chat_id, created_from_chat_id, created_from_message_id, objective, status, completion_summary, created_by_user_id, completed_by_user_id, completed_by_agent, created_at, updated_at, completed_at, cleared_at, replaced_at
|
|
`
|
|
|
|
type PauseChatGoalByIDParams struct {
|
|
RootChatID uuid.UUID `db:"root_chat_id" json:"root_chat_id"`
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
}
|
|
|
|
func (q *sqlQuerier) PauseChatGoalByID(ctx context.Context, arg PauseChatGoalByIDParams) (ChatGoal, error) {
|
|
row := q.db.QueryRowContext(ctx, pauseChatGoalByID, arg.RootChatID, arg.ID)
|
|
var i ChatGoal
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.GoalOrder,
|
|
&i.RootChatID,
|
|
&i.CreatedFromChatID,
|
|
&i.CreatedFromMessageID,
|
|
&i.Objective,
|
|
&i.Status,
|
|
&i.CompletionSummary,
|
|
&i.CreatedByUserID,
|
|
&i.CompletedByUserID,
|
|
&i.CompletedByAgent,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.CompletedAt,
|
|
&i.ClearedAt,
|
|
&i.ReplacedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
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 resumeChatGoalByID = `-- name: ResumeChatGoalByID :one
|
|
UPDATE
|
|
chat_goals
|
|
SET
|
|
status = 'active',
|
|
updated_at = NOW()
|
|
WHERE
|
|
root_chat_id = $1::uuid
|
|
AND id = $2::uuid
|
|
AND status = 'paused'
|
|
RETURNING id, goal_order, root_chat_id, created_from_chat_id, created_from_message_id, objective, status, completion_summary, created_by_user_id, completed_by_user_id, completed_by_agent, created_at, updated_at, completed_at, cleared_at, replaced_at
|
|
`
|
|
|
|
type ResumeChatGoalByIDParams struct {
|
|
RootChatID uuid.UUID `db:"root_chat_id" json:"root_chat_id"`
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
}
|
|
|
|
func (q *sqlQuerier) ResumeChatGoalByID(ctx context.Context, arg ResumeChatGoalByIDParams) (ChatGoal, error) {
|
|
row := q.db.QueryRowContext(ctx, resumeChatGoalByID, arg.RootChatID, arg.ID)
|
|
var i ChatGoal
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.GoalOrder,
|
|
&i.RootChatID,
|
|
&i.CreatedFromChatID,
|
|
&i.CreatedFromMessageID,
|
|
&i.Objective,
|
|
&i.Status,
|
|
&i.CompletionSummary,
|
|
&i.CreatedByUserID,
|
|
&i.CompletedByUserID,
|
|
&i.CompletedByAgent,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.CompletedAt,
|
|
&i.ClearedAt,
|
|
&i.ReplacedAt,
|
|
)
|
|
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, private_key_key_id
|
|
FROM
|
|
gitsshkeys
|
|
WHERE
|
|
user_id = $1
|
|
`
|
|
|
|
func (q *sqlQuerier) GetGitSSHKey(ctx context.Context, userID uuid.UUID) (GitSSHKey, error) {
|
|
row := q.db.QueryRowContext(ctx, getGitSSHKey, userID)
|
|
var i GitSSHKey
|
|
err := row.Scan(
|
|
&i.UserID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.PrivateKey,
|
|
&i.PublicKey,
|
|
&i.PrivateKeyKeyID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const insertGitSSHKey = `-- name: InsertGitSSHKey :one
|
|
INSERT INTO
|
|
gitsshkeys (
|
|
user_id,
|
|
created_at,
|
|
updated_at,
|
|
private_key,
|
|
private_key_key_id,
|
|
public_key
|
|
)
|
|
VALUES
|
|
($1, $2, $3, $4, $5, $6) RETURNING user_id, created_at, updated_at, private_key, public_key, private_key_key_id
|
|
`
|
|
|
|
type InsertGitSSHKeyParams struct {
|
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
|
PrivateKey string `db:"private_key" json:"private_key"`
|
|
PrivateKeyKeyID sql.NullString `db:"private_key_key_id" json:"private_key_key_id"`
|
|
PublicKey string `db:"public_key" json:"public_key"`
|
|
}
|
|
|
|
func (q *sqlQuerier) InsertGitSSHKey(ctx context.Context, arg InsertGitSSHKeyParams) (GitSSHKey, error) {
|
|
row := q.db.QueryRowContext(ctx, insertGitSSHKey,
|
|
arg.UserID,
|
|
arg.CreatedAt,
|
|
arg.UpdatedAt,
|
|
arg.PrivateKey,
|
|
arg.PrivateKeyKeyID,
|
|
arg.PublicKey,
|
|
)
|
|
var i GitSSHKey
|
|
err := row.Scan(
|
|
&i.UserID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.PrivateKey,
|
|
&i.PublicKey,
|
|
&i.PrivateKeyKeyID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const updateGitSSHKey = `-- name: UpdateGitSSHKey :one
|
|
UPDATE
|
|
gitsshkeys
|
|
SET
|
|
updated_at = $2,
|
|
private_key = $3,
|
|
private_key_key_id = $4,
|
|
public_key = $5
|
|
WHERE
|
|
user_id = $1
|
|
RETURNING
|
|
user_id, created_at, updated_at, private_key, public_key, private_key_key_id
|
|
`
|
|
|
|
type UpdateGitSSHKeyParams struct {
|
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
|
PrivateKey string `db:"private_key" json:"private_key"`
|
|
PrivateKeyKeyID sql.NullString `db:"private_key_key_id" json:"private_key_key_id"`
|
|
PublicKey string `db:"public_key" json:"public_key"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateGitSSHKey(ctx context.Context, arg UpdateGitSSHKeyParams) (GitSSHKey, error) {
|
|
row := q.db.QueryRowContext(ctx, updateGitSSHKey,
|
|
arg.UserID,
|
|
arg.UpdatedAt,
|
|
arg.PrivateKey,
|
|
arg.PrivateKeyKeyID,
|
|
arg.PublicKey,
|
|
)
|
|
var i GitSSHKey
|
|
err := row.Scan(
|
|
&i.UserID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.PrivateKey,
|
|
&i.PublicKey,
|
|
&i.PrivateKeyKeyID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const deleteGroupMemberFromGroup = `-- name: DeleteGroupMemberFromGroup :exec
|
|
DELETE FROM
|
|
group_members
|
|
WHERE
|
|
user_id = $1 AND
|
|
group_id = $2
|
|
`
|
|
|
|
type DeleteGroupMemberFromGroupParams struct {
|
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
|
GroupID uuid.UUID `db:"group_id" json:"group_id"`
|
|
}
|
|
|
|
func (q *sqlQuerier) DeleteGroupMemberFromGroup(ctx context.Context, arg DeleteGroupMemberFromGroupParams) error {
|
|
_, err := q.db.ExecContext(ctx, deleteGroupMemberFromGroup, arg.UserID, arg.GroupID)
|
|
return err
|
|
}
|
|
|
|
const getGroupMembers = `-- name: GetGroupMembers :many
|
|
SELECT user_id, user_email, user_username, user_hashed_password, user_created_at, user_updated_at, user_status, user_rbac_roles, user_login_type, user_avatar_url, user_deleted, user_last_seen_at, user_quiet_hours_schedule, user_name, user_github_com_user_id, user_is_system, user_is_service_account, organization_id, group_name, group_id FROM group_members_expanded
|
|
WHERE CASE
|
|
WHEN $1::bool THEN TRUE
|
|
ELSE
|
|
user_is_system = false
|
|
END
|
|
`
|
|
|
|
func (q *sqlQuerier) GetGroupMembers(ctx context.Context, includeSystem bool) ([]GroupMember, error) {
|
|
rows, err := q.db.QueryContext(ctx, getGroupMembers, includeSystem)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []GroupMember
|
|
for rows.Next() {
|
|
var i GroupMember
|
|
if err := rows.Scan(
|
|
&i.UserID,
|
|
&i.UserEmail,
|
|
&i.UserUsername,
|
|
&i.UserHashedPassword,
|
|
&i.UserCreatedAt,
|
|
&i.UserUpdatedAt,
|
|
&i.UserStatus,
|
|
pq.Array(&i.UserRbacRoles),
|
|
&i.UserLoginType,
|
|
&i.UserAvatarUrl,
|
|
&i.UserDeleted,
|
|
&i.UserLastSeenAt,
|
|
&i.UserQuietHoursSchedule,
|
|
&i.UserName,
|
|
&i.UserGithubComUserID,
|
|
&i.UserIsSystem,
|
|
&i.UserIsServiceAccount,
|
|
&i.OrganizationID,
|
|
&i.GroupName,
|
|
&i.GroupID,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getGroupMembersByGroupID = `-- name: GetGroupMembersByGroupID :many
|
|
SELECT user_id, user_email, user_username, user_hashed_password, user_created_at, user_updated_at, user_status, user_rbac_roles, user_login_type, user_avatar_url, user_deleted, user_last_seen_at, user_quiet_hours_schedule, user_name, user_github_com_user_id, user_is_system, user_is_service_account, organization_id, group_name, group_id
|
|
FROM group_members_expanded
|
|
WHERE group_id = $1
|
|
-- Filter by system type
|
|
AND CASE
|
|
WHEN $2::bool THEN TRUE
|
|
ELSE
|
|
user_is_system = false
|
|
END
|
|
`
|
|
|
|
type GetGroupMembersByGroupIDParams struct {
|
|
GroupID uuid.UUID `db:"group_id" json:"group_id"`
|
|
IncludeSystem bool `db:"include_system" json:"include_system"`
|
|
}
|
|
|
|
func (q *sqlQuerier) GetGroupMembersByGroupID(ctx context.Context, arg GetGroupMembersByGroupIDParams) ([]GroupMember, error) {
|
|
rows, err := q.db.QueryContext(ctx, getGroupMembersByGroupID, arg.GroupID, arg.IncludeSystem)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []GroupMember
|
|
for rows.Next() {
|
|
var i GroupMember
|
|
if err := rows.Scan(
|
|
&i.UserID,
|
|
&i.UserEmail,
|
|
&i.UserUsername,
|
|
&i.UserHashedPassword,
|
|
&i.UserCreatedAt,
|
|
&i.UserUpdatedAt,
|
|
&i.UserStatus,
|
|
pq.Array(&i.UserRbacRoles),
|
|
&i.UserLoginType,
|
|
&i.UserAvatarUrl,
|
|
&i.UserDeleted,
|
|
&i.UserLastSeenAt,
|
|
&i.UserQuietHoursSchedule,
|
|
&i.UserName,
|
|
&i.UserGithubComUserID,
|
|
&i.UserIsSystem,
|
|
&i.UserIsServiceAccount,
|
|
&i.OrganizationID,
|
|
&i.GroupName,
|
|
&i.GroupID,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getGroupMembersByGroupIDPaginated = `-- name: GetGroupMembersByGroupIDPaginated :many
|
|
SELECT
|
|
user_id, user_email, user_username, user_hashed_password, user_created_at, user_updated_at, user_status, user_rbac_roles, user_login_type, user_avatar_url, user_deleted, user_last_seen_at, user_quiet_hours_schedule, user_name, user_github_com_user_id, user_is_system, user_is_service_account, organization_id, group_name, group_id, COUNT(*) OVER() AS count
|
|
FROM
|
|
group_members_expanded
|
|
WHERE
|
|
group_members_expanded.group_id = $1
|
|
AND CASE
|
|
-- This allows using the last element on a page as effectively a cursor.
|
|
-- This is an important option for scripts that need to paginate without
|
|
-- duplicating or missing data.
|
|
WHEN $2 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN (
|
|
-- The pagination cursor is the last ID of the previous page.
|
|
-- The query is ordered by the username field, so select all
|
|
-- rows after the cursor.
|
|
(LOWER(user_username)) > (
|
|
SELECT
|
|
LOWER(user_username)
|
|
FROM
|
|
group_members_expanded
|
|
WHERE
|
|
group_id = $1
|
|
AND user_id = $2
|
|
)
|
|
)
|
|
ELSE true
|
|
END
|
|
-- Start filters
|
|
-- Filter by email 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, default_org_member_roles
|
|
FROM
|
|
organizations
|
|
WHERE
|
|
is_default = true
|
|
LIMIT
|
|
1
|
|
`
|
|
|
|
func (q *sqlQuerier) GetDefaultOrganization(ctx context.Context) (Organization, error) {
|
|
row := q.db.QueryRowContext(ctx, getDefaultOrganization)
|
|
var i Organization
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.Description,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.IsDefault,
|
|
&i.DisplayName,
|
|
&i.Icon,
|
|
&i.Deleted,
|
|
&i.ShareableWorkspaceOwners,
|
|
pq.Array(&i.DefaultOrgMemberRoles),
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getOrganizationByID = `-- name: GetOrganizationByID :one
|
|
SELECT
|
|
id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, shareable_workspace_owners, default_org_member_roles
|
|
FROM
|
|
organizations
|
|
WHERE
|
|
id = $1
|
|
`
|
|
|
|
func (q *sqlQuerier) GetOrganizationByID(ctx context.Context, id uuid.UUID) (Organization, error) {
|
|
row := q.db.QueryRowContext(ctx, getOrganizationByID, id)
|
|
var i Organization
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.Description,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.IsDefault,
|
|
&i.DisplayName,
|
|
&i.Icon,
|
|
&i.Deleted,
|
|
&i.ShareableWorkspaceOwners,
|
|
pq.Array(&i.DefaultOrgMemberRoles),
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getOrganizationByName = `-- name: GetOrganizationByName :one
|
|
SELECT
|
|
id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, shareable_workspace_owners, default_org_member_roles
|
|
FROM
|
|
organizations
|
|
WHERE
|
|
-- Optionally include deleted organizations
|
|
deleted = $1 AND
|
|
LOWER("name") = LOWER($2)
|
|
LIMIT
|
|
1
|
|
`
|
|
|
|
type GetOrganizationByNameParams struct {
|
|
Deleted bool `db:"deleted" json:"deleted"`
|
|
Name string `db:"name" json:"name"`
|
|
}
|
|
|
|
func (q *sqlQuerier) GetOrganizationByName(ctx context.Context, arg GetOrganizationByNameParams) (Organization, error) {
|
|
row := q.db.QueryRowContext(ctx, getOrganizationByName, arg.Deleted, arg.Name)
|
|
var i Organization
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.Description,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.IsDefault,
|
|
&i.DisplayName,
|
|
&i.Icon,
|
|
&i.Deleted,
|
|
&i.ShareableWorkspaceOwners,
|
|
pq.Array(&i.DefaultOrgMemberRoles),
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getOrganizationResourceCountByID = `-- name: GetOrganizationResourceCountByID :one
|
|
SELECT
|
|
(
|
|
SELECT
|
|
count(*)
|
|
FROM
|
|
workspaces
|
|
WHERE
|
|
workspaces.organization_id = $1
|
|
AND workspaces.deleted = FALSE) AS workspace_count,
|
|
(
|
|
SELECT
|
|
count(*)
|
|
FROM
|
|
GROUPS
|
|
WHERE
|
|
groups.organization_id = $1) AS group_count,
|
|
(
|
|
SELECT
|
|
count(*)
|
|
FROM
|
|
templates
|
|
WHERE
|
|
templates.organization_id = $1
|
|
AND templates.deleted = FALSE) AS template_count,
|
|
(
|
|
SELECT
|
|
count(*)
|
|
FROM
|
|
organization_members
|
|
LEFT JOIN users ON organization_members.user_id = users.id
|
|
WHERE
|
|
organization_members.organization_id = $1
|
|
AND users.deleted = FALSE) AS member_count,
|
|
(
|
|
SELECT
|
|
count(*)
|
|
FROM
|
|
provisioner_keys
|
|
WHERE
|
|
provisioner_keys.organization_id = $1) AS provisioner_key_count
|
|
`
|
|
|
|
type GetOrganizationResourceCountByIDRow struct {
|
|
WorkspaceCount int64 `db:"workspace_count" json:"workspace_count"`
|
|
GroupCount int64 `db:"group_count" json:"group_count"`
|
|
TemplateCount int64 `db:"template_count" json:"template_count"`
|
|
MemberCount int64 `db:"member_count" json:"member_count"`
|
|
ProvisionerKeyCount int64 `db:"provisioner_key_count" json:"provisioner_key_count"`
|
|
}
|
|
|
|
func (q *sqlQuerier) GetOrganizationResourceCountByID(ctx context.Context, organizationID uuid.UUID) (GetOrganizationResourceCountByIDRow, error) {
|
|
row := q.db.QueryRowContext(ctx, getOrganizationResourceCountByID, organizationID)
|
|
var i GetOrganizationResourceCountByIDRow
|
|
err := row.Scan(
|
|
&i.WorkspaceCount,
|
|
&i.GroupCount,
|
|
&i.TemplateCount,
|
|
&i.MemberCount,
|
|
&i.ProvisionerKeyCount,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getOrganizations = `-- name: GetOrganizations :many
|
|
SELECT
|
|
id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, shareable_workspace_owners, default_org_member_roles
|
|
FROM
|
|
organizations
|
|
WHERE
|
|
-- Optionally include deleted organizations
|
|
deleted = $1
|
|
-- Filter by ids
|
|
AND CASE
|
|
WHEN array_length($2 :: uuid[], 1) > 0 THEN
|
|
id = ANY($2)
|
|
ELSE true
|
|
END
|
|
AND CASE
|
|
WHEN $3::text != '' THEN
|
|
LOWER("name") = LOWER($3)
|
|
ELSE true
|
|
END
|
|
`
|
|
|
|
type GetOrganizationsParams struct {
|
|
Deleted bool `db:"deleted" json:"deleted"`
|
|
IDs []uuid.UUID `db:"ids" json:"ids"`
|
|
Name string `db:"name" json:"name"`
|
|
}
|
|
|
|
func (q *sqlQuerier) GetOrganizations(ctx context.Context, arg GetOrganizationsParams) ([]Organization, error) {
|
|
rows, err := q.db.QueryContext(ctx, getOrganizations, arg.Deleted, pq.Array(arg.IDs), arg.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []Organization
|
|
for rows.Next() {
|
|
var i Organization
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.Description,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.IsDefault,
|
|
&i.DisplayName,
|
|
&i.Icon,
|
|
&i.Deleted,
|
|
&i.ShareableWorkspaceOwners,
|
|
pq.Array(&i.DefaultOrgMemberRoles),
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getOrganizationsByUserID = `-- name: GetOrganizationsByUserID :many
|
|
SELECT
|
|
id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, shareable_workspace_owners, default_org_member_roles
|
|
FROM
|
|
organizations
|
|
WHERE
|
|
-- Optionally provide a filter for deleted organizations.
|
|
CASE WHEN
|
|
$2 :: boolean IS NULL THEN
|
|
true
|
|
ELSE
|
|
deleted = $2
|
|
END AND
|
|
id = ANY(
|
|
SELECT
|
|
organization_id
|
|
FROM
|
|
organization_members
|
|
WHERE
|
|
user_id = $1
|
|
)
|
|
`
|
|
|
|
type GetOrganizationsByUserIDParams struct {
|
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
|
Deleted sql.NullBool `db:"deleted" json:"deleted"`
|
|
}
|
|
|
|
func (q *sqlQuerier) GetOrganizationsByUserID(ctx context.Context, arg GetOrganizationsByUserIDParams) ([]Organization, error) {
|
|
rows, err := q.db.QueryContext(ctx, getOrganizationsByUserID, arg.UserID, arg.Deleted)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []Organization
|
|
for rows.Next() {
|
|
var i Organization
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.Description,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.IsDefault,
|
|
&i.DisplayName,
|
|
&i.Icon,
|
|
&i.Deleted,
|
|
&i.ShareableWorkspaceOwners,
|
|
pq.Array(&i.DefaultOrgMemberRoles),
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const insertOrganization = `-- name: InsertOrganization :one
|
|
INSERT INTO
|
|
organizations (id, "name", display_name, description, icon, created_at, updated_at, is_default, default_org_member_roles)
|
|
VALUES
|
|
-- If no organizations exist, and this is the first, make it the default.
|
|
($1, $2, $3, $4, $5, $6, $7, (SELECT TRUE FROM organizations LIMIT 1) IS NULL, $8) RETURNING id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, shareable_workspace_owners, default_org_member_roles
|
|
`
|
|
|
|
type InsertOrganizationParams struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
Name string `db:"name" json:"name"`
|
|
DisplayName string `db:"display_name" json:"display_name"`
|
|
Description string `db:"description" json:"description"`
|
|
Icon string `db:"icon" json:"icon"`
|
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
|
DefaultOrgMemberRoles []string `db:"default_org_member_roles" json:"default_org_member_roles"`
|
|
}
|
|
|
|
func (q *sqlQuerier) InsertOrganization(ctx context.Context, arg InsertOrganizationParams) (Organization, error) {
|
|
row := q.db.QueryRowContext(ctx, insertOrganization,
|
|
arg.ID,
|
|
arg.Name,
|
|
arg.DisplayName,
|
|
arg.Description,
|
|
arg.Icon,
|
|
arg.CreatedAt,
|
|
arg.UpdatedAt,
|
|
pq.Array(arg.DefaultOrgMemberRoles),
|
|
)
|
|
var i Organization
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.Description,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.IsDefault,
|
|
&i.DisplayName,
|
|
&i.Icon,
|
|
&i.Deleted,
|
|
&i.ShareableWorkspaceOwners,
|
|
pq.Array(&i.DefaultOrgMemberRoles),
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const updateOrganization = `-- name: UpdateOrganization :one
|
|
UPDATE
|
|
organizations
|
|
SET
|
|
updated_at = $1,
|
|
name = $2,
|
|
display_name = $3,
|
|
description = $4,
|
|
icon = $5,
|
|
default_org_member_roles = $6
|
|
WHERE
|
|
id = $7
|
|
RETURNING id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, shareable_workspace_owners, default_org_member_roles
|
|
`
|
|
|
|
type UpdateOrganizationParams struct {
|
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
|
Name string `db:"name" json:"name"`
|
|
DisplayName string `db:"display_name" json:"display_name"`
|
|
Description string `db:"description" json:"description"`
|
|
Icon string `db:"icon" json:"icon"`
|
|
DefaultOrgMemberRoles []string `db:"default_org_member_roles" json:"default_org_member_roles"`
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateOrganization(ctx context.Context, arg UpdateOrganizationParams) (Organization, error) {
|
|
row := q.db.QueryRowContext(ctx, updateOrganization,
|
|
arg.UpdatedAt,
|
|
arg.Name,
|
|
arg.DisplayName,
|
|
arg.Description,
|
|
arg.Icon,
|
|
pq.Array(arg.DefaultOrgMemberRoles),
|
|
arg.ID,
|
|
)
|
|
var i Organization
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.Description,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.IsDefault,
|
|
&i.DisplayName,
|
|
&i.Icon,
|
|
&i.Deleted,
|
|
&i.ShareableWorkspaceOwners,
|
|
pq.Array(&i.DefaultOrgMemberRoles),
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const updateOrganizationDeletedByID = `-- name: UpdateOrganizationDeletedByID :exec
|
|
UPDATE organizations
|
|
SET
|
|
deleted = true,
|
|
updated_at = $1
|
|
WHERE
|
|
id = $2 AND
|
|
is_default = false
|
|
`
|
|
|
|
type UpdateOrganizationDeletedByIDParams struct {
|
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateOrganizationDeletedByID(ctx context.Context, arg UpdateOrganizationDeletedByIDParams) error {
|
|
_, err := q.db.ExecContext(ctx, updateOrganizationDeletedByID, arg.UpdatedAt, arg.ID)
|
|
return err
|
|
}
|
|
|
|
const updateOrganizationWorkspaceSharingSettings = `-- name: UpdateOrganizationWorkspaceSharingSettings :one
|
|
UPDATE
|
|
organizations
|
|
SET
|
|
shareable_workspace_owners = $1,
|
|
updated_at = $2
|
|
WHERE
|
|
id = $3
|
|
RETURNING id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, shareable_workspace_owners, default_org_member_roles
|
|
`
|
|
|
|
type UpdateOrganizationWorkspaceSharingSettingsParams struct {
|
|
ShareableWorkspaceOwners ShareableWorkspaceOwners `db:"shareable_workspace_owners" json:"shareable_workspace_owners"`
|
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateOrganizationWorkspaceSharingSettings(ctx context.Context, arg UpdateOrganizationWorkspaceSharingSettingsParams) (Organization, error) {
|
|
row := q.db.QueryRowContext(ctx, updateOrganizationWorkspaceSharingSettings, arg.ShareableWorkspaceOwners, arg.UpdatedAt, arg.ID)
|
|
var i Organization
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.Description,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.IsDefault,
|
|
&i.DisplayName,
|
|
&i.Icon,
|
|
&i.Deleted,
|
|
&i.ShareableWorkspaceOwners,
|
|
pq.Array(&i.DefaultOrgMemberRoles),
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getParameterSchemasByJobID = `-- name: GetParameterSchemasByJobID :many
|
|
SELECT
|
|
id, created_at, job_id, name, description, default_source_scheme, default_source_value, allow_override_source, default_destination_scheme, allow_override_destination, default_refresh, redisplay_value, validation_error, validation_condition, validation_type_system, validation_value_type, index
|
|
FROM
|
|
parameter_schemas
|
|
WHERE
|
|
job_id = $1
|
|
ORDER BY
|
|
index
|
|
`
|
|
|
|
func (q *sqlQuerier) GetParameterSchemasByJobID(ctx context.Context, jobID uuid.UUID) ([]ParameterSchema, error) {
|
|
rows, err := q.db.QueryContext(ctx, getParameterSchemasByJobID, jobID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ParameterSchema
|
|
for rows.Next() {
|
|
var i ParameterSchema
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.CreatedAt,
|
|
&i.JobID,
|
|
&i.Name,
|
|
&i.Description,
|
|
&i.DefaultSourceScheme,
|
|
&i.DefaultSourceValue,
|
|
&i.AllowOverrideSource,
|
|
&i.DefaultDestinationScheme,
|
|
&i.AllowOverrideDestination,
|
|
&i.DefaultRefresh,
|
|
&i.RedisplayValue,
|
|
&i.ValidationError,
|
|
&i.ValidationCondition,
|
|
&i.ValidationTypeSystem,
|
|
&i.ValidationValueType,
|
|
&i.Index,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const claimPrebuiltWorkspace = `-- name: ClaimPrebuiltWorkspace :one
|
|
UPDATE workspaces w
|
|
SET owner_id = $1::uuid,
|
|
name = $2::text,
|
|
updated_at = $3::timestamptz,
|
|
-- Update autostart_schedule, next_start_at and ttl according to template and workspace-level
|
|
-- configurations, allowing the workspace to be managed by the lifecycle executor as expected.
|
|
autostart_schedule = $4,
|
|
next_start_at = $5,
|
|
ttl = $6,
|
|
-- Update last_used_at during claim to ensure the claimed workspace is treated as recently used.
|
|
-- This avoids unintended dormancy caused by prebuilds having stale usage timestamps.
|
|
last_used_at = $3::timestamptz,
|
|
-- Clear dormant and deletion timestamps as a safeguard to ensure a clean lifecycle state after claim.
|
|
-- These fields should not be set on prebuilds, but we defensively reset them here to prevent
|
|
-- accidental dormancy or deletion by the lifecycle executor.
|
|
dormant_at = NULL,
|
|
deleting_at = NULL
|
|
WHERE w.id IN (
|
|
SELECT p.id
|
|
FROM workspace_prebuilds p
|
|
INNER JOIN workspace_latest_builds b ON b.workspace_id = p.id
|
|
INNER JOIN templates t ON p.template_id = t.id
|
|
WHERE (b.transition = 'start'::workspace_transition
|
|
AND b.job_status IN ('succeeded'::provisioner_job_status))
|
|
-- The prebuilds system should never try to claim a prebuild for an inactive template version.
|
|
-- Nevertheless, this filter is here as a defensive measure:
|
|
AND b.template_version_id = t.active_version_id
|
|
AND p.current_preset_id = $7::uuid
|
|
AND p.ready
|
|
AND NOT t.deleted
|
|
LIMIT 1 FOR UPDATE OF p SKIP LOCKED -- Ensure that a concurrent request will not select the same prebuild.
|
|
)
|
|
RETURNING w.id, w.name
|
|
`
|
|
|
|
type ClaimPrebuiltWorkspaceParams struct {
|
|
NewUserID uuid.UUID `db:"new_user_id" json:"new_user_id"`
|
|
NewName string `db:"new_name" json:"new_name"`
|
|
Now time.Time `db:"now" json:"now"`
|
|
AutostartSchedule sql.NullString `db:"autostart_schedule" json:"autostart_schedule"`
|
|
NextStartAt sql.NullTime `db:"next_start_at" json:"next_start_at"`
|
|
WorkspaceTtl sql.NullInt64 `db:"workspace_ttl" json:"workspace_ttl"`
|
|
PresetID uuid.UUID `db:"preset_id" json:"preset_id"`
|
|
}
|
|
|
|
type ClaimPrebuiltWorkspaceRow struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
Name string `db:"name" json:"name"`
|
|
}
|
|
|
|
func (q *sqlQuerier) ClaimPrebuiltWorkspace(ctx context.Context, arg ClaimPrebuiltWorkspaceParams) (ClaimPrebuiltWorkspaceRow, error) {
|
|
row := q.db.QueryRowContext(ctx, claimPrebuiltWorkspace,
|
|
arg.NewUserID,
|
|
arg.NewName,
|
|
arg.Now,
|
|
arg.AutostartSchedule,
|
|
arg.NextStartAt,
|
|
arg.WorkspaceTtl,
|
|
arg.PresetID,
|
|
)
|
|
var i ClaimPrebuiltWorkspaceRow
|
|
err := row.Scan(&i.ID, &i.Name)
|
|
return i, err
|
|
}
|
|
|
|
const countInProgressPrebuilds = `-- name: CountInProgressPrebuilds :many
|
|
SELECT t.id AS template_id, wpb.template_version_id, wpb.transition, COUNT(wpb.transition)::int AS count, wlb.template_version_preset_id as preset_id
|
|
FROM workspace_latest_builds wlb
|
|
INNER JOIN workspace_prebuild_builds wpb ON wpb.id = wlb.id
|
|
-- We only need these counts for active template versions.
|
|
-- It doesn't influence whether we create or delete prebuilds
|
|
-- for inactive template versions. This is because we never create
|
|
-- prebuilds for inactive template versions, we always delete
|
|
-- running prebuilds for inactive template versions, and we ignore
|
|
-- prebuilds that are still building.
|
|
INNER JOIN templates t ON t.active_version_id = wlb.template_version_id
|
|
WHERE wlb.job_status IN ('pending'::provisioner_job_status, 'running'::provisioner_job_status)
|
|
-- AND NOT t.deleted -- We don't exclude deleted templates because there's no constraint in the DB preventing a soft deletion on a template while workspaces are running.
|
|
GROUP BY t.id, wpb.template_version_id, wpb.transition, wlb.template_version_preset_id
|
|
`
|
|
|
|
type CountInProgressPrebuildsRow struct {
|
|
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
|
|
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
|
|
Transition WorkspaceTransition `db:"transition" json:"transition"`
|
|
Count int32 `db:"count" json:"count"`
|
|
PresetID uuid.NullUUID `db:"preset_id" json:"preset_id"`
|
|
}
|
|
|
|
// CountInProgressPrebuilds returns the number of in-progress prebuilds, grouped by preset ID and transition.
|
|
// Prebuild considered in-progress if it's in the "pending", "starting", "stopping", or "deleting" state.
|
|
func (q *sqlQuerier) CountInProgressPrebuilds(ctx context.Context) ([]CountInProgressPrebuildsRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, countInProgressPrebuilds)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []CountInProgressPrebuildsRow
|
|
for rows.Next() {
|
|
var i CountInProgressPrebuildsRow
|
|
if err := rows.Scan(
|
|
&i.TemplateID,
|
|
&i.TemplateVersionID,
|
|
&i.Transition,
|
|
&i.Count,
|
|
&i.PresetID,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const countPendingNonActivePrebuilds = `-- name: CountPendingNonActivePrebuilds :many
|
|
SELECT
|
|
wpb.template_version_preset_id AS preset_id,
|
|
COUNT(*)::int AS count
|
|
FROM workspace_prebuild_builds wpb
|
|
INNER JOIN provisioner_jobs pj ON pj.id = wpb.job_id
|
|
INNER JOIN workspaces w ON w.id = wpb.workspace_id
|
|
INNER JOIN templates t ON t.id = w.template_id
|
|
WHERE
|
|
wpb.template_version_id != t.active_version_id
|
|
-- Only considers initial builds, i.e. created by the reconciliation loop
|
|
AND wpb.build_number = 1
|
|
-- Only consider 'start' transitions (provisioning), not 'stop'/'delete' (deprovisioning)
|
|
-- Deprovisioning jobs should complete naturally as they're already cleaning up resources
|
|
AND wpb.transition = 'start'::workspace_transition
|
|
-- Pending jobs that have not yet been picked up by a provisioner
|
|
AND pj.job_status = 'pending'::provisioner_job_status
|
|
AND pj.worker_id IS NULL
|
|
AND pj.canceled_at IS NULL
|
|
AND pj.completed_at IS NULL
|
|
GROUP BY wpb.template_version_preset_id
|
|
`
|
|
|
|
type CountPendingNonActivePrebuildsRow struct {
|
|
PresetID uuid.NullUUID `db:"preset_id" json:"preset_id"`
|
|
Count int32 `db:"count" json:"count"`
|
|
}
|
|
|
|
// CountPendingNonActivePrebuilds returns the number of pending prebuilds for non-active template versions
|
|
func (q *sqlQuerier) CountPendingNonActivePrebuilds(ctx context.Context) ([]CountPendingNonActivePrebuildsRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, countPendingNonActivePrebuilds)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []CountPendingNonActivePrebuildsRow
|
|
for rows.Next() {
|
|
var i CountPendingNonActivePrebuildsRow
|
|
if err := rows.Scan(&i.PresetID, &i.Count); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const findMatchingPresetID = `-- name: FindMatchingPresetID :one
|
|
WITH provided_params AS (
|
|
SELECT
|
|
unnest($1::text[]) AS name,
|
|
unnest($2::text[]) AS value
|
|
),
|
|
preset_matches AS (
|
|
SELECT
|
|
tvp.id AS template_version_preset_id,
|
|
COALESCE(COUNT(tvpp.name), 0) AS total_preset_params,
|
|
COALESCE(COUNT(pp.name), 0) AS matching_params
|
|
FROM template_version_presets tvp
|
|
LEFT JOIN template_version_preset_parameters tvpp ON tvpp.template_version_preset_id = tvp.id
|
|
LEFT JOIN provided_params pp ON pp.name = tvpp.name AND pp.value = tvpp.value
|
|
WHERE tvp.template_version_id = $3
|
|
GROUP BY tvp.id
|
|
)
|
|
SELECT pm.template_version_preset_id
|
|
FROM preset_matches pm
|
|
WHERE pm.total_preset_params = pm.matching_params -- All preset parameters must match
|
|
ORDER BY pm.total_preset_params DESC -- Return the preset with the most parameters
|
|
LIMIT 1
|
|
`
|
|
|
|
type FindMatchingPresetIDParams struct {
|
|
ParameterNames []string `db:"parameter_names" json:"parameter_names"`
|
|
ParameterValues []string `db:"parameter_values" json:"parameter_values"`
|
|
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
|
|
}
|
|
|
|
// FindMatchingPresetID finds a preset ID that is the largest exact subset of the provided parameters.
|
|
// It returns the preset ID if a match is found, or NULL if no match is found.
|
|
// The query finds presets where all preset parameters are present in the provided parameters,
|
|
// and returns the preset with the most parameters (largest subset).
|
|
func (q *sqlQuerier) FindMatchingPresetID(ctx context.Context, arg FindMatchingPresetIDParams) (uuid.UUID, error) {
|
|
row := q.db.QueryRowContext(ctx, findMatchingPresetID, pq.Array(arg.ParameterNames), pq.Array(arg.ParameterValues), arg.TemplateVersionID)
|
|
var template_version_preset_id uuid.UUID
|
|
err := row.Scan(&template_version_preset_id)
|
|
return template_version_preset_id, err
|
|
}
|
|
|
|
const getOrganizationsWithPrebuildStatus = `-- name: GetOrganizationsWithPrebuildStatus :many
|
|
WITH orgs_with_prebuilds AS (
|
|
-- Get unique organizations that have presets with prebuilds configured
|
|
SELECT DISTINCT o.id, o.name
|
|
FROM organizations o
|
|
INNER JOIN templates t ON t.organization_id = o.id
|
|
INNER JOIN template_versions tv ON tv.template_id = t.id
|
|
INNER JOIN template_version_presets tvp ON tvp.template_version_id = tv.id
|
|
WHERE tvp.desired_instances IS NOT NULL
|
|
),
|
|
prebuild_user_membership AS (
|
|
-- Check if the user is a member of the organizations
|
|
SELECT om.organization_id
|
|
FROM organization_members om
|
|
INNER JOIN orgs_with_prebuilds owp ON owp.id = om.organization_id
|
|
WHERE om.user_id = $1::uuid
|
|
),
|
|
prebuild_groups AS (
|
|
-- Check if the organizations have the prebuilds group
|
|
SELECT g.organization_id, g.id as group_id
|
|
FROM groups g
|
|
INNER JOIN orgs_with_prebuilds owp ON owp.id = g.organization_id
|
|
WHERE g.name = $2::text
|
|
),
|
|
prebuild_group_membership AS (
|
|
-- Check if the user is in the prebuilds group
|
|
SELECT pg.organization_id
|
|
FROM prebuild_groups pg
|
|
INNER JOIN group_members gm ON gm.group_id = pg.group_id
|
|
WHERE gm.user_id = $1::uuid
|
|
)
|
|
SELECT
|
|
owp.id AS organization_id,
|
|
owp.name AS organization_name,
|
|
(pum.organization_id IS NOT NULL)::boolean AS has_prebuild_user,
|
|
pg.group_id AS prebuilds_group_id,
|
|
(pgm.organization_id IS NOT NULL)::boolean AS has_prebuild_user_in_group
|
|
FROM orgs_with_prebuilds owp
|
|
LEFT JOIN prebuild_groups pg ON pg.organization_id = owp.id
|
|
LEFT JOIN prebuild_user_membership pum ON pum.organization_id = owp.id
|
|
LEFT JOIN prebuild_group_membership pgm ON pgm.organization_id = owp.id
|
|
`
|
|
|
|
type GetOrganizationsWithPrebuildStatusParams struct {
|
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
|
GroupName string `db:"group_name" json:"group_name"`
|
|
}
|
|
|
|
type GetOrganizationsWithPrebuildStatusRow struct {
|
|
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
|
OrganizationName string `db:"organization_name" json:"organization_name"`
|
|
HasPrebuildUser bool `db:"has_prebuild_user" json:"has_prebuild_user"`
|
|
PrebuildsGroupID uuid.NullUUID `db:"prebuilds_group_id" json:"prebuilds_group_id"`
|
|
HasPrebuildUserInGroup bool `db:"has_prebuild_user_in_group" json:"has_prebuild_user_in_group"`
|
|
}
|
|
|
|
// GetOrganizationsWithPrebuildStatus returns organizations with prebuilds configured and their
|
|
// membership status for the prebuilds system user (org membership, group existence, group membership).
|
|
func (q *sqlQuerier) GetOrganizationsWithPrebuildStatus(ctx context.Context, arg GetOrganizationsWithPrebuildStatusParams) ([]GetOrganizationsWithPrebuildStatusRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, getOrganizationsWithPrebuildStatus, arg.UserID, arg.GroupName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []GetOrganizationsWithPrebuildStatusRow
|
|
for rows.Next() {
|
|
var i GetOrganizationsWithPrebuildStatusRow
|
|
if err := rows.Scan(
|
|
&i.OrganizationID,
|
|
&i.OrganizationName,
|
|
&i.HasPrebuildUser,
|
|
&i.PrebuildsGroupID,
|
|
&i.HasPrebuildUserInGroup,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getPrebuildMetrics = `-- name: GetPrebuildMetrics :many
|
|
SELECT
|
|
t.name as template_name,
|
|
tvp.name as preset_name,
|
|
o.name as organization_name,
|
|
COUNT(*) as created_count,
|
|
COUNT(*) FILTER (WHERE pj.job_status = 'failed'::provisioner_job_status) as failed_count,
|
|
COUNT(*) FILTER (
|
|
WHERE w.owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid -- The system user responsible for prebuilds.
|
|
) as claimed_count
|
|
FROM workspaces w
|
|
INNER JOIN workspace_prebuild_builds wpb ON wpb.workspace_id = w.id
|
|
INNER JOIN templates t ON t.id = w.template_id
|
|
INNER JOIN template_version_presets tvp ON tvp.id = wpb.template_version_preset_id
|
|
INNER JOIN provisioner_jobs pj ON pj.id = wpb.job_id
|
|
INNER JOIN organizations o ON o.id = w.organization_id
|
|
WHERE NOT t.deleted AND wpb.build_number = 1
|
|
GROUP BY t.name, tvp.name, o.name
|
|
ORDER BY t.name, tvp.name, o.name
|
|
`
|
|
|
|
type GetPrebuildMetricsRow struct {
|
|
TemplateName string `db:"template_name" json:"template_name"`
|
|
PresetName string `db:"preset_name" json:"preset_name"`
|
|
OrganizationName string `db:"organization_name" json:"organization_name"`
|
|
CreatedCount int64 `db:"created_count" json:"created_count"`
|
|
FailedCount int64 `db:"failed_count" json:"failed_count"`
|
|
ClaimedCount int64 `db:"claimed_count" json:"claimed_count"`
|
|
}
|
|
|
|
func (q *sqlQuerier) GetPrebuildMetrics(ctx context.Context) ([]GetPrebuildMetricsRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, getPrebuildMetrics)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []GetPrebuildMetricsRow
|
|
for rows.Next() {
|
|
var i GetPrebuildMetricsRow
|
|
if err := rows.Scan(
|
|
&i.TemplateName,
|
|
&i.PresetName,
|
|
&i.OrganizationName,
|
|
&i.CreatedCount,
|
|
&i.FailedCount,
|
|
&i.ClaimedCount,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getPresetsAtFailureLimit = `-- name: GetPresetsAtFailureLimit :many
|
|
WITH filtered_builds AS (
|
|
-- Only select builds which are for prebuild creations
|
|
SELECT wlb.template_version_id, wlb.created_at, tvp.id AS preset_id, wlb.job_status, tvp.desired_instances
|
|
FROM template_version_presets tvp
|
|
INNER JOIN workspace_latest_builds wlb ON wlb.template_version_preset_id = tvp.id
|
|
INNER JOIN workspaces w ON wlb.workspace_id = w.id
|
|
INNER JOIN template_versions tv ON wlb.template_version_id = tv.id
|
|
INNER JOIN templates t ON tv.template_id = t.id AND t.active_version_id = tv.id
|
|
WHERE tvp.desired_instances IS NOT NULL -- Consider only presets that have a prebuild configuration.
|
|
AND wlb.transition = 'start'::workspace_transition
|
|
AND w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'
|
|
),
|
|
time_sorted_builds AS (
|
|
-- Group builds by preset, then sort each group by created_at.
|
|
SELECT fb.template_version_id, fb.created_at, fb.preset_id, fb.job_status, fb.desired_instances,
|
|
ROW_NUMBER() OVER (PARTITION BY fb.preset_id ORDER BY fb.created_at DESC) as rn
|
|
FROM filtered_builds fb
|
|
)
|
|
SELECT
|
|
tsb.template_version_id,
|
|
tsb.preset_id
|
|
FROM time_sorted_builds tsb
|
|
WHERE tsb.rn <= $1::bigint
|
|
AND tsb.job_status = 'failed'::provisioner_job_status
|
|
GROUP BY tsb.template_version_id, tsb.preset_id
|
|
HAVING COUNT(*) = $1::bigint
|
|
`
|
|
|
|
type GetPresetsAtFailureLimitRow struct {
|
|
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
|
|
PresetID uuid.UUID `db:"preset_id" json:"preset_id"`
|
|
}
|
|
|
|
// GetPresetsAtFailureLimit groups workspace builds by preset ID.
|
|
// Each preset is associated with exactly one template version ID.
|
|
// For each preset, the query checks the last hard_limit builds.
|
|
// If all of them failed, the preset is considered to have hit the hard failure limit.
|
|
// The query returns a list of preset IDs that have reached this failure threshold.
|
|
// Only active template versions with configured presets are considered.
|
|
// For each preset, check the last hard_limit builds.
|
|
// If all of them failed, the preset is considered to have hit the hard failure limit.
|
|
func (q *sqlQuerier) GetPresetsAtFailureLimit(ctx context.Context, hardLimit int64) ([]GetPresetsAtFailureLimitRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, getPresetsAtFailureLimit, hardLimit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []GetPresetsAtFailureLimitRow
|
|
for rows.Next() {
|
|
var i GetPresetsAtFailureLimitRow
|
|
if err := rows.Scan(&i.TemplateVersionID, &i.PresetID); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getPresetsBackoff = `-- name: GetPresetsBackoff :many
|
|
WITH filtered_builds AS (
|
|
-- Only select builds which are for prebuild creations
|
|
SELECT wlb.template_version_id, wlb.created_at, tvp.id AS preset_id, wlb.job_status, tvp.desired_instances
|
|
FROM template_version_presets tvp
|
|
INNER JOIN workspace_latest_builds wlb ON wlb.template_version_preset_id = tvp.id
|
|
INNER JOIN workspaces w ON wlb.workspace_id = w.id
|
|
INNER JOIN template_versions tv ON wlb.template_version_id = tv.id
|
|
INNER JOIN templates t ON tv.template_id = t.id AND t.active_version_id = tv.id
|
|
WHERE tvp.desired_instances IS NOT NULL -- Consider only presets that have a prebuild configuration.
|
|
AND wlb.transition = 'start'::workspace_transition
|
|
AND w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'
|
|
AND NOT t.deleted
|
|
),
|
|
time_sorted_builds AS (
|
|
-- Group builds by preset, then sort each group by created_at.
|
|
SELECT fb.template_version_id, fb.created_at, fb.preset_id, fb.job_status, fb.desired_instances,
|
|
ROW_NUMBER() OVER (PARTITION BY fb.preset_id ORDER BY fb.created_at DESC) as rn
|
|
FROM filtered_builds fb
|
|
),
|
|
failed_count AS (
|
|
-- Count failed builds per preset in the given period
|
|
SELECT preset_id, COUNT(*) AS num_failed
|
|
FROM filtered_builds
|
|
WHERE job_status = 'failed'::provisioner_job_status
|
|
AND created_at >= $1::timestamptz
|
|
GROUP BY preset_id
|
|
)
|
|
SELECT
|
|
tsb.template_version_id,
|
|
tsb.preset_id,
|
|
COALESCE(fc.num_failed, 0)::int AS num_failed,
|
|
MAX(tsb.created_at)::timestamptz AS last_build_at
|
|
FROM time_sorted_builds tsb
|
|
LEFT JOIN failed_count fc ON fc.preset_id = tsb.preset_id
|
|
WHERE tsb.rn <= tsb.desired_instances -- Fetch the last N builds, where N is the number of desired instances; if any fail, we backoff
|
|
AND tsb.job_status = 'failed'::provisioner_job_status
|
|
AND created_at >= $1::timestamptz
|
|
GROUP BY tsb.template_version_id, tsb.preset_id, fc.num_failed
|
|
`
|
|
|
|
type GetPresetsBackoffRow struct {
|
|
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
|
|
PresetID uuid.UUID `db:"preset_id" json:"preset_id"`
|
|
NumFailed int32 `db:"num_failed" json:"num_failed"`
|
|
LastBuildAt time.Time `db:"last_build_at" json:"last_build_at"`
|
|
}
|
|
|
|
// GetPresetsBackoff groups workspace builds by preset ID.
|
|
// Each preset is associated with exactly one template version ID.
|
|
// For each group, the query checks up to N of the most recent jobs that occurred within the
|
|
// lookback period, where N equals the number of desired instances for the corresponding preset.
|
|
// If at least one of the job within a group has failed, we should backoff on the corresponding preset ID.
|
|
// Query returns a list of preset IDs for which we should backoff.
|
|
// Only active template versions with configured presets are considered.
|
|
// We also return the number of failed workspace builds that occurred during the lookback period.
|
|
//
|
|
// NOTE:
|
|
// - To **decide whether to back off**, we look at up to the N most recent builds (within the defined lookback period).
|
|
// - To **calculate the number of failed builds**, we consider all builds within the defined lookback period.
|
|
//
|
|
// The number of failed builds is used downstream to determine the backoff duration.
|
|
func (q *sqlQuerier) GetPresetsBackoff(ctx context.Context, lookback time.Time) ([]GetPresetsBackoffRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, getPresetsBackoff, lookback)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []GetPresetsBackoffRow
|
|
for rows.Next() {
|
|
var i GetPresetsBackoffRow
|
|
if err := rows.Scan(
|
|
&i.TemplateVersionID,
|
|
&i.PresetID,
|
|
&i.NumFailed,
|
|
&i.LastBuildAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getRunningPrebuiltWorkspaces = `-- name: GetRunningPrebuiltWorkspaces :many
|
|
WITH latest_prebuilds AS (
|
|
-- All workspaces that match the following criteria:
|
|
-- 1. Owned by prebuilds user
|
|
-- 2. Not deleted
|
|
-- 3. Latest build is a 'start' transition
|
|
-- 4. Latest build was successful
|
|
SELECT
|
|
workspaces.id,
|
|
workspaces.name,
|
|
workspaces.template_id,
|
|
workspace_latest_builds.template_version_id,
|
|
workspace_latest_builds.job_id,
|
|
workspaces.created_at
|
|
FROM workspace_latest_builds
|
|
JOIN workspaces ON workspaces.id = workspace_latest_builds.workspace_id
|
|
WHERE workspace_latest_builds.transition = 'start'::workspace_transition
|
|
AND workspace_latest_builds.job_status = 'succeeded'::provisioner_job_status
|
|
AND workspaces.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID
|
|
AND NOT workspaces.deleted
|
|
),
|
|
workspace_latest_presets AS (
|
|
-- For each of the above workspaces, the preset_id of the most recent
|
|
-- successful start transition.
|
|
SELECT DISTINCT ON (latest_prebuilds.id)
|
|
latest_prebuilds.id AS workspace_id,
|
|
workspace_builds.template_version_preset_id AS current_preset_id
|
|
FROM latest_prebuilds
|
|
JOIN workspace_builds ON workspace_builds.workspace_id = latest_prebuilds.id
|
|
WHERE workspace_builds.transition = 'start'::workspace_transition
|
|
AND workspace_builds.template_version_preset_id IS NOT NULL
|
|
ORDER BY latest_prebuilds.id, workspace_builds.build_number DESC
|
|
),
|
|
ready_agents AS (
|
|
-- For each of the above workspaces, check if all agents are ready.
|
|
SELECT
|
|
latest_prebuilds.job_id,
|
|
BOOL_AND(workspace_agents.lifecycle_state = 'ready'::workspace_agent_lifecycle_state)::boolean AS ready
|
|
FROM latest_prebuilds
|
|
JOIN workspace_resources ON workspace_resources.job_id = latest_prebuilds.job_id
|
|
JOIN workspace_agents ON workspace_agents.resource_id = workspace_resources.id
|
|
WHERE workspace_agents.deleted = false
|
|
AND workspace_agents.parent_id IS NULL
|
|
GROUP BY latest_prebuilds.job_id
|
|
)
|
|
SELECT
|
|
latest_prebuilds.id,
|
|
latest_prebuilds.name,
|
|
latest_prebuilds.template_id,
|
|
latest_prebuilds.template_version_id,
|
|
workspace_latest_presets.current_preset_id,
|
|
COALESCE(ready_agents.ready, false)::boolean AS ready,
|
|
latest_prebuilds.created_at
|
|
FROM latest_prebuilds
|
|
LEFT JOIN ready_agents ON ready_agents.job_id = latest_prebuilds.job_id
|
|
LEFT JOIN workspace_latest_presets ON workspace_latest_presets.workspace_id = latest_prebuilds.id
|
|
ORDER BY latest_prebuilds.id
|
|
`
|
|
|
|
type GetRunningPrebuiltWorkspacesRow struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
Name string `db:"name" json:"name"`
|
|
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
|
|
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
|
|
CurrentPresetID uuid.NullUUID `db:"current_preset_id" json:"current_preset_id"`
|
|
Ready bool `db:"ready" json:"ready"`
|
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
|
}
|
|
|
|
func (q *sqlQuerier) GetRunningPrebuiltWorkspaces(ctx context.Context) ([]GetRunningPrebuiltWorkspacesRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, getRunningPrebuiltWorkspaces)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []GetRunningPrebuiltWorkspacesRow
|
|
for rows.Next() {
|
|
var i GetRunningPrebuiltWorkspacesRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.TemplateID,
|
|
&i.TemplateVersionID,
|
|
&i.CurrentPresetID,
|
|
&i.Ready,
|
|
&i.CreatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getTemplatePresetsWithPrebuilds = `-- name: GetTemplatePresetsWithPrebuilds :many
|
|
SELECT
|
|
t.id AS template_id,
|
|
t.name AS template_name,
|
|
o.id AS organization_id,
|
|
o.name AS organization_name,
|
|
tv.id AS template_version_id,
|
|
tv.name AS template_version_name,
|
|
tv.id = t.active_version_id AS using_active_version,
|
|
tvp.id,
|
|
tvp.name,
|
|
tvp.desired_instances AS desired_instances,
|
|
tvp.scheduling_timezone,
|
|
tvp.invalidate_after_secs AS ttl,
|
|
tvp.prebuild_status,
|
|
tvp.last_invalidated_at,
|
|
t.deleted,
|
|
t.deprecated != '' AS deprecated
|
|
FROM templates t
|
|
INNER JOIN template_versions tv ON tv.template_id = t.id
|
|
INNER JOIN template_version_presets tvp ON tvp.template_version_id = tv.id
|
|
INNER JOIN organizations o ON o.id = t.organization_id
|
|
WHERE tvp.desired_instances IS NOT NULL -- Consider only presets that have a prebuild configuration.
|
|
-- AND NOT t.deleted -- We don't exclude deleted templates because there's no constraint in the DB preventing a soft deletion on a template while workspaces are running.
|
|
AND (t.id = $1::uuid OR $1 IS NULL)
|
|
`
|
|
|
|
type GetTemplatePresetsWithPrebuildsRow struct {
|
|
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
|
|
TemplateName string `db:"template_name" json:"template_name"`
|
|
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
|
OrganizationName string `db:"organization_name" json:"organization_name"`
|
|
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
|
|
TemplateVersionName string `db:"template_version_name" json:"template_version_name"`
|
|
UsingActiveVersion bool `db:"using_active_version" json:"using_active_version"`
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
Name string `db:"name" json:"name"`
|
|
DesiredInstances sql.NullInt32 `db:"desired_instances" json:"desired_instances"`
|
|
SchedulingTimezone string `db:"scheduling_timezone" json:"scheduling_timezone"`
|
|
Ttl sql.NullInt32 `db:"ttl" json:"ttl"`
|
|
PrebuildStatus PrebuildStatus `db:"prebuild_status" json:"prebuild_status"`
|
|
LastInvalidatedAt sql.NullTime `db:"last_invalidated_at" json:"last_invalidated_at"`
|
|
Deleted bool `db:"deleted" json:"deleted"`
|
|
Deprecated bool `db:"deprecated" json:"deprecated"`
|
|
}
|
|
|
|
// GetTemplatePresetsWithPrebuilds retrieves template versions with configured presets and prebuilds.
|
|
// It also returns the number of desired instances for each preset.
|
|
// If template_id is specified, only template versions associated with that template will be returned.
|
|
func (q *sqlQuerier) GetTemplatePresetsWithPrebuilds(ctx context.Context, templateID uuid.NullUUID) ([]GetTemplatePresetsWithPrebuildsRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, getTemplatePresetsWithPrebuilds, templateID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []GetTemplatePresetsWithPrebuildsRow
|
|
for rows.Next() {
|
|
var i GetTemplatePresetsWithPrebuildsRow
|
|
if err := rows.Scan(
|
|
&i.TemplateID,
|
|
&i.TemplateName,
|
|
&i.OrganizationID,
|
|
&i.OrganizationName,
|
|
&i.TemplateVersionID,
|
|
&i.TemplateVersionName,
|
|
&i.UsingActiveVersion,
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.DesiredInstances,
|
|
&i.SchedulingTimezone,
|
|
&i.Ttl,
|
|
&i.PrebuildStatus,
|
|
&i.LastInvalidatedAt,
|
|
&i.Deleted,
|
|
&i.Deprecated,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const updatePrebuildProvisionerJobWithCancel = `-- name: UpdatePrebuildProvisionerJobWithCancel :many
|
|
WITH jobs_to_cancel AS (
|
|
SELECT pj.id, w.id AS workspace_id, w.template_id, wpb.template_version_preset_id
|
|
FROM provisioner_jobs pj
|
|
INNER JOIN workspace_prebuild_builds wpb ON wpb.job_id = pj.id
|
|
INNER JOIN workspaces w ON w.id = wpb.workspace_id
|
|
INNER JOIN templates t ON t.id = w.template_id
|
|
WHERE
|
|
wpb.template_version_id != t.active_version_id
|
|
AND wpb.template_version_preset_id = $2
|
|
-- Only considers initial builds, i.e. created by the reconciliation loop
|
|
AND wpb.build_number = 1
|
|
-- Only consider 'start' transitions (provisioning), not 'stop'/'delete' (deprovisioning)
|
|
-- Deprovisioning jobs should complete naturally as they're already cleaning up resources
|
|
AND wpb.transition = 'start'::workspace_transition
|
|
-- Pending jobs that have not yet been picked up by a provisioner
|
|
AND pj.job_status = 'pending'::provisioner_job_status
|
|
AND pj.worker_id IS NULL
|
|
AND pj.canceled_at IS NULL
|
|
AND pj.completed_at IS NULL
|
|
)
|
|
UPDATE provisioner_jobs
|
|
SET
|
|
canceled_at = $1::timestamptz,
|
|
completed_at = $1::timestamptz
|
|
FROM jobs_to_cancel
|
|
WHERE provisioner_jobs.id = jobs_to_cancel.id
|
|
RETURNING jobs_to_cancel.id, jobs_to_cancel.workspace_id, jobs_to_cancel.template_id, jobs_to_cancel.template_version_preset_id
|
|
`
|
|
|
|
type UpdatePrebuildProvisionerJobWithCancelParams struct {
|
|
Now time.Time `db:"now" json:"now"`
|
|
PresetID uuid.NullUUID `db:"preset_id" json:"preset_id"`
|
|
}
|
|
|
|
type UpdatePrebuildProvisionerJobWithCancelRow struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
|
|
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
|
|
TemplateVersionPresetID uuid.NullUUID `db:"template_version_preset_id" json:"template_version_preset_id"`
|
|
}
|
|
|
|
// Cancels all pending provisioner jobs for prebuilt workspaces on a specific preset from an
|
|
// inactive template version.
|
|
// This is an optimization to clean up stale pending jobs.
|
|
func (q *sqlQuerier) UpdatePrebuildProvisionerJobWithCancel(ctx context.Context, arg UpdatePrebuildProvisionerJobWithCancelParams) ([]UpdatePrebuildProvisionerJobWithCancelRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, updatePrebuildProvisionerJobWithCancel, arg.Now, arg.PresetID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []UpdatePrebuildProvisionerJobWithCancelRow
|
|
for rows.Next() {
|
|
var i UpdatePrebuildProvisionerJobWithCancelRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.TemplateID,
|
|
&i.TemplateVersionPresetID,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getActivePresetPrebuildSchedules = `-- name: GetActivePresetPrebuildSchedules :many
|
|
SELECT
|
|
tvpps.id, tvpps.preset_id, tvpps.cron_expression, tvpps.desired_instances
|
|
FROM
|
|
template_version_preset_prebuild_schedules tvpps
|
|
INNER JOIN template_version_presets tvp ON tvp.id = tvpps.preset_id
|
|
INNER JOIN template_versions tv ON tv.id = tvp.template_version_id
|
|
INNER JOIN templates t ON t.id = tv.template_id
|
|
WHERE
|
|
-- Template version is active, and template is not deleted or deprecated
|
|
tv.id = t.active_version_id
|
|
AND NOT t.deleted
|
|
AND t.deprecated = ''
|
|
`
|
|
|
|
func (q *sqlQuerier) GetActivePresetPrebuildSchedules(ctx context.Context) ([]TemplateVersionPresetPrebuildSchedule, error) {
|
|
rows, err := q.db.QueryContext(ctx, getActivePresetPrebuildSchedules)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []TemplateVersionPresetPrebuildSchedule
|
|
for rows.Next() {
|
|
var i TemplateVersionPresetPrebuildSchedule
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.PresetID,
|
|
&i.CronExpression,
|
|
&i.DesiredInstances,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getPresetByID = `-- name: GetPresetByID :one
|
|
SELECT tvp.id, tvp.template_version_id, tvp.name, tvp.created_at, tvp.desired_instances, tvp.invalidate_after_secs, tvp.prebuild_status, tvp.scheduling_timezone, tvp.is_default, tvp.description, tvp.icon, tvp.last_invalidated_at, tv.template_id, tv.organization_id FROM
|
|
template_version_presets tvp
|
|
INNER JOIN template_versions tv ON tvp.template_version_id = tv.id
|
|
WHERE tvp.id = $1
|
|
`
|
|
|
|
type GetPresetByIDRow struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
|
|
Name string `db:"name" json:"name"`
|
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
|
DesiredInstances sql.NullInt32 `db:"desired_instances" json:"desired_instances"`
|
|
InvalidateAfterSecs sql.NullInt32 `db:"invalidate_after_secs" json:"invalidate_after_secs"`
|
|
PrebuildStatus PrebuildStatus `db:"prebuild_status" json:"prebuild_status"`
|
|
SchedulingTimezone string `db:"scheduling_timezone" json:"scheduling_timezone"`
|
|
IsDefault bool `db:"is_default" json:"is_default"`
|
|
Description string `db:"description" json:"description"`
|
|
Icon string `db:"icon" json:"icon"`
|
|
LastInvalidatedAt sql.NullTime `db:"last_invalidated_at" json:"last_invalidated_at"`
|
|
TemplateID uuid.NullUUID `db:"template_id" json:"template_id"`
|
|
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
|
}
|
|
|
|
func (q *sqlQuerier) GetPresetByID(ctx context.Context, presetID uuid.UUID) (GetPresetByIDRow, error) {
|
|
row := q.db.QueryRowContext(ctx, getPresetByID, presetID)
|
|
var i GetPresetByIDRow
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.TemplateVersionID,
|
|
&i.Name,
|
|
&i.CreatedAt,
|
|
&i.DesiredInstances,
|
|
&i.InvalidateAfterSecs,
|
|
&i.PrebuildStatus,
|
|
&i.SchedulingTimezone,
|
|
&i.IsDefault,
|
|
&i.Description,
|
|
&i.Icon,
|
|
&i.LastInvalidatedAt,
|
|
&i.TemplateID,
|
|
&i.OrganizationID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getPresetByWorkspaceBuildID = `-- name: GetPresetByWorkspaceBuildID :one
|
|
SELECT
|
|
template_version_presets.id, template_version_presets.template_version_id, template_version_presets.name, template_version_presets.created_at, template_version_presets.desired_instances, template_version_presets.invalidate_after_secs, template_version_presets.prebuild_status, template_version_presets.scheduling_timezone, template_version_presets.is_default, template_version_presets.description, template_version_presets.icon, template_version_presets.last_invalidated_at
|
|
FROM
|
|
template_version_presets
|
|
INNER JOIN workspace_builds ON workspace_builds.template_version_preset_id = template_version_presets.id
|
|
WHERE
|
|
workspace_builds.id = $1
|
|
`
|
|
|
|
func (q *sqlQuerier) GetPresetByWorkspaceBuildID(ctx context.Context, workspaceBuildID uuid.UUID) (TemplateVersionPreset, error) {
|
|
row := q.db.QueryRowContext(ctx, getPresetByWorkspaceBuildID, workspaceBuildID)
|
|
var i TemplateVersionPreset
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.TemplateVersionID,
|
|
&i.Name,
|
|
&i.CreatedAt,
|
|
&i.DesiredInstances,
|
|
&i.InvalidateAfterSecs,
|
|
&i.PrebuildStatus,
|
|
&i.SchedulingTimezone,
|
|
&i.IsDefault,
|
|
&i.Description,
|
|
&i.Icon,
|
|
&i.LastInvalidatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getPresetParametersByPresetID = `-- name: GetPresetParametersByPresetID :many
|
|
SELECT
|
|
tvpp.id, tvpp.template_version_preset_id, tvpp.name, tvpp.value
|
|
FROM
|
|
template_version_preset_parameters tvpp
|
|
WHERE
|
|
tvpp.template_version_preset_id = $1
|
|
`
|
|
|
|
func (q *sqlQuerier) GetPresetParametersByPresetID(ctx context.Context, presetID uuid.UUID) ([]TemplateVersionPresetParameter, error) {
|
|
rows, err := q.db.QueryContext(ctx, getPresetParametersByPresetID, presetID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []TemplateVersionPresetParameter
|
|
for rows.Next() {
|
|
var i TemplateVersionPresetParameter
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.TemplateVersionPresetID,
|
|
&i.Name,
|
|
&i.Value,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getPresetParametersByTemplateVersionID = `-- name: GetPresetParametersByTemplateVersionID :many
|
|
SELECT
|
|
template_version_preset_parameters.id, template_version_preset_parameters.template_version_preset_id, template_version_preset_parameters.name, template_version_preset_parameters.value
|
|
FROM
|
|
template_version_preset_parameters
|
|
INNER JOIN template_version_presets ON template_version_preset_parameters.template_version_preset_id = template_version_presets.id
|
|
WHERE
|
|
template_version_presets.template_version_id = $1
|
|
`
|
|
|
|
func (q *sqlQuerier) GetPresetParametersByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionPresetParameter, error) {
|
|
rows, err := q.db.QueryContext(ctx, getPresetParametersByTemplateVersionID, templateVersionID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []TemplateVersionPresetParameter
|
|
for rows.Next() {
|
|
var i TemplateVersionPresetParameter
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.TemplateVersionPresetID,
|
|
&i.Name,
|
|
&i.Value,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getPresetsByTemplateVersionID = `-- name: GetPresetsByTemplateVersionID :many
|
|
SELECT
|
|
id, template_version_id, name, created_at, desired_instances, invalidate_after_secs, prebuild_status, scheduling_timezone, is_default, description, icon, last_invalidated_at
|
|
FROM
|
|
template_version_presets
|
|
WHERE
|
|
template_version_id = $1
|
|
`
|
|
|
|
func (q *sqlQuerier) GetPresetsByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionPreset, error) {
|
|
rows, err := q.db.QueryContext(ctx, getPresetsByTemplateVersionID, templateVersionID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []TemplateVersionPreset
|
|
for rows.Next() {
|
|
var i TemplateVersionPreset
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.TemplateVersionID,
|
|
&i.Name,
|
|
&i.CreatedAt,
|
|
&i.DesiredInstances,
|
|
&i.InvalidateAfterSecs,
|
|
&i.PrebuildStatus,
|
|
&i.SchedulingTimezone,
|
|
&i.IsDefault,
|
|
&i.Description,
|
|
&i.Icon,
|
|
&i.LastInvalidatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const insertPreset = `-- name: InsertPreset :one
|
|
INSERT INTO template_version_presets (
|
|
id,
|
|
template_version_id,
|
|
name,
|
|
created_at,
|
|
desired_instances,
|
|
invalidate_after_secs,
|
|
scheduling_timezone,
|
|
is_default,
|
|
description,
|
|
icon,
|
|
last_invalidated_at
|
|
)
|
|
VALUES (
|
|
$1,
|
|
$2,
|
|
$3,
|
|
$4,
|
|
$5,
|
|
$6,
|
|
$7,
|
|
$8,
|
|
$9,
|
|
$10,
|
|
$11
|
|
) RETURNING id, template_version_id, name, created_at, desired_instances, invalidate_after_secs, prebuild_status, scheduling_timezone, is_default, description, icon, last_invalidated_at
|
|
`
|
|
|
|
type InsertPresetParams struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
|
|
Name string `db:"name" json:"name"`
|
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
|
DesiredInstances sql.NullInt32 `db:"desired_instances" json:"desired_instances"`
|
|
InvalidateAfterSecs sql.NullInt32 `db:"invalidate_after_secs" json:"invalidate_after_secs"`
|
|
SchedulingTimezone string `db:"scheduling_timezone" json:"scheduling_timezone"`
|
|
IsDefault bool `db:"is_default" json:"is_default"`
|
|
Description string `db:"description" json:"description"`
|
|
Icon string `db:"icon" json:"icon"`
|
|
LastInvalidatedAt sql.NullTime `db:"last_invalidated_at" json:"last_invalidated_at"`
|
|
}
|
|
|
|
func (q *sqlQuerier) InsertPreset(ctx context.Context, arg InsertPresetParams) (TemplateVersionPreset, error) {
|
|
row := q.db.QueryRowContext(ctx, insertPreset,
|
|
arg.ID,
|
|
arg.TemplateVersionID,
|
|
arg.Name,
|
|
arg.CreatedAt,
|
|
arg.DesiredInstances,
|
|
arg.InvalidateAfterSecs,
|
|
arg.SchedulingTimezone,
|
|
arg.IsDefault,
|
|
arg.Description,
|
|
arg.Icon,
|
|
arg.LastInvalidatedAt,
|
|
)
|
|
var i TemplateVersionPreset
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.TemplateVersionID,
|
|
&i.Name,
|
|
&i.CreatedAt,
|
|
&i.DesiredInstances,
|
|
&i.InvalidateAfterSecs,
|
|
&i.PrebuildStatus,
|
|
&i.SchedulingTimezone,
|
|
&i.IsDefault,
|
|
&i.Description,
|
|
&i.Icon,
|
|
&i.LastInvalidatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const insertPresetParameters = `-- name: InsertPresetParameters :many
|
|
INSERT INTO
|
|
template_version_preset_parameters (template_version_preset_id, name, value)
|
|
SELECT
|
|
$1,
|
|
unnest($2 :: TEXT[]),
|
|
unnest($3 :: TEXT[])
|
|
RETURNING id, template_version_preset_id, name, value
|
|
`
|
|
|
|
type InsertPresetParametersParams struct {
|
|
TemplateVersionPresetID uuid.UUID `db:"template_version_preset_id" json:"template_version_preset_id"`
|
|
Names []string `db:"names" json:"names"`
|
|
Values []string `db:"values" json:"values"`
|
|
}
|
|
|
|
func (q *sqlQuerier) InsertPresetParameters(ctx context.Context, arg InsertPresetParametersParams) ([]TemplateVersionPresetParameter, error) {
|
|
rows, err := q.db.QueryContext(ctx, insertPresetParameters, arg.TemplateVersionPresetID, pq.Array(arg.Names), pq.Array(arg.Values))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []TemplateVersionPresetParameter
|
|
for rows.Next() {
|
|
var i TemplateVersionPresetParameter
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.TemplateVersionPresetID,
|
|
&i.Name,
|
|
&i.Value,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const insertPresetPrebuildSchedule = `-- name: InsertPresetPrebuildSchedule :one
|
|
INSERT INTO template_version_preset_prebuild_schedules (
|
|
preset_id,
|
|
cron_expression,
|
|
desired_instances
|
|
)
|
|
VALUES (
|
|
$1,
|
|
$2,
|
|
$3
|
|
) RETURNING id, preset_id, cron_expression, desired_instances
|
|
`
|
|
|
|
type InsertPresetPrebuildScheduleParams struct {
|
|
PresetID uuid.UUID `db:"preset_id" json:"preset_id"`
|
|
CronExpression string `db:"cron_expression" json:"cron_expression"`
|
|
DesiredInstances int32 `db:"desired_instances" json:"desired_instances"`
|
|
}
|
|
|
|
func (q *sqlQuerier) InsertPresetPrebuildSchedule(ctx context.Context, arg InsertPresetPrebuildScheduleParams) (TemplateVersionPresetPrebuildSchedule, error) {
|
|
row := q.db.QueryRowContext(ctx, insertPresetPrebuildSchedule, arg.PresetID, arg.CronExpression, arg.DesiredInstances)
|
|
var i TemplateVersionPresetPrebuildSchedule
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.PresetID,
|
|
&i.CronExpression,
|
|
&i.DesiredInstances,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const updatePresetPrebuildStatus = `-- name: UpdatePresetPrebuildStatus :exec
|
|
UPDATE template_version_presets
|
|
SET prebuild_status = $1
|
|
WHERE id = $2
|
|
`
|
|
|
|
type UpdatePresetPrebuildStatusParams struct {
|
|
Status PrebuildStatus `db:"status" json:"status"`
|
|
PresetID uuid.UUID `db:"preset_id" json:"preset_id"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdatePresetPrebuildStatus(ctx context.Context, arg UpdatePresetPrebuildStatusParams) error {
|
|
_, err := q.db.ExecContext(ctx, updatePresetPrebuildStatus, arg.Status, arg.PresetID)
|
|
return err
|
|
}
|
|
|
|
const updatePresetsLastInvalidatedAt = `-- name: UpdatePresetsLastInvalidatedAt :many
|
|
UPDATE
|
|
template_version_presets tvp
|
|
SET
|
|
last_invalidated_at = $1
|
|
FROM
|
|
templates t
|
|
JOIN template_versions tv ON tv.id = t.active_version_id
|
|
WHERE
|
|
t.id = $2
|
|
AND tvp.template_version_id = tv.id
|
|
RETURNING
|
|
t.name AS template_name,
|
|
tv.name AS template_version_name,
|
|
tvp.name AS template_version_preset_name
|
|
`
|
|
|
|
type UpdatePresetsLastInvalidatedAtParams struct {
|
|
LastInvalidatedAt sql.NullTime `db:"last_invalidated_at" json:"last_invalidated_at"`
|
|
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
|
|
}
|
|
|
|
type UpdatePresetsLastInvalidatedAtRow struct {
|
|
TemplateName string `db:"template_name" json:"template_name"`
|
|
TemplateVersionName string `db:"template_version_name" json:"template_version_name"`
|
|
TemplateVersionPresetName string `db:"template_version_preset_name" json:"template_version_preset_name"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdatePresetsLastInvalidatedAt(ctx context.Context, arg UpdatePresetsLastInvalidatedAtParams) ([]UpdatePresetsLastInvalidatedAtRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, updatePresetsLastInvalidatedAt, arg.LastInvalidatedAt, arg.TemplateID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []UpdatePresetsLastInvalidatedAtRow
|
|
for rows.Next() {
|
|
var i UpdatePresetsLastInvalidatedAtRow
|
|
if err := rows.Scan(&i.TemplateName, &i.TemplateVersionName, &i.TemplateVersionPresetName); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const deleteOldProvisionerDaemons = `-- name: DeleteOldProvisionerDaemons :exec
|
|
DELETE FROM provisioner_daemons WHERE (
|
|
(created_at < (NOW() - INTERVAL '7 days') AND last_seen_at IS NULL) OR
|
|
(last_seen_at IS NOT NULL AND last_seen_at < (NOW() - INTERVAL '7 days'))
|
|
)
|
|
`
|
|
|
|
// Delete provisioner daemons that have been created at least a week ago
|
|
// and have not connected to coderd since a week.
|
|
// A provisioner daemon with "zeroed" last_seen_at column indicates possible
|
|
// connectivity issues (no provisioner daemon activity since registration).
|
|
func (q *sqlQuerier) DeleteOldProvisionerDaemons(ctx context.Context) error {
|
|
_, err := q.db.ExecContext(ctx, deleteOldProvisionerDaemons)
|
|
return err
|
|
}
|
|
|
|
const getEligibleProvisionerDaemonsByProvisionerJobIDs = `-- name: GetEligibleProvisionerDaemonsByProvisionerJobIDs :many
|
|
SELECT DISTINCT
|
|
provisioner_jobs.id as job_id, provisioner_daemons.id, provisioner_daemons.created_at, provisioner_daemons.name, provisioner_daemons.provisioners, provisioner_daemons.replica_id, provisioner_daemons.tags, provisioner_daemons.last_seen_at, provisioner_daemons.version, provisioner_daemons.api_version, provisioner_daemons.organization_id, provisioner_daemons.key_id
|
|
FROM
|
|
provisioner_jobs
|
|
JOIN
|
|
provisioner_daemons ON provisioner_daemons.organization_id = provisioner_jobs.organization_id
|
|
AND provisioner_tagset_contains(provisioner_daemons.tags::tagset, provisioner_jobs.tags::tagset)
|
|
AND provisioner_jobs.provisioner = ANY(provisioner_daemons.provisioners)
|
|
WHERE
|
|
provisioner_jobs.id = ANY($1 :: uuid[])
|
|
`
|
|
|
|
type GetEligibleProvisionerDaemonsByProvisionerJobIDsRow struct {
|
|
JobID uuid.UUID `db:"job_id" json:"job_id"`
|
|
ProvisionerDaemon ProvisionerDaemon `db:"provisioner_daemon" json:"provisioner_daemon"`
|
|
}
|
|
|
|
func (q *sqlQuerier) GetEligibleProvisionerDaemonsByProvisionerJobIDs(ctx context.Context, provisionerJobIds []uuid.UUID) ([]GetEligibleProvisionerDaemonsByProvisionerJobIDsRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, getEligibleProvisionerDaemonsByProvisionerJobIDs, pq.Array(provisionerJobIds))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []GetEligibleProvisionerDaemonsByProvisionerJobIDsRow
|
|
for rows.Next() {
|
|
var i GetEligibleProvisionerDaemonsByProvisionerJobIDsRow
|
|
if err := rows.Scan(
|
|
&i.JobID,
|
|
&i.ProvisionerDaemon.ID,
|
|
&i.ProvisionerDaemon.CreatedAt,
|
|
&i.ProvisionerDaemon.Name,
|
|
pq.Array(&i.ProvisionerDaemon.Provisioners),
|
|
&i.ProvisionerDaemon.ReplicaID,
|
|
&i.ProvisionerDaemon.Tags,
|
|
&i.ProvisionerDaemon.LastSeenAt,
|
|
&i.ProvisionerDaemon.Version,
|
|
&i.ProvisionerDaemon.APIVersion,
|
|
&i.ProvisionerDaemon.OrganizationID,
|
|
&i.ProvisionerDaemon.KeyID,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getProvisionerDaemons = `-- name: GetProvisionerDaemons :many
|
|
SELECT
|
|
id, created_at, name, provisioners, replica_id, tags, last_seen_at, version, api_version, organization_id, key_id
|
|
FROM
|
|
provisioner_daemons
|
|
`
|
|
|
|
func (q *sqlQuerier) GetProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error) {
|
|
rows, err := q.db.QueryContext(ctx, getProvisionerDaemons)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ProvisionerDaemon
|
|
for rows.Next() {
|
|
var i ProvisionerDaemon
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.CreatedAt,
|
|
&i.Name,
|
|
pq.Array(&i.Provisioners),
|
|
&i.ReplicaID,
|
|
&i.Tags,
|
|
&i.LastSeenAt,
|
|
&i.Version,
|
|
&i.APIVersion,
|
|
&i.OrganizationID,
|
|
&i.KeyID,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getProvisionerDaemonsByOrganization = `-- name: GetProvisionerDaemonsByOrganization :many
|
|
SELECT
|
|
id, created_at, name, provisioners, replica_id, tags, last_seen_at, version, api_version, organization_id, key_id
|
|
FROM
|
|
provisioner_daemons
|
|
WHERE
|
|
-- This is the original search criteria:
|
|
organization_id = $1 :: uuid
|
|
AND
|
|
-- adding support for searching by tags:
|
|
($2 :: tagset = 'null' :: tagset OR provisioner_tagset_contains(provisioner_daemons.tags::tagset, $2::tagset))
|
|
`
|
|
|
|
type GetProvisionerDaemonsByOrganizationParams struct {
|
|
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
|
WantTags StringMap `db:"want_tags" json:"want_tags"`
|
|
}
|
|
|
|
func (q *sqlQuerier) GetProvisionerDaemonsByOrganization(ctx context.Context, arg GetProvisionerDaemonsByOrganizationParams) ([]ProvisionerDaemon, error) {
|
|
rows, err := q.db.QueryContext(ctx, getProvisionerDaemonsByOrganization, arg.OrganizationID, arg.WantTags)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ProvisionerDaemon
|
|
for rows.Next() {
|
|
var i ProvisionerDaemon
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.CreatedAt,
|
|
&i.Name,
|
|
pq.Array(&i.Provisioners),
|
|
&i.ReplicaID,
|
|
&i.Tags,
|
|
&i.LastSeenAt,
|
|
&i.Version,
|
|
&i.APIVersion,
|
|
&i.OrganizationID,
|
|
&i.KeyID,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getProvisionerDaemonsWithStatusByOrganization = `-- name: GetProvisionerDaemonsWithStatusByOrganization :many
|
|
SELECT
|
|
pd.id, pd.created_at, pd.name, pd.provisioners, pd.replica_id, pd.tags, pd.last_seen_at, pd.version, pd.api_version, pd.organization_id, pd.key_id,
|
|
CASE
|
|
WHEN current_job.id IS NOT NULL THEN 'busy'::provisioner_daemon_status
|
|
WHEN (COALESCE($1::bool, false) = true
|
|
OR 'offline'::provisioner_daemon_status = ANY($2::provisioner_daemon_status[]))
|
|
AND (pd.last_seen_at IS NULL OR pd.last_seen_at < (NOW() - ($3::bigint || ' ms')::interval))
|
|
THEN 'offline'::provisioner_daemon_status
|
|
ELSE 'idle'::provisioner_daemon_status
|
|
END AS status,
|
|
pk.name AS key_name,
|
|
-- NOTE(mafredri): sqlc.embed doesn't support nullable tables nor renaming them.
|
|
current_job.id AS current_job_id,
|
|
current_job.job_status AS current_job_status,
|
|
previous_job.id AS previous_job_id,
|
|
previous_job.job_status AS previous_job_status,
|
|
COALESCE(current_template.name, ''::text) AS current_job_template_name,
|
|
COALESCE(current_template.display_name, ''::text) AS current_job_template_display_name,
|
|
COALESCE(current_template.icon, ''::text) AS current_job_template_icon,
|
|
COALESCE(previous_template.name, ''::text) AS previous_job_template_name,
|
|
COALESCE(previous_template.display_name, ''::text) AS previous_job_template_display_name,
|
|
COALESCE(previous_template.icon, ''::text) AS previous_job_template_icon
|
|
FROM
|
|
provisioner_daemons pd
|
|
JOIN
|
|
provisioner_keys pk ON pk.id = pd.key_id
|
|
LEFT JOIN
|
|
provisioner_jobs current_job ON (
|
|
current_job.worker_id = pd.id
|
|
AND current_job.organization_id = pd.organization_id
|
|
AND current_job.completed_at IS NULL
|
|
)
|
|
LEFT JOIN
|
|
provisioner_jobs previous_job ON (
|
|
previous_job.id = (
|
|
SELECT
|
|
id
|
|
FROM
|
|
provisioner_jobs
|
|
WHERE
|
|
worker_id = pd.id
|
|
AND organization_id = pd.organization_id
|
|
AND completed_at IS NOT NULL
|
|
ORDER BY
|
|
completed_at DESC
|
|
LIMIT 1
|
|
)
|
|
AND previous_job.organization_id = pd.organization_id
|
|
)
|
|
LEFT JOIN
|
|
workspace_builds current_build ON current_build.id = CASE WHEN current_job.input ? 'workspace_build_id' THEN (current_job.input->>'workspace_build_id')::uuid END
|
|
LEFT JOIN
|
|
-- We should always have a template version, either explicitly or implicitly via workspace build.
|
|
template_versions current_version ON (
|
|
current_version.id = CASE WHEN current_job.input ? 'template_version_id' THEN (current_job.input->>'template_version_id')::uuid ELSE current_build.template_version_id END
|
|
AND current_version.organization_id = pd.organization_id
|
|
)
|
|
LEFT JOIN
|
|
templates current_template ON (
|
|
current_template.id = current_version.template_id
|
|
AND current_template.organization_id = pd.organization_id
|
|
)
|
|
LEFT JOIN
|
|
workspace_builds previous_build ON previous_build.id = CASE WHEN previous_job.input ? 'workspace_build_id' THEN (previous_job.input->>'workspace_build_id')::uuid END
|
|
LEFT JOIN
|
|
-- We should always have a template version, either explicitly or implicitly via workspace build.
|
|
template_versions previous_version ON (
|
|
previous_version.id = CASE WHEN previous_job.input ? 'template_version_id' THEN (previous_job.input->>'template_version_id')::uuid ELSE previous_build.template_version_id END
|
|
AND previous_version.organization_id = pd.organization_id
|
|
)
|
|
LEFT JOIN
|
|
templates previous_template ON (
|
|
previous_template.id = previous_version.template_id
|
|
AND previous_template.organization_id = pd.organization_id
|
|
)
|
|
WHERE
|
|
pd.organization_id = $4::uuid
|
|
AND (COALESCE(array_length($5::uuid[], 1), 0) = 0 OR pd.id = ANY($5::uuid[]))
|
|
AND ($6::tagset = 'null'::tagset OR provisioner_tagset_contains(pd.tags::tagset, $6::tagset))
|
|
-- Filter by max age if provided
|
|
AND (
|
|
$7::bigint IS NULL
|
|
OR pd.last_seen_at IS NULL
|
|
OR pd.last_seen_at >= (NOW() - ($7::bigint || ' ms')::interval)
|
|
)
|
|
AND (
|
|
-- Always include online daemons
|
|
(pd.last_seen_at IS NOT NULL AND pd.last_seen_at >= (NOW() - ($3::bigint || ' ms')::interval))
|
|
-- Include offline daemons if offline param is true or 'offline' status is requested
|
|
OR (
|
|
(pd.last_seen_at IS NULL OR pd.last_seen_at < (NOW() - ($3::bigint || ' ms')::interval))
|
|
AND (
|
|
COALESCE($1::bool, false) = true
|
|
OR 'offline'::provisioner_daemon_status = ANY($2::provisioner_daemon_status[])
|
|
)
|
|
)
|
|
)
|
|
AND (
|
|
-- Filter daemons by any statuses if provided
|
|
COALESCE(array_length($2::provisioner_daemon_status[], 1), 0) = 0
|
|
OR (current_job.id IS NOT NULL AND 'busy'::provisioner_daemon_status = ANY($2::provisioner_daemon_status[]))
|
|
OR (current_job.id IS NULL AND 'idle'::provisioner_daemon_status = ANY($2::provisioner_daemon_status[]))
|
|
OR (
|
|
'offline'::provisioner_daemon_status = ANY($2::provisioner_daemon_status[])
|
|
AND (pd.last_seen_at IS NULL OR pd.last_seen_at < (NOW() - ($3::bigint || ' ms')::interval))
|
|
)
|
|
OR (
|
|
COALESCE($1::bool, false) = true
|
|
AND (pd.last_seen_at IS NULL OR pd.last_seen_at < (NOW() - ($3::bigint || ' ms')::interval))
|
|
)
|
|
)
|
|
ORDER BY
|
|
pd.created_at DESC
|
|
LIMIT
|
|
$8::int
|
|
`
|
|
|
|
type GetProvisionerDaemonsWithStatusByOrganizationParams struct {
|
|
Offline sql.NullBool `db:"offline" json:"offline"`
|
|
Statuses []ProvisionerDaemonStatus `db:"statuses" json:"statuses"`
|
|
StaleIntervalMS int64 `db:"stale_interval_ms" json:"stale_interval_ms"`
|
|
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
|
IDs []uuid.UUID `db:"ids" json:"ids"`
|
|
Tags StringMap `db:"tags" json:"tags"`
|
|
MaxAgeMs sql.NullInt64 `db:"max_age_ms" json:"max_age_ms"`
|
|
Limit sql.NullInt32 `db:"limit" json:"limit"`
|
|
}
|
|
|
|
type GetProvisionerDaemonsWithStatusByOrganizationRow struct {
|
|
ProvisionerDaemon ProvisionerDaemon `db:"provisioner_daemon" json:"provisioner_daemon"`
|
|
Status ProvisionerDaemonStatus `db:"status" json:"status"`
|
|
KeyName string `db:"key_name" json:"key_name"`
|
|
CurrentJobID uuid.NullUUID `db:"current_job_id" json:"current_job_id"`
|
|
CurrentJobStatus NullProvisionerJobStatus `db:"current_job_status" json:"current_job_status"`
|
|
PreviousJobID uuid.NullUUID `db:"previous_job_id" json:"previous_job_id"`
|
|
PreviousJobStatus NullProvisionerJobStatus `db:"previous_job_status" json:"previous_job_status"`
|
|
CurrentJobTemplateName string `db:"current_job_template_name" json:"current_job_template_name"`
|
|
CurrentJobTemplateDisplayName string `db:"current_job_template_display_name" json:"current_job_template_display_name"`
|
|
CurrentJobTemplateIcon string `db:"current_job_template_icon" json:"current_job_template_icon"`
|
|
PreviousJobTemplateName string `db:"previous_job_template_name" json:"previous_job_template_name"`
|
|
PreviousJobTemplateDisplayName string `db:"previous_job_template_display_name" json:"previous_job_template_display_name"`
|
|
PreviousJobTemplateIcon string `db:"previous_job_template_icon" json:"previous_job_template_icon"`
|
|
}
|
|
|
|
// Current job information.
|
|
// Previous job information.
|
|
func (q *sqlQuerier) GetProvisionerDaemonsWithStatusByOrganization(ctx context.Context, arg GetProvisionerDaemonsWithStatusByOrganizationParams) ([]GetProvisionerDaemonsWithStatusByOrganizationRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, getProvisionerDaemonsWithStatusByOrganization,
|
|
arg.Offline,
|
|
pq.Array(arg.Statuses),
|
|
arg.StaleIntervalMS,
|
|
arg.OrganizationID,
|
|
pq.Array(arg.IDs),
|
|
arg.Tags,
|
|
arg.MaxAgeMs,
|
|
arg.Limit,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []GetProvisionerDaemonsWithStatusByOrganizationRow
|
|
for rows.Next() {
|
|
var i GetProvisionerDaemonsWithStatusByOrganizationRow
|
|
if err := rows.Scan(
|
|
&i.ProvisionerDaemon.ID,
|
|
&i.ProvisionerDaemon.CreatedAt,
|
|
&i.ProvisionerDaemon.Name,
|
|
pq.Array(&i.ProvisionerDaemon.Provisioners),
|
|
&i.ProvisionerDaemon.ReplicaID,
|
|
&i.ProvisionerDaemon.Tags,
|
|
&i.ProvisionerDaemon.LastSeenAt,
|
|
&i.ProvisionerDaemon.Version,
|
|
&i.ProvisionerDaemon.APIVersion,
|
|
&i.ProvisionerDaemon.OrganizationID,
|
|
&i.ProvisionerDaemon.KeyID,
|
|
&i.Status,
|
|
&i.KeyName,
|
|
&i.CurrentJobID,
|
|
&i.CurrentJobStatus,
|
|
&i.PreviousJobID,
|
|
&i.PreviousJobStatus,
|
|
&i.CurrentJobTemplateName,
|
|
&i.CurrentJobTemplateDisplayName,
|
|
&i.CurrentJobTemplateIcon,
|
|
&i.PreviousJobTemplateName,
|
|
&i.PreviousJobTemplateDisplayName,
|
|
&i.PreviousJobTemplateIcon,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const updateProvisionerDaemonLastSeenAt = `-- name: UpdateProvisionerDaemonLastSeenAt :exec
|
|
UPDATE provisioner_daemons
|
|
SET
|
|
last_seen_at = $1
|
|
WHERE
|
|
id = $2
|
|
AND
|
|
last_seen_at <= $1
|
|
`
|
|
|
|
type UpdateProvisionerDaemonLastSeenAtParams struct {
|
|
LastSeenAt sql.NullTime `db:"last_seen_at" json:"last_seen_at"`
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg UpdateProvisionerDaemonLastSeenAtParams) error {
|
|
_, err := q.db.ExecContext(ctx, updateProvisionerDaemonLastSeenAt, arg.LastSeenAt, arg.ID)
|
|
return err
|
|
}
|
|
|
|
const upsertProvisionerDaemon = `-- name: UpsertProvisionerDaemon :one
|
|
INSERT INTO
|
|
provisioner_daemons (
|
|
id,
|
|
created_at,
|
|
"name",
|
|
provisioners,
|
|
tags,
|
|
last_seen_at,
|
|
"version",
|
|
organization_id,
|
|
api_version,
|
|
key_id
|
|
)
|
|
VALUES (
|
|
gen_random_uuid(),
|
|
$1,
|
|
$2,
|
|
$3,
|
|
$4,
|
|
$5,
|
|
$6,
|
|
$7,
|
|
$8,
|
|
$9
|
|
) ON CONFLICT("organization_id", "name", LOWER(COALESCE(tags ->> 'owner'::text, ''::text))) DO UPDATE SET
|
|
provisioners = $3,
|
|
tags = $4,
|
|
last_seen_at = $5,
|
|
"version" = $6,
|
|
api_version = $8,
|
|
organization_id = $7,
|
|
key_id = $9
|
|
RETURNING id, created_at, name, provisioners, replica_id, tags, last_seen_at, version, api_version, organization_id, key_id
|
|
`
|
|
|
|
type UpsertProvisionerDaemonParams struct {
|
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
|
Name string `db:"name" json:"name"`
|
|
Provisioners []ProvisionerType `db:"provisioners" json:"provisioners"`
|
|
Tags StringMap `db:"tags" json:"tags"`
|
|
LastSeenAt sql.NullTime `db:"last_seen_at" json:"last_seen_at"`
|
|
Version string `db:"version" json:"version"`
|
|
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
|
APIVersion string `db:"api_version" json:"api_version"`
|
|
KeyID uuid.UUID `db:"key_id" json:"key_id"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpsertProvisionerDaemon(ctx context.Context, arg UpsertProvisionerDaemonParams) (ProvisionerDaemon, error) {
|
|
row := q.db.QueryRowContext(ctx, upsertProvisionerDaemon,
|
|
arg.CreatedAt,
|
|
arg.Name,
|
|
pq.Array(arg.Provisioners),
|
|
arg.Tags,
|
|
arg.LastSeenAt,
|
|
arg.Version,
|
|
arg.OrganizationID,
|
|
arg.APIVersion,
|
|
arg.KeyID,
|
|
)
|
|
var i ProvisionerDaemon
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.CreatedAt,
|
|
&i.Name,
|
|
pq.Array(&i.Provisioners),
|
|
&i.ReplicaID,
|
|
&i.Tags,
|
|
&i.LastSeenAt,
|
|
&i.Version,
|
|
&i.APIVersion,
|
|
&i.OrganizationID,
|
|
&i.KeyID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getProvisionerLogsAfterID = `-- name: GetProvisionerLogsAfterID :many
|
|
SELECT
|
|
job_id, created_at, source, level, stage, output, id
|
|
FROM
|
|
provisioner_job_logs
|
|
WHERE
|
|
job_id = $1
|
|
AND (
|
|
id > $2
|
|
) ORDER BY id ASC
|
|
`
|
|
|
|
type GetProvisionerLogsAfterIDParams struct {
|
|
JobID uuid.UUID `db:"job_id" json:"job_id"`
|
|
CreatedAfter int64 `db:"created_after" json:"created_after"`
|
|
}
|
|
|
|
func (q *sqlQuerier) GetProvisionerLogsAfterID(ctx context.Context, arg GetProvisionerLogsAfterIDParams) ([]ProvisionerJobLog, error) {
|
|
rows, err := q.db.QueryContext(ctx, getProvisionerLogsAfterID, arg.JobID, arg.CreatedAfter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ProvisionerJobLog
|
|
for rows.Next() {
|
|
var i ProvisionerJobLog
|
|
if err := rows.Scan(
|
|
&i.JobID,
|
|
&i.CreatedAt,
|
|
&i.Source,
|
|
&i.Level,
|
|
&i.Stage,
|
|
&i.Output,
|
|
&i.ID,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const insertProvisionerJobLogs = `-- name: InsertProvisionerJobLogs :many
|
|
INSERT INTO
|
|
provisioner_job_logs
|
|
SELECT
|
|
$1 :: uuid AS job_id,
|
|
unnest($2 :: timestamptz [ ]) AS created_at,
|
|
unnest($3 :: log_source [ ]) AS source,
|
|
unnest($4 :: log_level [ ]) AS LEVEL,
|
|
unnest($5 :: VARCHAR(128) [ ]) AS stage,
|
|
unnest($6 :: VARCHAR(1024) [ ]) AS output RETURNING job_id, created_at, source, level, stage, output, id
|
|
`
|
|
|
|
type InsertProvisionerJobLogsParams struct {
|
|
JobID uuid.UUID `db:"job_id" json:"job_id"`
|
|
CreatedAt []time.Time `db:"created_at" json:"created_at"`
|
|
Source []LogSource `db:"source" json:"source"`
|
|
Level []LogLevel `db:"level" json:"level"`
|
|
Stage []string `db:"stage" json:"stage"`
|
|
Output []string `db:"output" json:"output"`
|
|
}
|
|
|
|
func (q *sqlQuerier) InsertProvisionerJobLogs(ctx context.Context, arg InsertProvisionerJobLogsParams) ([]ProvisionerJobLog, error) {
|
|
rows, err := q.db.QueryContext(ctx, insertProvisionerJobLogs,
|
|
arg.JobID,
|
|
pq.Array(arg.CreatedAt),
|
|
pq.Array(arg.Source),
|
|
pq.Array(arg.Level),
|
|
pq.Array(arg.Stage),
|
|
pq.Array(arg.Output),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ProvisionerJobLog
|
|
for rows.Next() {
|
|
var i ProvisionerJobLog
|
|
if err := rows.Scan(
|
|
&i.JobID,
|
|
&i.CreatedAt,
|
|
&i.Source,
|
|
&i.Level,
|
|
&i.Stage,
|
|
&i.Output,
|
|
&i.ID,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const updateProvisionerJobLogsLength = `-- name: UpdateProvisionerJobLogsLength :exec
|
|
UPDATE
|
|
provisioner_jobs
|
|
SET
|
|
logs_length = logs_length + $2
|
|
WHERE
|
|
id = $1
|
|
`
|
|
|
|
type UpdateProvisionerJobLogsLengthParams struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
LogsLength int32 `db:"logs_length" json:"logs_length"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateProvisionerJobLogsLength(ctx context.Context, arg UpdateProvisionerJobLogsLengthParams) error {
|
|
_, err := q.db.ExecContext(ctx, updateProvisionerJobLogsLength, arg.ID, arg.LogsLength)
|
|
return err
|
|
}
|
|
|
|
const updateProvisionerJobLogsOverflowed = `-- name: UpdateProvisionerJobLogsOverflowed :exec
|
|
UPDATE
|
|
provisioner_jobs
|
|
SET
|
|
logs_overflowed = $2
|
|
WHERE
|
|
id = $1
|
|
`
|
|
|
|
type UpdateProvisionerJobLogsOverflowedParams struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
LogsOverflowed bool `db:"logs_overflowed" json:"logs_overflowed"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateProvisionerJobLogsOverflowed(ctx context.Context, arg UpdateProvisionerJobLogsOverflowedParams) error {
|
|
_, err := q.db.ExecContext(ctx, updateProvisionerJobLogsOverflowed, arg.ID, arg.LogsOverflowed)
|
|
return err
|
|
}
|
|
|
|
const acquireProvisionerJob = `-- name: AcquireProvisionerJob :one
|
|
UPDATE
|
|
provisioner_jobs
|
|
SET
|
|
started_at = $1,
|
|
updated_at = $1,
|
|
worker_id = $2
|
|
WHERE
|
|
id = (
|
|
SELECT
|
|
id
|
|
FROM
|
|
provisioner_jobs AS potential_job
|
|
WHERE
|
|
potential_job.started_at IS NULL
|
|
AND potential_job.completed_at IS NULL
|
|
AND potential_job.organization_id = $3
|
|
-- Ensure the caller has the correct provisioner.
|
|
AND potential_job.provisioner = ANY($4 :: provisioner_type [ ])
|
|
-- elsewhere, we use the tagset type, but here we use jsonb for backward compatibility
|
|
-- they are aliases and the code that calls this query already relies on a different type
|
|
AND provisioner_tagset_contains($5 :: jsonb, potential_job.tags :: jsonb)
|
|
ORDER BY
|
|
-- Ensure that human-initiated jobs are prioritized over prebuilds.
|
|
potential_job.initiator_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid ASC,
|
|
potential_job.created_at ASC
|
|
FOR UPDATE
|
|
SKIP LOCKED
|
|
LIMIT
|
|
1
|
|
) RETURNING id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status, logs_length, logs_overflowed
|
|
`
|
|
|
|
type AcquireProvisionerJobParams struct {
|
|
StartedAt sql.NullTime `db:"started_at" json:"started_at"`
|
|
WorkerID uuid.NullUUID `db:"worker_id" json:"worker_id"`
|
|
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
|
Types []ProvisionerType `db:"types" json:"types"`
|
|
ProvisionerTags json.RawMessage `db:"provisioner_tags" json:"provisioner_tags"`
|
|
}
|
|
|
|
// Acquires the lock for a single job that isn't started, completed,
|
|
// canceled, and that matches an array of provisioner types.
|
|
//
|
|
// SKIP LOCKED is used to jump over locked rows. This prevents
|
|
// multiple provisioners from acquiring the same jobs. See:
|
|
// https://www.postgresql.org/docs/9.5/sql-select.html#SQL-FOR-UPDATE-SHARE
|
|
func (q *sqlQuerier) AcquireProvisionerJob(ctx context.Context, arg AcquireProvisionerJobParams) (ProvisionerJob, error) {
|
|
row := q.db.QueryRowContext(ctx, acquireProvisionerJob,
|
|
arg.StartedAt,
|
|
arg.WorkerID,
|
|
arg.OrganizationID,
|
|
pq.Array(arg.Types),
|
|
arg.ProvisionerTags,
|
|
)
|
|
var i ProvisionerJob
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.StartedAt,
|
|
&i.CanceledAt,
|
|
&i.CompletedAt,
|
|
&i.Error,
|
|
&i.OrganizationID,
|
|
&i.InitiatorID,
|
|
&i.Provisioner,
|
|
&i.StorageMethod,
|
|
&i.Type,
|
|
&i.Input,
|
|
&i.WorkerID,
|
|
&i.FileID,
|
|
&i.Tags,
|
|
&i.ErrorCode,
|
|
&i.TraceMetadata,
|
|
&i.JobStatus,
|
|
&i.LogsLength,
|
|
&i.LogsOverflowed,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getProvisionerJobByID = `-- name: GetProvisionerJobByID :one
|
|
SELECT
|
|
id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status, logs_length, logs_overflowed
|
|
FROM
|
|
provisioner_jobs
|
|
WHERE
|
|
id = $1
|
|
`
|
|
|
|
func (q *sqlQuerier) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (ProvisionerJob, error) {
|
|
row := q.db.QueryRowContext(ctx, getProvisionerJobByID, id)
|
|
var i ProvisionerJob
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.StartedAt,
|
|
&i.CanceledAt,
|
|
&i.CompletedAt,
|
|
&i.Error,
|
|
&i.OrganizationID,
|
|
&i.InitiatorID,
|
|
&i.Provisioner,
|
|
&i.StorageMethod,
|
|
&i.Type,
|
|
&i.Input,
|
|
&i.WorkerID,
|
|
&i.FileID,
|
|
&i.Tags,
|
|
&i.ErrorCode,
|
|
&i.TraceMetadata,
|
|
&i.JobStatus,
|
|
&i.LogsLength,
|
|
&i.LogsOverflowed,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getProvisionerJobByIDForUpdate = `-- name: GetProvisionerJobByIDForUpdate :one
|
|
SELECT
|
|
id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status, logs_length, logs_overflowed
|
|
FROM
|
|
provisioner_jobs
|
|
WHERE
|
|
id = $1
|
|
FOR UPDATE
|
|
SKIP LOCKED
|
|
`
|
|
|
|
// Gets a single provisioner job by ID for update.
|
|
// This is used to securely reap jobs that have been hung/pending for a long time.
|
|
func (q *sqlQuerier) GetProvisionerJobByIDForUpdate(ctx context.Context, id uuid.UUID) (ProvisionerJob, error) {
|
|
row := q.db.QueryRowContext(ctx, getProvisionerJobByIDForUpdate, id)
|
|
var i ProvisionerJob
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.StartedAt,
|
|
&i.CanceledAt,
|
|
&i.CompletedAt,
|
|
&i.Error,
|
|
&i.OrganizationID,
|
|
&i.InitiatorID,
|
|
&i.Provisioner,
|
|
&i.StorageMethod,
|
|
&i.Type,
|
|
&i.Input,
|
|
&i.WorkerID,
|
|
&i.FileID,
|
|
&i.Tags,
|
|
&i.ErrorCode,
|
|
&i.TraceMetadata,
|
|
&i.JobStatus,
|
|
&i.LogsLength,
|
|
&i.LogsOverflowed,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getProvisionerJobByIDWithLock = `-- name: GetProvisionerJobByIDWithLock :one
|
|
SELECT
|
|
id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status, logs_length, logs_overflowed
|
|
FROM
|
|
provisioner_jobs
|
|
WHERE
|
|
id = $1
|
|
FOR UPDATE
|
|
`
|
|
|
|
// Gets a provisioner job by ID with exclusive lock.
|
|
// Blocks until the row is available for update.
|
|
func (q *sqlQuerier) GetProvisionerJobByIDWithLock(ctx context.Context, id uuid.UUID) (ProvisionerJob, error) {
|
|
row := q.db.QueryRowContext(ctx, getProvisionerJobByIDWithLock, id)
|
|
var i ProvisionerJob
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.StartedAt,
|
|
&i.CanceledAt,
|
|
&i.CompletedAt,
|
|
&i.Error,
|
|
&i.OrganizationID,
|
|
&i.InitiatorID,
|
|
&i.Provisioner,
|
|
&i.StorageMethod,
|
|
&i.Type,
|
|
&i.Input,
|
|
&i.WorkerID,
|
|
&i.FileID,
|
|
&i.Tags,
|
|
&i.ErrorCode,
|
|
&i.TraceMetadata,
|
|
&i.JobStatus,
|
|
&i.LogsLength,
|
|
&i.LogsOverflowed,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getProvisionerJobTimingsByJobID = `-- name: GetProvisionerJobTimingsByJobID :many
|
|
SELECT job_id, started_at, ended_at, stage, source, action, resource FROM provisioner_job_timings
|
|
WHERE job_id = $1
|
|
ORDER BY started_at ASC
|
|
`
|
|
|
|
func (q *sqlQuerier) GetProvisionerJobTimingsByJobID(ctx context.Context, jobID uuid.UUID) ([]ProvisionerJobTiming, error) {
|
|
rows, err := q.db.QueryContext(ctx, getProvisionerJobTimingsByJobID, jobID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ProvisionerJobTiming
|
|
for rows.Next() {
|
|
var i ProvisionerJobTiming
|
|
if err := rows.Scan(
|
|
&i.JobID,
|
|
&i.StartedAt,
|
|
&i.EndedAt,
|
|
&i.Stage,
|
|
&i.Source,
|
|
&i.Action,
|
|
&i.Resource,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getProvisionerJobsByIDsWithQueuePosition = `-- name: GetProvisionerJobsByIDsWithQueuePosition :many
|
|
WITH filtered_provisioner_jobs AS (
|
|
-- Step 1: Filter provisioner_jobs
|
|
SELECT
|
|
id, created_at, tags
|
|
FROM
|
|
provisioner_jobs
|
|
WHERE
|
|
id = ANY($1 :: uuid [ ]) -- Apply filter early to reduce dataset size before expensive JOIN
|
|
),
|
|
pending_jobs AS (
|
|
-- Step 2: Extract only pending jobs
|
|
SELECT
|
|
id, initiator_id, created_at, tags
|
|
FROM
|
|
provisioner_jobs
|
|
WHERE
|
|
job_status = 'pending'
|
|
),
|
|
unique_daemon_tags AS (
|
|
SELECT DISTINCT tags FROM provisioner_daemons pd
|
|
WHERE pd.last_seen_at IS NOT NULL
|
|
AND pd.last_seen_at >= (NOW() - ($2::bigint || ' ms')::interval)
|
|
),
|
|
relevant_daemon_tags AS (
|
|
SELECT udt.tags
|
|
FROM unique_daemon_tags udt
|
|
WHERE EXISTS (
|
|
SELECT 1 FROM filtered_provisioner_jobs fpj
|
|
WHERE provisioner_tagset_contains(udt.tags, fpj.tags)
|
|
)
|
|
),
|
|
ranked_jobs AS (
|
|
-- Step 3: Rank only pending jobs based on provisioner availability
|
|
SELECT
|
|
pj.id,
|
|
pj.created_at,
|
|
ROW_NUMBER() OVER (PARTITION BY rdt.tags ORDER BY pj.initiator_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid ASC, pj.created_at ASC) AS queue_position,
|
|
COUNT(*) OVER (PARTITION BY rdt.tags) AS queue_size
|
|
FROM
|
|
pending_jobs pj
|
|
INNER JOIN
|
|
relevant_daemon_tags rdt
|
|
ON
|
|
provisioner_tagset_contains(rdt.tags, pj.tags)
|
|
),
|
|
final_jobs AS (
|
|
-- Step 4: Compute best queue position and max queue size per job
|
|
SELECT
|
|
fpj.id,
|
|
fpj.created_at,
|
|
COALESCE(MIN(rj.queue_position), 0) :: BIGINT AS queue_position, -- Best queue position across provisioners
|
|
COALESCE(MAX(rj.queue_size), 0) :: BIGINT AS queue_size -- Max queue size across provisioners
|
|
FROM
|
|
filtered_provisioner_jobs fpj -- Use the pre-filtered dataset instead of full provisioner_jobs
|
|
LEFT JOIN ranked_jobs rj
|
|
ON fpj.id = rj.id -- Join with the ranking jobs CTE to assign a rank to each specified provisioner job.
|
|
GROUP BY
|
|
fpj.id, fpj.created_at
|
|
)
|
|
SELECT
|
|
-- Step 5: Final SELECT with INNER JOIN provisioner_jobs
|
|
fj.id,
|
|
fj.created_at,
|
|
pj.id, pj.created_at, pj.updated_at, pj.started_at, pj.canceled_at, pj.completed_at, pj.error, pj.organization_id, pj.initiator_id, pj.provisioner, pj.storage_method, pj.type, pj.input, pj.worker_id, pj.file_id, pj.tags, pj.error_code, pj.trace_metadata, pj.job_status, pj.logs_length, pj.logs_overflowed,
|
|
fj.queue_position,
|
|
fj.queue_size
|
|
FROM
|
|
final_jobs fj
|
|
INNER JOIN provisioner_jobs pj
|
|
ON fj.id = pj.id -- Ensure we retrieve full details from ` + "`" + `provisioner_jobs` + "`" + `.
|
|
-- JOIN with pj is required for sqlc.embed(pj) to compile successfully.
|
|
ORDER BY
|
|
fj.created_at
|
|
`
|
|
|
|
type GetProvisionerJobsByIDsWithQueuePositionParams struct {
|
|
IDs []uuid.UUID `db:"ids" json:"ids"`
|
|
StaleIntervalMS int64 `db:"stale_interval_ms" json:"stale_interval_ms"`
|
|
}
|
|
|
|
type GetProvisionerJobsByIDsWithQueuePositionRow struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
|
ProvisionerJob ProvisionerJob `db:"provisioner_job" json:"provisioner_job"`
|
|
QueuePosition int64 `db:"queue_position" json:"queue_position"`
|
|
QueueSize int64 `db:"queue_size" json:"queue_size"`
|
|
}
|
|
|
|
func (q *sqlQuerier) GetProvisionerJobsByIDsWithQueuePosition(ctx context.Context, arg GetProvisionerJobsByIDsWithQueuePositionParams) ([]GetProvisionerJobsByIDsWithQueuePositionRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, getProvisionerJobsByIDsWithQueuePosition, pq.Array(arg.IDs), arg.StaleIntervalMS)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []GetProvisionerJobsByIDsWithQueuePositionRow
|
|
for rows.Next() {
|
|
var i GetProvisionerJobsByIDsWithQueuePositionRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.CreatedAt,
|
|
&i.ProvisionerJob.ID,
|
|
&i.ProvisionerJob.CreatedAt,
|
|
&i.ProvisionerJob.UpdatedAt,
|
|
&i.ProvisionerJob.StartedAt,
|
|
&i.ProvisionerJob.CanceledAt,
|
|
&i.ProvisionerJob.CompletedAt,
|
|
&i.ProvisionerJob.Error,
|
|
&i.ProvisionerJob.OrganizationID,
|
|
&i.ProvisionerJob.InitiatorID,
|
|
&i.ProvisionerJob.Provisioner,
|
|
&i.ProvisionerJob.StorageMethod,
|
|
&i.ProvisionerJob.Type,
|
|
&i.ProvisionerJob.Input,
|
|
&i.ProvisionerJob.WorkerID,
|
|
&i.ProvisionerJob.FileID,
|
|
&i.ProvisionerJob.Tags,
|
|
&i.ProvisionerJob.ErrorCode,
|
|
&i.ProvisionerJob.TraceMetadata,
|
|
&i.ProvisionerJob.JobStatus,
|
|
&i.ProvisionerJob.LogsLength,
|
|
&i.ProvisionerJob.LogsOverflowed,
|
|
&i.QueuePosition,
|
|
&i.QueueSize,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner = `-- name: GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner :many
|
|
WITH pending_jobs AS (
|
|
SELECT
|
|
id, initiator_id, created_at
|
|
FROM
|
|
provisioner_jobs
|
|
WHERE
|
|
started_at IS NULL
|
|
AND
|
|
canceled_at IS NULL
|
|
AND
|
|
completed_at IS NULL
|
|
AND
|
|
error IS NULL
|
|
),
|
|
queue_position AS (
|
|
SELECT
|
|
id,
|
|
ROW_NUMBER() OVER (ORDER BY initiator_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid ASC, created_at ASC) AS queue_position
|
|
FROM
|
|
pending_jobs
|
|
),
|
|
queue_size AS (
|
|
SELECT COUNT(*) AS count FROM pending_jobs
|
|
)
|
|
SELECT
|
|
pj.id, pj.created_at, pj.updated_at, pj.started_at, pj.canceled_at, pj.completed_at, pj.error, pj.organization_id, pj.initiator_id, pj.provisioner, pj.storage_method, pj.type, pj.input, pj.worker_id, pj.file_id, pj.tags, pj.error_code, pj.trace_metadata, pj.job_status, pj.logs_length, pj.logs_overflowed,
|
|
COALESCE(qp.queue_position, 0) AS queue_position,
|
|
COALESCE(qs.count, 0) AS queue_size,
|
|
-- Use subquery to utilize ORDER BY in array_agg since it cannot be
|
|
-- combined with FILTER.
|
|
(
|
|
SELECT
|
|
-- Order for stable output.
|
|
array_agg(pd.id ORDER BY pd.created_at ASC)::uuid[]
|
|
FROM
|
|
provisioner_daemons pd
|
|
WHERE
|
|
-- See AcquireProvisionerJob.
|
|
pj.started_at IS NULL
|
|
AND pj.organization_id = pd.organization_id
|
|
AND pj.provisioner = ANY(pd.provisioners)
|
|
AND provisioner_tagset_contains(pd.tags, pj.tags)
|
|
) AS available_workers,
|
|
-- Include template and workspace information.
|
|
COALESCE(tv.name, '') AS template_version_name,
|
|
t.id AS template_id,
|
|
COALESCE(t.name, '') AS template_name,
|
|
COALESCE(t.display_name, '') AS template_display_name,
|
|
COALESCE(t.icon, '') AS template_icon,
|
|
w.id AS workspace_id,
|
|
COALESCE(w.name, '') AS workspace_name,
|
|
-- Include the name of the provisioner_daemon associated to the job
|
|
COALESCE(pd.name, '') AS worker_name,
|
|
wb.transition as workspace_build_transition
|
|
FROM
|
|
provisioner_jobs pj
|
|
LEFT JOIN
|
|
queue_position qp ON qp.id = pj.id
|
|
LEFT JOIN
|
|
queue_size qs ON TRUE
|
|
LEFT JOIN
|
|
workspace_builds wb ON wb.id = CASE WHEN pj.input ? 'workspace_build_id' THEN (pj.input->>'workspace_build_id')::uuid END
|
|
LEFT JOIN
|
|
workspaces w ON (
|
|
w.id = wb.workspace_id
|
|
AND w.organization_id = pj.organization_id
|
|
)
|
|
LEFT JOIN
|
|
-- We should always have a template version, either explicitly or implicitly via workspace build.
|
|
template_versions tv ON (
|
|
tv.id = CASE WHEN pj.input ? 'template_version_id' THEN (pj.input->>'template_version_id')::uuid ELSE wb.template_version_id END
|
|
AND tv.organization_id = pj.organization_id
|
|
)
|
|
LEFT JOIN
|
|
templates t ON (
|
|
t.id = tv.template_id
|
|
AND t.organization_id = pj.organization_id
|
|
)
|
|
LEFT JOIN
|
|
-- Join to get the daemon name corresponding to the job's worker_id
|
|
provisioner_daemons pd ON pd.id = pj.worker_id
|
|
WHERE
|
|
pj.organization_id = $1::uuid
|
|
AND (COALESCE(array_length($2::uuid[], 1), 0) = 0 OR pj.id = ANY($2::uuid[]))
|
|
AND (COALESCE(array_length($3::provisioner_job_status[], 1), 0) = 0 OR pj.job_status = ANY($3::provisioner_job_status[]))
|
|
AND ($4::tagset = 'null'::tagset OR provisioner_tagset_contains(pj.tags::tagset, $4::tagset))
|
|
AND ($5::uuid = '00000000-0000-0000-0000-000000000000'::uuid OR pj.initiator_id = $5::uuid)
|
|
GROUP BY
|
|
pj.id,
|
|
qp.queue_position,
|
|
qs.count,
|
|
tv.name,
|
|
t.id,
|
|
t.name,
|
|
t.display_name,
|
|
t.icon,
|
|
w.id,
|
|
w.name,
|
|
pd.name,
|
|
wb.transition
|
|
ORDER BY
|
|
pj.created_at DESC
|
|
LIMIT
|
|
$6::int
|
|
`
|
|
|
|
type GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams struct {
|
|
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
|
IDs []uuid.UUID `db:"ids" json:"ids"`
|
|
Status []ProvisionerJobStatus `db:"status" json:"status"`
|
|
Tags StringMap `db:"tags" json:"tags"`
|
|
InitiatorID uuid.UUID `db:"initiator_id" json:"initiator_id"`
|
|
Limit sql.NullInt32 `db:"limit" json:"limit"`
|
|
}
|
|
|
|
type GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow struct {
|
|
ProvisionerJob ProvisionerJob `db:"provisioner_job" json:"provisioner_job"`
|
|
QueuePosition int64 `db:"queue_position" json:"queue_position"`
|
|
QueueSize int64 `db:"queue_size" json:"queue_size"`
|
|
AvailableWorkers []uuid.UUID `db:"available_workers" json:"available_workers"`
|
|
TemplateVersionName string `db:"template_version_name" json:"template_version_name"`
|
|
TemplateID uuid.NullUUID `db:"template_id" json:"template_id"`
|
|
TemplateName string `db:"template_name" json:"template_name"`
|
|
TemplateDisplayName string `db:"template_display_name" json:"template_display_name"`
|
|
TemplateIcon string `db:"template_icon" json:"template_icon"`
|
|
WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"`
|
|
WorkspaceName string `db:"workspace_name" json:"workspace_name"`
|
|
WorkerName string `db:"worker_name" json:"worker_name"`
|
|
WorkspaceBuildTransition NullWorkspaceTransition `db:"workspace_build_transition" json:"workspace_build_transition"`
|
|
}
|
|
|
|
func (q *sqlQuerier) GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner(ctx context.Context, arg GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams) ([]GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, getProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner,
|
|
arg.OrganizationID,
|
|
pq.Array(arg.IDs),
|
|
pq.Array(arg.Status),
|
|
arg.Tags,
|
|
arg.InitiatorID,
|
|
arg.Limit,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow
|
|
for rows.Next() {
|
|
var i GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow
|
|
if err := rows.Scan(
|
|
&i.ProvisionerJob.ID,
|
|
&i.ProvisionerJob.CreatedAt,
|
|
&i.ProvisionerJob.UpdatedAt,
|
|
&i.ProvisionerJob.StartedAt,
|
|
&i.ProvisionerJob.CanceledAt,
|
|
&i.ProvisionerJob.CompletedAt,
|
|
&i.ProvisionerJob.Error,
|
|
&i.ProvisionerJob.OrganizationID,
|
|
&i.ProvisionerJob.InitiatorID,
|
|
&i.ProvisionerJob.Provisioner,
|
|
&i.ProvisionerJob.StorageMethod,
|
|
&i.ProvisionerJob.Type,
|
|
&i.ProvisionerJob.Input,
|
|
&i.ProvisionerJob.WorkerID,
|
|
&i.ProvisionerJob.FileID,
|
|
&i.ProvisionerJob.Tags,
|
|
&i.ProvisionerJob.ErrorCode,
|
|
&i.ProvisionerJob.TraceMetadata,
|
|
&i.ProvisionerJob.JobStatus,
|
|
&i.ProvisionerJob.LogsLength,
|
|
&i.ProvisionerJob.LogsOverflowed,
|
|
&i.QueuePosition,
|
|
&i.QueueSize,
|
|
pq.Array(&i.AvailableWorkers),
|
|
&i.TemplateVersionName,
|
|
&i.TemplateID,
|
|
&i.TemplateName,
|
|
&i.TemplateDisplayName,
|
|
&i.TemplateIcon,
|
|
&i.WorkspaceID,
|
|
&i.WorkspaceName,
|
|
&i.WorkerName,
|
|
&i.WorkspaceBuildTransition,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getProvisionerJobsCreatedAfter = `-- name: GetProvisionerJobsCreatedAfter :many
|
|
SELECT id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status, logs_length, logs_overflowed FROM provisioner_jobs WHERE created_at > $1
|
|
`
|
|
|
|
func (q *sqlQuerier) GetProvisionerJobsCreatedAfter(ctx context.Context, createdAt time.Time) ([]ProvisionerJob, error) {
|
|
rows, err := q.db.QueryContext(ctx, getProvisionerJobsCreatedAfter, createdAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ProvisionerJob
|
|
for rows.Next() {
|
|
var i ProvisionerJob
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.StartedAt,
|
|
&i.CanceledAt,
|
|
&i.CompletedAt,
|
|
&i.Error,
|
|
&i.OrganizationID,
|
|
&i.InitiatorID,
|
|
&i.Provisioner,
|
|
&i.StorageMethod,
|
|
&i.Type,
|
|
&i.Input,
|
|
&i.WorkerID,
|
|
&i.FileID,
|
|
&i.Tags,
|
|
&i.ErrorCode,
|
|
&i.TraceMetadata,
|
|
&i.JobStatus,
|
|
&i.LogsLength,
|
|
&i.LogsOverflowed,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getProvisionerJobsToBeReaped = `-- name: GetProvisionerJobsToBeReaped :many
|
|
SELECT
|
|
id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status, logs_length, logs_overflowed
|
|
FROM
|
|
provisioner_jobs
|
|
WHERE
|
|
(
|
|
-- If the job has not been started before @pending_since, reap it.
|
|
updated_at < $1
|
|
AND started_at IS NULL
|
|
AND completed_at IS NULL
|
|
)
|
|
OR
|
|
(
|
|
-- If the job has been started but not completed before @hung_since, reap it.
|
|
updated_at < $2
|
|
AND started_at IS NOT NULL
|
|
AND completed_at IS NULL
|
|
)
|
|
ORDER BY random()
|
|
LIMIT $3
|
|
`
|
|
|
|
type GetProvisionerJobsToBeReapedParams struct {
|
|
PendingSince time.Time `db:"pending_since" json:"pending_since"`
|
|
HungSince time.Time `db:"hung_since" json:"hung_since"`
|
|
MaxJobs int32 `db:"max_jobs" json:"max_jobs"`
|
|
}
|
|
|
|
// To avoid repeatedly attempting to reap the same jobs, we randomly order and limit to @max_jobs.
|
|
func (q *sqlQuerier) GetProvisionerJobsToBeReaped(ctx context.Context, arg GetProvisionerJobsToBeReapedParams) ([]ProvisionerJob, error) {
|
|
rows, err := q.db.QueryContext(ctx, getProvisionerJobsToBeReaped, arg.PendingSince, arg.HungSince, arg.MaxJobs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ProvisionerJob
|
|
for rows.Next() {
|
|
var i ProvisionerJob
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.StartedAt,
|
|
&i.CanceledAt,
|
|
&i.CompletedAt,
|
|
&i.Error,
|
|
&i.OrganizationID,
|
|
&i.InitiatorID,
|
|
&i.Provisioner,
|
|
&i.StorageMethod,
|
|
&i.Type,
|
|
&i.Input,
|
|
&i.WorkerID,
|
|
&i.FileID,
|
|
&i.Tags,
|
|
&i.ErrorCode,
|
|
&i.TraceMetadata,
|
|
&i.JobStatus,
|
|
&i.LogsLength,
|
|
&i.LogsOverflowed,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const insertProvisionerJob = `-- name: InsertProvisionerJob :one
|
|
INSERT INTO
|
|
provisioner_jobs (
|
|
id,
|
|
created_at,
|
|
updated_at,
|
|
organization_id,
|
|
initiator_id,
|
|
provisioner,
|
|
storage_method,
|
|
file_id,
|
|
"type",
|
|
"input",
|
|
tags,
|
|
trace_metadata,
|
|
logs_overflowed
|
|
)
|
|
VALUES
|
|
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status, logs_length, logs_overflowed
|
|
`
|
|
|
|
type InsertProvisionerJobParams struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
|
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
|
InitiatorID uuid.UUID `db:"initiator_id" json:"initiator_id"`
|
|
Provisioner ProvisionerType `db:"provisioner" json:"provisioner"`
|
|
StorageMethod ProvisionerStorageMethod `db:"storage_method" json:"storage_method"`
|
|
FileID uuid.UUID `db:"file_id" json:"file_id"`
|
|
Type ProvisionerJobType `db:"type" json:"type"`
|
|
Input json.RawMessage `db:"input" json:"input"`
|
|
Tags StringMap `db:"tags" json:"tags"`
|
|
TraceMetadata pqtype.NullRawMessage `db:"trace_metadata" json:"trace_metadata"`
|
|
LogsOverflowed bool `db:"logs_overflowed" json:"logs_overflowed"`
|
|
}
|
|
|
|
func (q *sqlQuerier) InsertProvisionerJob(ctx context.Context, arg InsertProvisionerJobParams) (ProvisionerJob, error) {
|
|
row := q.db.QueryRowContext(ctx, insertProvisionerJob,
|
|
arg.ID,
|
|
arg.CreatedAt,
|
|
arg.UpdatedAt,
|
|
arg.OrganizationID,
|
|
arg.InitiatorID,
|
|
arg.Provisioner,
|
|
arg.StorageMethod,
|
|
arg.FileID,
|
|
arg.Type,
|
|
arg.Input,
|
|
arg.Tags,
|
|
arg.TraceMetadata,
|
|
arg.LogsOverflowed,
|
|
)
|
|
var i ProvisionerJob
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.StartedAt,
|
|
&i.CanceledAt,
|
|
&i.CompletedAt,
|
|
&i.Error,
|
|
&i.OrganizationID,
|
|
&i.InitiatorID,
|
|
&i.Provisioner,
|
|
&i.StorageMethod,
|
|
&i.Type,
|
|
&i.Input,
|
|
&i.WorkerID,
|
|
&i.FileID,
|
|
&i.Tags,
|
|
&i.ErrorCode,
|
|
&i.TraceMetadata,
|
|
&i.JobStatus,
|
|
&i.LogsLength,
|
|
&i.LogsOverflowed,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const insertProvisionerJobTimings = `-- name: InsertProvisionerJobTimings :many
|
|
INSERT INTO provisioner_job_timings (job_id, started_at, ended_at, stage, source, action, resource)
|
|
SELECT
|
|
$1::uuid AS provisioner_job_id,
|
|
unnest($2::timestamptz[]),
|
|
unnest($3::timestamptz[]),
|
|
unnest($4::provisioner_job_timing_stage[]),
|
|
unnest($5::text[]),
|
|
unnest($6::text[]),
|
|
unnest($7::text[])
|
|
RETURNING job_id, started_at, ended_at, stage, source, action, resource
|
|
`
|
|
|
|
type InsertProvisionerJobTimingsParams struct {
|
|
JobID uuid.UUID `db:"job_id" json:"job_id"`
|
|
StartedAt []time.Time `db:"started_at" json:"started_at"`
|
|
EndedAt []time.Time `db:"ended_at" json:"ended_at"`
|
|
Stage []ProvisionerJobTimingStage `db:"stage" json:"stage"`
|
|
Source []string `db:"source" json:"source"`
|
|
Action []string `db:"action" json:"action"`
|
|
Resource []string `db:"resource" json:"resource"`
|
|
}
|
|
|
|
func (q *sqlQuerier) InsertProvisionerJobTimings(ctx context.Context, arg InsertProvisionerJobTimingsParams) ([]ProvisionerJobTiming, error) {
|
|
rows, err := q.db.QueryContext(ctx, insertProvisionerJobTimings,
|
|
arg.JobID,
|
|
pq.Array(arg.StartedAt),
|
|
pq.Array(arg.EndedAt),
|
|
pq.Array(arg.Stage),
|
|
pq.Array(arg.Source),
|
|
pq.Array(arg.Action),
|
|
pq.Array(arg.Resource),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ProvisionerJobTiming
|
|
for rows.Next() {
|
|
var i ProvisionerJobTiming
|
|
if err := rows.Scan(
|
|
&i.JobID,
|
|
&i.StartedAt,
|
|
&i.EndedAt,
|
|
&i.Stage,
|
|
&i.Source,
|
|
&i.Action,
|
|
&i.Resource,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const updateProvisionerJobByID = `-- name: UpdateProvisionerJobByID :exec
|
|
UPDATE
|
|
provisioner_jobs
|
|
SET
|
|
updated_at = $2
|
|
WHERE
|
|
id = $1
|
|
`
|
|
|
|
type UpdateProvisionerJobByIDParams struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateProvisionerJobByID(ctx context.Context, arg UpdateProvisionerJobByIDParams) error {
|
|
_, err := q.db.ExecContext(ctx, updateProvisionerJobByID, arg.ID, arg.UpdatedAt)
|
|
return err
|
|
}
|
|
|
|
const updateProvisionerJobWithCancelByID = `-- name: UpdateProvisionerJobWithCancelByID :exec
|
|
UPDATE
|
|
provisioner_jobs
|
|
SET
|
|
canceled_at = $2,
|
|
completed_at = $3
|
|
WHERE
|
|
id = $1
|
|
`
|
|
|
|
type UpdateProvisionerJobWithCancelByIDParams struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
CanceledAt sql.NullTime `db:"canceled_at" json:"canceled_at"`
|
|
CompletedAt sql.NullTime `db:"completed_at" json:"completed_at"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateProvisionerJobWithCancelByID(ctx context.Context, arg UpdateProvisionerJobWithCancelByIDParams) error {
|
|
_, err := q.db.ExecContext(ctx, updateProvisionerJobWithCancelByID, arg.ID, arg.CanceledAt, arg.CompletedAt)
|
|
return err
|
|
}
|
|
|
|
const updateProvisionerJobWithCompleteByID = `-- name: UpdateProvisionerJobWithCompleteByID :exec
|
|
UPDATE
|
|
provisioner_jobs
|
|
SET
|
|
updated_at = $2,
|
|
completed_at = $3,
|
|
error = $4,
|
|
error_code = $5
|
|
WHERE
|
|
id = $1
|
|
`
|
|
|
|
type UpdateProvisionerJobWithCompleteByIDParams struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
|
CompletedAt sql.NullTime `db:"completed_at" json:"completed_at"`
|
|
Error sql.NullString `db:"error" json:"error"`
|
|
ErrorCode sql.NullString `db:"error_code" json:"error_code"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateProvisionerJobWithCompleteByID(ctx context.Context, arg UpdateProvisionerJobWithCompleteByIDParams) error {
|
|
_, err := q.db.ExecContext(ctx, updateProvisionerJobWithCompleteByID,
|
|
arg.ID,
|
|
arg.UpdatedAt,
|
|
arg.CompletedAt,
|
|
arg.Error,
|
|
arg.ErrorCode,
|
|
)
|
|
return err
|
|
}
|
|
|
|
const updateProvisionerJobWithCompleteWithStartedAtByID = `-- name: UpdateProvisionerJobWithCompleteWithStartedAtByID :exec
|
|
UPDATE
|
|
provisioner_jobs
|
|
SET
|
|
updated_at = $2,
|
|
completed_at = $3,
|
|
error = $4,
|
|
error_code = $5,
|
|
started_at = $6
|
|
WHERE
|
|
id = $1
|
|
`
|
|
|
|
type UpdateProvisionerJobWithCompleteWithStartedAtByIDParams struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
|
CompletedAt sql.NullTime `db:"completed_at" json:"completed_at"`
|
|
Error sql.NullString `db:"error" json:"error"`
|
|
ErrorCode sql.NullString `db:"error_code" json:"error_code"`
|
|
StartedAt sql.NullTime `db:"started_at" json:"started_at"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateProvisionerJobWithCompleteWithStartedAtByID(ctx context.Context, arg UpdateProvisionerJobWithCompleteWithStartedAtByIDParams) error {
|
|
_, err := q.db.ExecContext(ctx, updateProvisionerJobWithCompleteWithStartedAtByID,
|
|
arg.ID,
|
|
arg.UpdatedAt,
|
|
arg.CompletedAt,
|
|
arg.Error,
|
|
arg.ErrorCode,
|
|
arg.StartedAt,
|
|
)
|
|
return err
|
|
}
|
|
|
|
const deleteProvisionerKey = `-- name: DeleteProvisionerKey :exec
|
|
DELETE FROM
|
|
provisioner_keys
|
|
WHERE
|
|
id = $1
|
|
`
|
|
|
|
func (q *sqlQuerier) DeleteProvisionerKey(ctx context.Context, id uuid.UUID) error {
|
|
_, err := q.db.ExecContext(ctx, deleteProvisionerKey, id)
|
|
return err
|
|
}
|
|
|
|
const getProvisionerKeyByHashedSecret = `-- name: GetProvisionerKeyByHashedSecret :one
|
|
SELECT
|
|
id, created_at, organization_id, name, hashed_secret, tags
|
|
FROM
|
|
provisioner_keys
|
|
WHERE
|
|
hashed_secret = $1
|
|
`
|
|
|
|
func (q *sqlQuerier) GetProvisionerKeyByHashedSecret(ctx context.Context, hashedSecret []byte) (ProvisionerKey, error) {
|
|
row := q.db.QueryRowContext(ctx, getProvisionerKeyByHashedSecret, hashedSecret)
|
|
var i ProvisionerKey
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.CreatedAt,
|
|
&i.OrganizationID,
|
|
&i.Name,
|
|
&i.HashedSecret,
|
|
&i.Tags,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getProvisionerKeyByID = `-- name: GetProvisionerKeyByID :one
|
|
SELECT
|
|
id, created_at, organization_id, name, hashed_secret, tags
|
|
FROM
|
|
provisioner_keys
|
|
WHERE
|
|
id = $1
|
|
`
|
|
|
|
func (q *sqlQuerier) GetProvisionerKeyByID(ctx context.Context, id uuid.UUID) (ProvisionerKey, error) {
|
|
row := q.db.QueryRowContext(ctx, getProvisionerKeyByID, id)
|
|
var i ProvisionerKey
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.CreatedAt,
|
|
&i.OrganizationID,
|
|
&i.Name,
|
|
&i.HashedSecret,
|
|
&i.Tags,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getProvisionerKeyByName = `-- name: GetProvisionerKeyByName :one
|
|
SELECT
|
|
id, created_at, organization_id, name, hashed_secret, tags
|
|
FROM
|
|
provisioner_keys
|
|
WHERE
|
|
organization_id = $1
|
|
AND
|
|
lower(name) = lower($2)
|
|
`
|
|
|
|
type GetProvisionerKeyByNameParams struct {
|
|
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
|
Name string `db:"name" json:"name"`
|
|
}
|
|
|
|
func (q *sqlQuerier) GetProvisionerKeyByName(ctx context.Context, arg GetProvisionerKeyByNameParams) (ProvisionerKey, error) {
|
|
row := q.db.QueryRowContext(ctx, getProvisionerKeyByName, arg.OrganizationID, arg.Name)
|
|
var i ProvisionerKey
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.CreatedAt,
|
|
&i.OrganizationID,
|
|
&i.Name,
|
|
&i.HashedSecret,
|
|
&i.Tags,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const insertProvisionerKey = `-- name: InsertProvisionerKey :one
|
|
INSERT INTO
|
|
provisioner_keys (
|
|
id,
|
|
created_at,
|
|
organization_id,
|
|
name,
|
|
hashed_secret,
|
|
tags
|
|
)
|
|
VALUES
|
|
($1, $2, $3, lower($6), $4, $5) RETURNING id, created_at, organization_id, name, hashed_secret, tags
|
|
`
|
|
|
|
type InsertProvisionerKeyParams struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
|
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
|
HashedSecret []byte `db:"hashed_secret" json:"hashed_secret"`
|
|
Tags StringMap `db:"tags" json:"tags"`
|
|
Name string `db:"name" json:"name"`
|
|
}
|
|
|
|
func (q *sqlQuerier) InsertProvisionerKey(ctx context.Context, arg InsertProvisionerKeyParams) (ProvisionerKey, error) {
|
|
row := q.db.QueryRowContext(ctx, insertProvisionerKey,
|
|
arg.ID,
|
|
arg.CreatedAt,
|
|
arg.OrganizationID,
|
|
arg.HashedSecret,
|
|
arg.Tags,
|
|
arg.Name,
|
|
)
|
|
var i ProvisionerKey
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.CreatedAt,
|
|
&i.OrganizationID,
|
|
&i.Name,
|
|
&i.HashedSecret,
|
|
&i.Tags,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const listProvisionerKeysByOrganization = `-- name: ListProvisionerKeysByOrganization :many
|
|
SELECT
|
|
id, created_at, organization_id, name, hashed_secret, tags
|
|
FROM
|
|
provisioner_keys
|
|
WHERE
|
|
organization_id = $1
|
|
`
|
|
|
|
func (q *sqlQuerier) ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerKey, error) {
|
|
rows, err := q.db.QueryContext(ctx, listProvisionerKeysByOrganization, organizationID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ProvisionerKey
|
|
for rows.Next() {
|
|
var i ProvisionerKey
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.CreatedAt,
|
|
&i.OrganizationID,
|
|
&i.Name,
|
|
&i.HashedSecret,
|
|
&i.Tags,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listProvisionerKeysByOrganizationExcludeReserved = `-- name: ListProvisionerKeysByOrganizationExcludeReserved :many
|
|
SELECT
|
|
id, created_at, organization_id, name, hashed_secret, tags
|
|
FROM
|
|
provisioner_keys
|
|
WHERE
|
|
organization_id = $1
|
|
AND
|
|
-- exclude reserved built-in key
|
|
id != '00000000-0000-0000-0000-000000000001'::uuid
|
|
AND
|
|
-- exclude reserved user-auth key
|
|
id != '00000000-0000-0000-0000-000000000002'::uuid
|
|
AND
|
|
-- exclude reserved psk key
|
|
id != '00000000-0000-0000-0000-000000000003'::uuid
|
|
`
|
|
|
|
func (q *sqlQuerier) ListProvisionerKeysByOrganizationExcludeReserved(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerKey, error) {
|
|
rows, err := q.db.QueryContext(ctx, listProvisionerKeysByOrganizationExcludeReserved, organizationID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ProvisionerKey
|
|
for rows.Next() {
|
|
var i ProvisionerKey
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.CreatedAt,
|
|
&i.OrganizationID,
|
|
&i.Name,
|
|
&i.HashedSecret,
|
|
&i.Tags,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const 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 getChatGoalsEnabled = `-- name: GetChatGoalsEnabled :one
|
|
SELECT
|
|
COALESCE((SELECT value = 'true' FROM site_configs WHERE key = 'agents_chat_goals_enabled'), false) :: boolean AS enabled
|
|
`
|
|
|
|
// GetChatGoalsEnabled returns whether the chat goals experiment is enabled.
|
|
// It defaults to false when unset.
|
|
func (q *sqlQuerier) GetChatGoalsEnabled(ctx context.Context) (bool, error) {
|
|
row := q.db.QueryRowContext(ctx, getChatGoalsEnabled)
|
|
var enabled bool
|
|
err := row.Scan(&enabled)
|
|
return enabled, 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 upsertChatGoalsEnabled = `-- name: UpsertChatGoalsEnabled :exec
|
|
INSERT INTO site_configs (key, value)
|
|
VALUES (
|
|
'agents_chat_goals_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_goals_enabled'
|
|
`
|
|
|
|
// UpsertChatGoalsEnabled updates whether the chat goals experiment is enabled.
|
|
func (q *sqlQuerier) UpsertChatGoalsEnabled(ctx context.Context, enabled bool) error {
|
|
_, err := q.db.ExecContext(ctx, upsertChatGoalsEnabled, enabled)
|
|
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 updateUserLinkedID = `-- name: UpdateUserLinkedID :one
|
|
UPDATE
|
|
user_links
|
|
SET
|
|
linked_id = $1
|
|
WHERE
|
|
user_id = $2 AND login_type = $3 AND linked_id = '' RETURNING user_id, login_type, linked_id, oauth_access_token, oauth_refresh_token, oauth_expiry, oauth_access_token_key_id, oauth_refresh_token_key_id, claims
|
|
`
|
|
|
|
type UpdateUserLinkedIDParams struct {
|
|
LinkedID string `db:"linked_id" json:"linked_id"`
|
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
|
LoginType LoginType `db:"login_type" json:"login_type"`
|
|
}
|
|
|
|
// Backfills linked_id for legacy user_links that were created before
|
|
// linked_id tracking was added. Only updates when linked_id is empty
|
|
// to avoid overwriting a valid binding.
|
|
func (q *sqlQuerier) UpdateUserLinkedID(ctx context.Context, arg UpdateUserLinkedIDParams) (UserLink, error) {
|
|
row := q.db.QueryRowContext(ctx, updateUserLinkedID, arg.LinkedID, arg.UserID, arg.LoginType)
|
|
var i UserLink
|
|
err := row.Scan(
|
|
&i.UserID,
|
|
&i.LoginType,
|
|
&i.LinkedID,
|
|
&i.OAuthAccessToken,
|
|
&i.OAuthRefreshToken,
|
|
&i.OAuthExpiry,
|
|
&i.OAuthAccessTokenKeyID,
|
|
&i.OAuthRefreshTokenKeyID,
|
|
&i.Claims,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const createUserSecret = `-- name: CreateUserSecret :one
|
|
INSERT INTO user_secrets (
|
|
id,
|
|
user_id,
|
|
name,
|
|
description,
|
|
value,
|
|
value_key_id,
|
|
env_name,
|
|
file_path
|
|
) 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
|
|
JOIN organizations ON organizations.id = organization_members.organization_id,
|
|
-- All org members get an implied role for their orgs. Most members
|
|
-- get organization-member, but service accounts will get
|
|
-- organization-service-account instead. They're largely the same,
|
|
-- but having them be distinct means we can allow configuring
|
|
-- service-accounts to have slightly broader permissions, such as
|
|
-- for workspace sharing.
|
|
--
|
|
-- organizations.default_org_member_roles is unioned in so changes
|
|
-- to org defaults propagate to every member on the next request.
|
|
unnest(
|
|
array_cat(
|
|
array_append(
|
|
roles,
|
|
CASE WHEN users.is_service_account THEN
|
|
'organization-service-account'
|
|
ELSE
|
|
'organization-member'
|
|
END
|
|
),
|
|
organizations.default_org_member_roles
|
|
)
|
|
) AS org_roles
|
|
WHERE
|
|
user_id = users.id
|
|
)
|
|
) :: text[] AS roles,
|
|
-- All groups the user is in.
|
|
(
|
|
SELECT
|
|
array_agg(
|
|
group_members.group_id :: text
|
|
)
|
|
FROM
|
|
group_members
|
|
WHERE
|
|
user_id = users.id
|
|
) :: text[] AS groups
|
|
FROM
|
|
users
|
|
WHERE
|
|
users.id = $1
|
|
`
|
|
|
|
type GetAuthorizationUserRolesRow struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
Username string `db:"username" json:"username"`
|
|
Status UserStatus `db:"status" json:"status"`
|
|
Email string `db:"email" json:"email"`
|
|
Roles []string `db:"roles" json:"roles"`
|
|
Groups []string `db:"groups" json:"groups"`
|
|
}
|
|
|
|
// This function returns roles for authorization purposes. Implied member roles
|
|
// are included.
|
|
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 exact username
|
|
AND CASE
|
|
WHEN $4 :: text != '' THEN
|
|
lower(username) = lower($4)
|
|
ELSE true
|
|
END
|
|
-- Filter by exact email
|
|
AND CASE
|
|
WHEN $5 :: text != '' THEN
|
|
lower(email) = lower($5)
|
|
ELSE true
|
|
END
|
|
-- Filter by status
|
|
AND CASE
|
|
-- @status needs to be a text because it can be empty, If it was
|
|
-- user_status enum, it would not.
|
|
WHEN cardinality($6 :: user_status[]) > 0 THEN
|
|
status = ANY($6 :: user_status[])
|
|
ELSE true
|
|
END
|
|
-- Filter by rbac_roles
|
|
AND CASE
|
|
-- @rbac_role allows filtering by rbac roles. If 'member' is included, show everyone, as
|
|
-- everyone is a member.
|
|
WHEN cardinality($7 :: text[]) > 0 AND 'member' != ANY($7 :: text[]) THEN
|
|
rbac_roles && $7 :: text[]
|
|
ELSE true
|
|
END
|
|
-- Filter by last_seen
|
|
AND CASE
|
|
WHEN $8 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
|
|
last_seen_at <= $8
|
|
ELSE true
|
|
END
|
|
AND CASE
|
|
WHEN $9 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
|
|
last_seen_at >= $9
|
|
ELSE true
|
|
END
|
|
-- Filter by created_at
|
|
AND CASE
|
|
WHEN $10 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
|
|
created_at <= $10
|
|
ELSE true
|
|
END
|
|
AND CASE
|
|
WHEN $11 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
|
|
created_at >= $11
|
|
ELSE true
|
|
END
|
|
-- Filter by system type
|
|
AND CASE
|
|
WHEN $12::bool THEN TRUE
|
|
ELSE is_system = false
|
|
END
|
|
-- Filter by github.com user ID
|
|
AND CASE
|
|
WHEN $13 :: bigint != 0 THEN
|
|
github_com_user_id = $13
|
|
ELSE true
|
|
END
|
|
-- Filter by login_type
|
|
AND CASE
|
|
WHEN cardinality($14 :: login_type[]) > 0 THEN
|
|
login_type = ANY($14 :: login_type[])
|
|
ELSE true
|
|
END
|
|
-- Filter by service account.
|
|
AND CASE
|
|
WHEN $15 :: boolean IS NOT NULL THEN
|
|
is_service_account = $15 :: boolean
|
|
ELSE true
|
|
END
|
|
-- End of filters
|
|
|
|
-- Authorize Filter clause will be injected below in GetAuthorizedUsers
|
|
-- @authorize_filter
|
|
ORDER BY
|
|
-- Deterministic and consistent ordering of all users. This is to ensure consistent pagination.
|
|
LOWER(username) ASC OFFSET $16
|
|
LIMIT
|
|
-- A null limit means "no limit", so 0 means return all
|
|
NULLIF($17 :: int, 0)
|
|
`
|
|
|
|
type GetUsersParams struct {
|
|
AfterID uuid.UUID `db:"after_id" json:"after_id"`
|
|
Search string `db:"search" json:"search"`
|
|
Name string `db:"name" json:"name"`
|
|
ExactUsername string `db:"exact_username" json:"exact_username"`
|
|
ExactEmail string `db:"exact_email" json:"exact_email"`
|
|
Status []UserStatus `db:"status" json:"status"`
|
|
RbacRole []string `db:"rbac_role" json:"rbac_role"`
|
|
LastSeenBefore time.Time `db:"last_seen_before" json:"last_seen_before"`
|
|
LastSeenAfter time.Time `db:"last_seen_after" json:"last_seen_after"`
|
|
CreatedBefore time.Time `db:"created_before" json:"created_before"`
|
|
CreatedAfter time.Time `db:"created_after" json:"created_after"`
|
|
IncludeSystem bool `db:"include_system" json:"include_system"`
|
|
GithubComUserID int64 `db:"github_com_user_id" json:"github_com_user_id"`
|
|
LoginType []LoginType `db:"login_type" json:"login_type"`
|
|
IsServiceAccount sql.NullBool `db:"is_service_account" json:"is_service_account"`
|
|
OffsetOpt int32 `db:"offset_opt" json:"offset_opt"`
|
|
LimitOpt int32 `db:"limit_opt" json:"limit_opt"`
|
|
}
|
|
|
|
type GetUsersRow struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
Email string `db:"email" json:"email"`
|
|
Username string `db:"username" json:"username"`
|
|
HashedPassword []byte `db:"hashed_password" json:"hashed_password"`
|
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
|
Status UserStatus `db:"status" json:"status"`
|
|
RBACRoles pq.StringArray `db:"rbac_roles" json:"rbac_roles"`
|
|
LoginType LoginType `db:"login_type" json:"login_type"`
|
|
AvatarURL string `db:"avatar_url" json:"avatar_url"`
|
|
Deleted bool `db:"deleted" json:"deleted"`
|
|
LastSeenAt time.Time `db:"last_seen_at" json:"last_seen_at"`
|
|
QuietHoursSchedule string `db:"quiet_hours_schedule" json:"quiet_hours_schedule"`
|
|
Name string `db:"name" json:"name"`
|
|
GithubComUserID sql.NullInt64 `db:"github_com_user_id" json:"github_com_user_id"`
|
|
HashedOneTimePasscode []byte `db:"hashed_one_time_passcode" json:"hashed_one_time_passcode"`
|
|
OneTimePasscodeExpiresAt sql.NullTime `db:"one_time_passcode_expires_at" json:"one_time_passcode_expires_at"`
|
|
IsSystem bool `db:"is_system" json:"is_system"`
|
|
IsServiceAccount bool `db:"is_service_account" json:"is_service_account"`
|
|
ChatSpendLimitMicros sql.NullInt64 `db:"chat_spend_limit_micros" json:"chat_spend_limit_micros"`
|
|
Count int64 `db:"count" json:"count"`
|
|
}
|
|
|
|
// This will never return deleted users.
|
|
func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUsersRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, getUsers,
|
|
arg.AfterID,
|
|
arg.Search,
|
|
arg.Name,
|
|
arg.ExactUsername,
|
|
arg.ExactEmail,
|
|
pq.Array(arg.Status),
|
|
pq.Array(arg.RbacRole),
|
|
arg.LastSeenBefore,
|
|
arg.LastSeenAfter,
|
|
arg.CreatedBefore,
|
|
arg.CreatedAfter,
|
|
arg.IncludeSystem,
|
|
arg.GithubComUserID,
|
|
pq.Array(arg.LoginType),
|
|
arg.IsServiceAccount,
|
|
arg.OffsetOpt,
|
|
arg.LimitOpt,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []GetUsersRow
|
|
for rows.Next() {
|
|
var i GetUsersRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.Email,
|
|
&i.Username,
|
|
&i.HashedPassword,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Status,
|
|
&i.RBACRoles,
|
|
&i.LoginType,
|
|
&i.AvatarURL,
|
|
&i.Deleted,
|
|
&i.LastSeenAt,
|
|
&i.QuietHoursSchedule,
|
|
&i.Name,
|
|
&i.GithubComUserID,
|
|
&i.HashedOneTimePasscode,
|
|
&i.OneTimePasscodeExpiresAt,
|
|
&i.IsSystem,
|
|
&i.IsServiceAccount,
|
|
&i.ChatSpendLimitMicros,
|
|
&i.Count,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getUsersByIDs = `-- name: GetUsersByIDs :many
|
|
SELECT id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, is_service_account, chat_spend_limit_micros FROM users WHERE id = ANY($1 :: uuid [ ])
|
|
`
|
|
|
|
// This shouldn't check for deleted, because it's frequently used
|
|
// to look up references to actions. eg. a user could build a workspace
|
|
// for another user, then be deleted... we still want them to appear!
|
|
func (q *sqlQuerier) GetUsersByIDs(ctx context.Context, ids []uuid.UUID) ([]User, error) {
|
|
rows, err := q.db.QueryContext(ctx, getUsersByIDs, pq.Array(ids))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []User
|
|
for rows.Next() {
|
|
var i User
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.Email,
|
|
&i.Username,
|
|
&i.HashedPassword,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Status,
|
|
&i.RBACRoles,
|
|
&i.LoginType,
|
|
&i.AvatarURL,
|
|
&i.Deleted,
|
|
&i.LastSeenAt,
|
|
&i.QuietHoursSchedule,
|
|
&i.Name,
|
|
&i.GithubComUserID,
|
|
&i.HashedOneTimePasscode,
|
|
&i.OneTimePasscodeExpiresAt,
|
|
&i.IsSystem,
|
|
&i.IsServiceAccount,
|
|
&i.ChatSpendLimitMicros,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const insertUser = `-- name: InsertUser :one
|
|
INSERT INTO
|
|
users (
|
|
id,
|
|
email,
|
|
username,
|
|
name,
|
|
hashed_password,
|
|
created_at,
|
|
updated_at,
|
|
rbac_roles,
|
|
login_type,
|
|
status,
|
|
is_service_account
|
|
)
|
|
VALUES
|
|
($1, $2, $3, $4, $5, $6, $7, $8, $9,
|
|
-- if the status passed in is empty, fallback to dormant, which is what
|
|
-- we were doing before.
|
|
COALESCE(NULLIF($10::text, '')::user_status, 'dormant'::user_status),
|
|
$11::bool
|
|
) RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, is_service_account, chat_spend_limit_micros
|
|
`
|
|
|
|
type InsertUserParams struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
Email string `db:"email" json:"email"`
|
|
Username string `db:"username" json:"username"`
|
|
Name string `db:"name" json:"name"`
|
|
HashedPassword []byte `db:"hashed_password" json:"hashed_password"`
|
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
|
RBACRoles pq.StringArray `db:"rbac_roles" json:"rbac_roles"`
|
|
LoginType LoginType `db:"login_type" json:"login_type"`
|
|
Status string `db:"status" json:"status"`
|
|
IsServiceAccount bool `db:"is_service_account" json:"is_service_account"`
|
|
}
|
|
|
|
func (q *sqlQuerier) InsertUser(ctx context.Context, arg InsertUserParams) (User, error) {
|
|
row := q.db.QueryRowContext(ctx, insertUser,
|
|
arg.ID,
|
|
arg.Email,
|
|
arg.Username,
|
|
arg.Name,
|
|
arg.HashedPassword,
|
|
arg.CreatedAt,
|
|
arg.UpdatedAt,
|
|
arg.RBACRoles,
|
|
arg.LoginType,
|
|
arg.Status,
|
|
arg.IsServiceAccount,
|
|
)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Email,
|
|
&i.Username,
|
|
&i.HashedPassword,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Status,
|
|
&i.RBACRoles,
|
|
&i.LoginType,
|
|
&i.AvatarURL,
|
|
&i.Deleted,
|
|
&i.LastSeenAt,
|
|
&i.QuietHoursSchedule,
|
|
&i.Name,
|
|
&i.GithubComUserID,
|
|
&i.HashedOneTimePasscode,
|
|
&i.OneTimePasscodeExpiresAt,
|
|
&i.IsSystem,
|
|
&i.IsServiceAccount,
|
|
&i.ChatSpendLimitMicros,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const listUserChatCompactionThresholds = `-- name: ListUserChatCompactionThresholds :many
|
|
SELECT user_id, key, value FROM user_configs
|
|
WHERE user_id = $1
|
|
AND key LIKE 'chat\_compaction\_threshold\_pct:%'
|
|
ORDER BY key
|
|
`
|
|
|
|
func (q *sqlQuerier) ListUserChatCompactionThresholds(ctx context.Context, userID uuid.UUID) ([]UserConfig, error) {
|
|
rows, err := q.db.QueryContext(ctx, listUserChatCompactionThresholds, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []UserConfig
|
|
for rows.Next() {
|
|
var i UserConfig
|
|
if err := rows.Scan(&i.UserID, &i.Key, &i.Value); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listUserChatPersonalModelOverrides = `-- name: ListUserChatPersonalModelOverrides :many
|
|
SELECT key, value FROM user_configs
|
|
WHERE user_id = $1
|
|
AND key LIKE 'chat\_personal\_model\_override:%'
|
|
ORDER BY key
|
|
`
|
|
|
|
type ListUserChatPersonalModelOverridesRow struct {
|
|
Key string `db:"key" json:"key"`
|
|
Value string `db:"value" json:"value"`
|
|
}
|
|
|
|
func (q *sqlQuerier) ListUserChatPersonalModelOverrides(ctx context.Context, userID uuid.UUID) ([]ListUserChatPersonalModelOverridesRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, listUserChatPersonalModelOverrides, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListUserChatPersonalModelOverridesRow
|
|
for rows.Next() {
|
|
var i ListUserChatPersonalModelOverridesRow
|
|
if err := rows.Scan(&i.Key, &i.Value); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const updateInactiveUsersToDormant = `-- name: UpdateInactiveUsersToDormant :many
|
|
UPDATE
|
|
users
|
|
SET
|
|
status = 'dormant'::user_status,
|
|
updated_at = $1
|
|
WHERE
|
|
last_seen_at < $2 :: timestamp
|
|
AND status = 'active'::user_status
|
|
AND NOT is_system
|
|
RETURNING id, email, username, last_seen_at
|
|
`
|
|
|
|
type UpdateInactiveUsersToDormantParams struct {
|
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
|
LastSeenAfter time.Time `db:"last_seen_after" json:"last_seen_after"`
|
|
}
|
|
|
|
type UpdateInactiveUsersToDormantRow struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
Email string `db:"email" json:"email"`
|
|
Username string `db:"username" json:"username"`
|
|
LastSeenAt time.Time `db:"last_seen_at" json:"last_seen_at"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateInactiveUsersToDormant(ctx context.Context, arg UpdateInactiveUsersToDormantParams) ([]UpdateInactiveUsersToDormantRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, updateInactiveUsersToDormant, arg.UpdatedAt, arg.LastSeenAfter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []UpdateInactiveUsersToDormantRow
|
|
for rows.Next() {
|
|
var i UpdateInactiveUsersToDormantRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.Email,
|
|
&i.Username,
|
|
&i.LastSeenAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const updateUserAgentChatSendShortcut = `-- name: UpdateUserAgentChatSendShortcut :one
|
|
INSERT INTO
|
|
user_configs (user_id, key, value)
|
|
VALUES
|
|
($1, 'preference_agent_chat_send_shortcut', $2::text)
|
|
ON CONFLICT
|
|
ON CONSTRAINT user_configs_pkey
|
|
DO UPDATE
|
|
SET
|
|
value = $2
|
|
WHERE user_configs.user_id = $1
|
|
AND user_configs.key = 'preference_agent_chat_send_shortcut'
|
|
RETURNING value AS agent_chat_send_shortcut
|
|
`
|
|
|
|
type UpdateUserAgentChatSendShortcutParams struct {
|
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
|
AgentChatSendShortcut string `db:"agent_chat_send_shortcut" json:"agent_chat_send_shortcut"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateUserAgentChatSendShortcut(ctx context.Context, arg UpdateUserAgentChatSendShortcutParams) (string, error) {
|
|
row := q.db.QueryRowContext(ctx, updateUserAgentChatSendShortcut, arg.UserID, arg.AgentChatSendShortcut)
|
|
var agent_chat_send_shortcut string
|
|
err := row.Scan(&agent_chat_send_shortcut)
|
|
return agent_chat_send_shortcut, err
|
|
}
|
|
|
|
const updateUserChatCompactionThreshold = `-- name: UpdateUserChatCompactionThreshold :one
|
|
INSERT INTO user_configs (user_id, key, value)
|
|
VALUES ($1, $2, ($3::int)::text)
|
|
ON CONFLICT ON CONSTRAINT user_configs_pkey
|
|
DO UPDATE SET value = ($3::int)::text
|
|
RETURNING user_id, key, value
|
|
`
|
|
|
|
type UpdateUserChatCompactionThresholdParams struct {
|
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
|
Key string `db:"key" json:"key"`
|
|
ThresholdPercent int32 `db:"threshold_percent" json:"threshold_percent"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateUserChatCompactionThreshold(ctx context.Context, arg UpdateUserChatCompactionThresholdParams) (UserConfig, error) {
|
|
row := q.db.QueryRowContext(ctx, updateUserChatCompactionThreshold, arg.UserID, arg.Key, arg.ThresholdPercent)
|
|
var i UserConfig
|
|
err := row.Scan(&i.UserID, &i.Key, &i.Value)
|
|
return i, err
|
|
}
|
|
|
|
const updateUserChatCustomPrompt = `-- name: UpdateUserChatCustomPrompt :one
|
|
INSERT INTO
|
|
user_configs (user_id, key, value)
|
|
VALUES
|
|
($1, 'chat_custom_prompt', $2)
|
|
ON CONFLICT
|
|
ON CONSTRAINT user_configs_pkey
|
|
DO UPDATE
|
|
SET
|
|
value = $2
|
|
WHERE user_configs.user_id = $1
|
|
AND user_configs.key = 'chat_custom_prompt'
|
|
RETURNING user_id, key, value
|
|
`
|
|
|
|
type UpdateUserChatCustomPromptParams struct {
|
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
|
ChatCustomPrompt string `db:"chat_custom_prompt" json:"chat_custom_prompt"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateUserChatCustomPrompt(ctx context.Context, arg UpdateUserChatCustomPromptParams) (UserConfig, error) {
|
|
row := q.db.QueryRowContext(ctx, updateUserChatCustomPrompt, arg.UserID, arg.ChatCustomPrompt)
|
|
var i UserConfig
|
|
err := row.Scan(&i.UserID, &i.Key, &i.Value)
|
|
return i, err
|
|
}
|
|
|
|
const updateUserCodeDiffDisplayMode = `-- name: UpdateUserCodeDiffDisplayMode :one
|
|
INSERT INTO
|
|
user_configs (user_id, key, value)
|
|
VALUES
|
|
($1, 'preference_code_diff_display_mode', $2::text)
|
|
ON CONFLICT
|
|
ON CONSTRAINT user_configs_pkey
|
|
DO UPDATE
|
|
SET
|
|
value = $2
|
|
WHERE user_configs.user_id = $1
|
|
AND user_configs.key = 'preference_code_diff_display_mode'
|
|
RETURNING value AS code_diff_display_mode
|
|
`
|
|
|
|
type UpdateUserCodeDiffDisplayModeParams struct {
|
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
|
CodeDiffDisplayMode string `db:"code_diff_display_mode" json:"code_diff_display_mode"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateUserCodeDiffDisplayMode(ctx context.Context, arg UpdateUserCodeDiffDisplayModeParams) (string, error) {
|
|
row := q.db.QueryRowContext(ctx, updateUserCodeDiffDisplayMode, arg.UserID, arg.CodeDiffDisplayMode)
|
|
var code_diff_display_mode string
|
|
err := row.Scan(&code_diff_display_mode)
|
|
return code_diff_display_mode, err
|
|
}
|
|
|
|
const updateUserDeletedByID = `-- name: UpdateUserDeletedByID :exec
|
|
UPDATE
|
|
users
|
|
SET
|
|
deleted = true
|
|
WHERE
|
|
id = $1
|
|
`
|
|
|
|
func (q *sqlQuerier) UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) error {
|
|
_, err := q.db.ExecContext(ctx, updateUserDeletedByID, id)
|
|
return err
|
|
}
|
|
|
|
const updateUserGithubComUserID = `-- name: UpdateUserGithubComUserID :exec
|
|
UPDATE
|
|
users
|
|
SET
|
|
github_com_user_id = $2
|
|
WHERE
|
|
id = $1
|
|
`
|
|
|
|
type UpdateUserGithubComUserIDParams struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
GithubComUserID sql.NullInt64 `db:"github_com_user_id" json:"github_com_user_id"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateUserGithubComUserID(ctx context.Context, arg UpdateUserGithubComUserIDParams) error {
|
|
_, err := q.db.ExecContext(ctx, updateUserGithubComUserID, arg.ID, arg.GithubComUserID)
|
|
return err
|
|
}
|
|
|
|
const updateUserHashedOneTimePasscode = `-- name: UpdateUserHashedOneTimePasscode :exec
|
|
UPDATE
|
|
users
|
|
SET
|
|
hashed_one_time_passcode = $2,
|
|
one_time_passcode_expires_at = $3
|
|
WHERE
|
|
id = $1
|
|
`
|
|
|
|
type UpdateUserHashedOneTimePasscodeParams struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
HashedOneTimePasscode []byte `db:"hashed_one_time_passcode" json:"hashed_one_time_passcode"`
|
|
OneTimePasscodeExpiresAt sql.NullTime `db:"one_time_passcode_expires_at" json:"one_time_passcode_expires_at"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateUserHashedOneTimePasscode(ctx context.Context, arg UpdateUserHashedOneTimePasscodeParams) error {
|
|
_, err := q.db.ExecContext(ctx, updateUserHashedOneTimePasscode, arg.ID, arg.HashedOneTimePasscode, arg.OneTimePasscodeExpiresAt)
|
|
return err
|
|
}
|
|
|
|
const updateUserHashedPassword = `-- name: UpdateUserHashedPassword :exec
|
|
UPDATE
|
|
users
|
|
SET
|
|
hashed_password = $2,
|
|
hashed_one_time_passcode = NULL,
|
|
one_time_passcode_expires_at = NULL
|
|
WHERE
|
|
id = $1
|
|
`
|
|
|
|
type UpdateUserHashedPasswordParams struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
HashedPassword []byte `db:"hashed_password" json:"hashed_password"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateUserHashedPassword(ctx context.Context, arg UpdateUserHashedPasswordParams) error {
|
|
_, err := q.db.ExecContext(ctx, updateUserHashedPassword, arg.ID, arg.HashedPassword)
|
|
return err
|
|
}
|
|
|
|
const updateUserLastSeenAt = `-- name: UpdateUserLastSeenAt :one
|
|
UPDATE
|
|
users
|
|
SET
|
|
last_seen_at = $2,
|
|
updated_at = $3
|
|
WHERE
|
|
id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, is_service_account, chat_spend_limit_micros
|
|
`
|
|
|
|
type UpdateUserLastSeenAtParams struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
LastSeenAt time.Time `db:"last_seen_at" json:"last_seen_at"`
|
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateUserLastSeenAt(ctx context.Context, arg UpdateUserLastSeenAtParams) (User, error) {
|
|
row := q.db.QueryRowContext(ctx, updateUserLastSeenAt, arg.ID, arg.LastSeenAt, arg.UpdatedAt)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Email,
|
|
&i.Username,
|
|
&i.HashedPassword,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Status,
|
|
&i.RBACRoles,
|
|
&i.LoginType,
|
|
&i.AvatarURL,
|
|
&i.Deleted,
|
|
&i.LastSeenAt,
|
|
&i.QuietHoursSchedule,
|
|
&i.Name,
|
|
&i.GithubComUserID,
|
|
&i.HashedOneTimePasscode,
|
|
&i.OneTimePasscodeExpiresAt,
|
|
&i.IsSystem,
|
|
&i.IsServiceAccount,
|
|
&i.ChatSpendLimitMicros,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const updateUserLoginType = `-- name: UpdateUserLoginType :one
|
|
UPDATE
|
|
users
|
|
SET
|
|
login_type = $1,
|
|
hashed_password = CASE WHEN $1 = 'password' :: login_type THEN
|
|
users.hashed_password
|
|
ELSE
|
|
-- If the login type is not password, then the password should be
|
|
-- cleared.
|
|
'':: bytea
|
|
END
|
|
WHERE
|
|
id = $2
|
|
AND NOT is_system
|
|
RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, is_service_account, chat_spend_limit_micros
|
|
`
|
|
|
|
type UpdateUserLoginTypeParams struct {
|
|
NewLoginType LoginType `db:"new_login_type" json:"new_login_type"`
|
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateUserLoginType(ctx context.Context, arg UpdateUserLoginTypeParams) (User, error) {
|
|
row := q.db.QueryRowContext(ctx, updateUserLoginType, arg.NewLoginType, arg.UserID)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Email,
|
|
&i.Username,
|
|
&i.HashedPassword,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Status,
|
|
&i.RBACRoles,
|
|
&i.LoginType,
|
|
&i.AvatarURL,
|
|
&i.Deleted,
|
|
&i.LastSeenAt,
|
|
&i.QuietHoursSchedule,
|
|
&i.Name,
|
|
&i.GithubComUserID,
|
|
&i.HashedOneTimePasscode,
|
|
&i.OneTimePasscodeExpiresAt,
|
|
&i.IsSystem,
|
|
&i.IsServiceAccount,
|
|
&i.ChatSpendLimitMicros,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const updateUserProfile = `-- name: UpdateUserProfile :one
|
|
UPDATE
|
|
users
|
|
SET
|
|
email = $2,
|
|
username = $3,
|
|
avatar_url = $4,
|
|
updated_at = $5,
|
|
name = $6
|
|
WHERE
|
|
id = $1
|
|
RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, is_service_account, chat_spend_limit_micros
|
|
`
|
|
|
|
type UpdateUserProfileParams struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
Email string `db:"email" json:"email"`
|
|
Username string `db:"username" json:"username"`
|
|
AvatarURL string `db:"avatar_url" json:"avatar_url"`
|
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
|
Name string `db:"name" json:"name"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) (User, error) {
|
|
row := q.db.QueryRowContext(ctx, updateUserProfile,
|
|
arg.ID,
|
|
arg.Email,
|
|
arg.Username,
|
|
arg.AvatarURL,
|
|
arg.UpdatedAt,
|
|
arg.Name,
|
|
)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Email,
|
|
&i.Username,
|
|
&i.HashedPassword,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Status,
|
|
&i.RBACRoles,
|
|
&i.LoginType,
|
|
&i.AvatarURL,
|
|
&i.Deleted,
|
|
&i.LastSeenAt,
|
|
&i.QuietHoursSchedule,
|
|
&i.Name,
|
|
&i.GithubComUserID,
|
|
&i.HashedOneTimePasscode,
|
|
&i.OneTimePasscodeExpiresAt,
|
|
&i.IsSystem,
|
|
&i.IsServiceAccount,
|
|
&i.ChatSpendLimitMicros,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const updateUserQuietHoursSchedule = `-- name: UpdateUserQuietHoursSchedule :one
|
|
UPDATE
|
|
users
|
|
SET
|
|
quiet_hours_schedule = $2
|
|
WHERE
|
|
id = $1
|
|
RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, is_service_account, chat_spend_limit_micros
|
|
`
|
|
|
|
type UpdateUserQuietHoursScheduleParams struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
QuietHoursSchedule string `db:"quiet_hours_schedule" json:"quiet_hours_schedule"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateUserQuietHoursSchedule(ctx context.Context, arg UpdateUserQuietHoursScheduleParams) (User, error) {
|
|
row := q.db.QueryRowContext(ctx, updateUserQuietHoursSchedule, arg.ID, arg.QuietHoursSchedule)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Email,
|
|
&i.Username,
|
|
&i.HashedPassword,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Status,
|
|
&i.RBACRoles,
|
|
&i.LoginType,
|
|
&i.AvatarURL,
|
|
&i.Deleted,
|
|
&i.LastSeenAt,
|
|
&i.QuietHoursSchedule,
|
|
&i.Name,
|
|
&i.GithubComUserID,
|
|
&i.HashedOneTimePasscode,
|
|
&i.OneTimePasscodeExpiresAt,
|
|
&i.IsSystem,
|
|
&i.IsServiceAccount,
|
|
&i.ChatSpendLimitMicros,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const updateUserRoles = `-- name: UpdateUserRoles :one
|
|
UPDATE
|
|
users
|
|
SET
|
|
-- Remove all duplicates from the roles.
|
|
rbac_roles = ARRAY(SELECT DISTINCT UNNEST($1 :: text[]))
|
|
WHERE
|
|
id = $2
|
|
RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, is_service_account, chat_spend_limit_micros
|
|
`
|
|
|
|
type UpdateUserRolesParams struct {
|
|
GrantedRoles []string `db:"granted_roles" json:"granted_roles"`
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateUserRoles(ctx context.Context, arg UpdateUserRolesParams) (User, error) {
|
|
row := q.db.QueryRowContext(ctx, updateUserRoles, pq.Array(arg.GrantedRoles), arg.ID)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Email,
|
|
&i.Username,
|
|
&i.HashedPassword,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Status,
|
|
&i.RBACRoles,
|
|
&i.LoginType,
|
|
&i.AvatarURL,
|
|
&i.Deleted,
|
|
&i.LastSeenAt,
|
|
&i.QuietHoursSchedule,
|
|
&i.Name,
|
|
&i.GithubComUserID,
|
|
&i.HashedOneTimePasscode,
|
|
&i.OneTimePasscodeExpiresAt,
|
|
&i.IsSystem,
|
|
&i.IsServiceAccount,
|
|
&i.ChatSpendLimitMicros,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const updateUserShellToolDisplayMode = `-- name: UpdateUserShellToolDisplayMode :one
|
|
INSERT INTO
|
|
user_configs (user_id, key, value)
|
|
VALUES
|
|
($1, 'preference_shell_tool_display_mode', $2::text)
|
|
ON CONFLICT
|
|
ON CONSTRAINT user_configs_pkey
|
|
DO UPDATE
|
|
SET
|
|
value = $2
|
|
WHERE user_configs.user_id = $1
|
|
AND user_configs.key = 'preference_shell_tool_display_mode'
|
|
RETURNING value AS shell_tool_display_mode
|
|
`
|
|
|
|
type UpdateUserShellToolDisplayModeParams struct {
|
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
|
ShellToolDisplayMode string `db:"shell_tool_display_mode" json:"shell_tool_display_mode"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateUserShellToolDisplayMode(ctx context.Context, arg UpdateUserShellToolDisplayModeParams) (string, error) {
|
|
row := q.db.QueryRowContext(ctx, updateUserShellToolDisplayMode, arg.UserID, arg.ShellToolDisplayMode)
|
|
var shell_tool_display_mode string
|
|
err := row.Scan(&shell_tool_display_mode)
|
|
return shell_tool_display_mode, err
|
|
}
|
|
|
|
const updateUserStatus = `-- name: UpdateUserStatus :one
|
|
UPDATE
|
|
users
|
|
SET
|
|
status = $2,
|
|
updated_at = $3,
|
|
-- If the user is logging in, set last_seen_at to updated_at.
|
|
last_seen_at = CASE WHEN $4 :: boolean THEN $3 :: timestamptz ELSE last_seen_at END
|
|
WHERE
|
|
id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, is_service_account, chat_spend_limit_micros
|
|
`
|
|
|
|
type UpdateUserStatusParams struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
Status UserStatus `db:"status" json:"status"`
|
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
|
UserIsSeen bool `db:"user_is_seen" json:"user_is_seen"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusParams) (User, error) {
|
|
row := q.db.QueryRowContext(ctx, updateUserStatus,
|
|
arg.ID,
|
|
arg.Status,
|
|
arg.UpdatedAt,
|
|
arg.UserIsSeen,
|
|
)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Email,
|
|
&i.Username,
|
|
&i.HashedPassword,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Status,
|
|
&i.RBACRoles,
|
|
&i.LoginType,
|
|
&i.AvatarURL,
|
|
&i.Deleted,
|
|
&i.LastSeenAt,
|
|
&i.QuietHoursSchedule,
|
|
&i.Name,
|
|
&i.GithubComUserID,
|
|
&i.HashedOneTimePasscode,
|
|
&i.OneTimePasscodeExpiresAt,
|
|
&i.IsSystem,
|
|
&i.IsServiceAccount,
|
|
&i.ChatSpendLimitMicros,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const updateUserTaskNotificationAlertDismissed = `-- name: UpdateUserTaskNotificationAlertDismissed :one
|
|
INSERT INTO
|
|
user_configs (user_id, key, value)
|
|
VALUES
|
|
($1, 'preference_task_notification_alert_dismissed', ($2::boolean)::text)
|
|
ON CONFLICT
|
|
ON CONSTRAINT user_configs_pkey
|
|
DO UPDATE
|
|
SET
|
|
value = $2
|
|
WHERE user_configs.user_id = $1
|
|
AND user_configs.key = 'preference_task_notification_alert_dismissed'
|
|
RETURNING value::boolean AS task_notification_alert_dismissed
|
|
`
|
|
|
|
type UpdateUserTaskNotificationAlertDismissedParams struct {
|
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
|
TaskNotificationAlertDismissed bool `db:"task_notification_alert_dismissed" json:"task_notification_alert_dismissed"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateUserTaskNotificationAlertDismissed(ctx context.Context, arg UpdateUserTaskNotificationAlertDismissedParams) (bool, error) {
|
|
row := q.db.QueryRowContext(ctx, updateUserTaskNotificationAlertDismissed, arg.UserID, arg.TaskNotificationAlertDismissed)
|
|
var task_notification_alert_dismissed bool
|
|
err := row.Scan(&task_notification_alert_dismissed)
|
|
return task_notification_alert_dismissed, err
|
|
}
|
|
|
|
const updateUserTerminalFont = `-- name: UpdateUserTerminalFont :one
|
|
INSERT INTO
|
|
user_configs (user_id, key, value)
|
|
VALUES
|
|
($1, 'terminal_font', $2)
|
|
ON CONFLICT
|
|
ON CONSTRAINT user_configs_pkey
|
|
DO UPDATE
|
|
SET
|
|
value = $2
|
|
WHERE user_configs.user_id = $1
|
|
AND user_configs.key = 'terminal_font'
|
|
RETURNING user_id, key, value
|
|
`
|
|
|
|
type UpdateUserTerminalFontParams struct {
|
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
|
TerminalFont string `db:"terminal_font" json:"terminal_font"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateUserTerminalFont(ctx context.Context, arg UpdateUserTerminalFontParams) (UserConfig, error) {
|
|
row := q.db.QueryRowContext(ctx, updateUserTerminalFont, arg.UserID, arg.TerminalFont)
|
|
var i UserConfig
|
|
err := row.Scan(&i.UserID, &i.Key, &i.Value)
|
|
return i, err
|
|
}
|
|
|
|
const updateUserThemeDark = `-- name: UpdateUserThemeDark :one
|
|
INSERT INTO
|
|
user_configs (user_id, key, value)
|
|
VALUES
|
|
($1, 'theme_dark', $2)
|
|
ON CONFLICT
|
|
ON CONSTRAINT user_configs_pkey
|
|
DO UPDATE
|
|
SET
|
|
value = $2
|
|
WHERE user_configs.user_id = $1
|
|
AND user_configs.key = 'theme_dark'
|
|
RETURNING user_id, key, value
|
|
`
|
|
|
|
type UpdateUserThemeDarkParams struct {
|
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
|
ThemeDark string `db:"theme_dark" json:"theme_dark"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateUserThemeDark(ctx context.Context, arg UpdateUserThemeDarkParams) (UserConfig, error) {
|
|
row := q.db.QueryRowContext(ctx, updateUserThemeDark, arg.UserID, arg.ThemeDark)
|
|
var i UserConfig
|
|
err := row.Scan(&i.UserID, &i.Key, &i.Value)
|
|
return i, err
|
|
}
|
|
|
|
const updateUserThemeLight = `-- name: UpdateUserThemeLight :one
|
|
INSERT INTO
|
|
user_configs (user_id, key, value)
|
|
VALUES
|
|
($1, 'theme_light', $2)
|
|
ON CONFLICT
|
|
ON CONSTRAINT user_configs_pkey
|
|
DO UPDATE
|
|
SET
|
|
value = $2
|
|
WHERE user_configs.user_id = $1
|
|
AND user_configs.key = 'theme_light'
|
|
RETURNING user_id, key, value
|
|
`
|
|
|
|
type UpdateUserThemeLightParams struct {
|
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
|
ThemeLight string `db:"theme_light" json:"theme_light"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateUserThemeLight(ctx context.Context, arg UpdateUserThemeLightParams) (UserConfig, error) {
|
|
row := q.db.QueryRowContext(ctx, updateUserThemeLight, arg.UserID, arg.ThemeLight)
|
|
var i UserConfig
|
|
err := row.Scan(&i.UserID, &i.Key, &i.Value)
|
|
return i, err
|
|
}
|
|
|
|
const updateUserThemeMode = `-- name: UpdateUserThemeMode :one
|
|
INSERT INTO
|
|
user_configs (user_id, key, value)
|
|
VALUES
|
|
($1, 'theme_mode', $2)
|
|
ON CONFLICT
|
|
ON CONSTRAINT user_configs_pkey
|
|
DO UPDATE
|
|
SET
|
|
value = $2
|
|
WHERE user_configs.user_id = $1
|
|
AND user_configs.key = 'theme_mode'
|
|
RETURNING user_id, key, value
|
|
`
|
|
|
|
type UpdateUserThemeModeParams struct {
|
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
|
ThemeMode string `db:"theme_mode" json:"theme_mode"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateUserThemeMode(ctx context.Context, arg UpdateUserThemeModeParams) (UserConfig, error) {
|
|
row := q.db.QueryRowContext(ctx, updateUserThemeMode, arg.UserID, arg.ThemeMode)
|
|
var i UserConfig
|
|
err := row.Scan(&i.UserID, &i.Key, &i.Value)
|
|
return i, err
|
|
}
|
|
|
|
const updateUserThemePreference = `-- name: UpdateUserThemePreference :one
|
|
INSERT INTO
|
|
user_configs (user_id, key, value)
|
|
VALUES
|
|
($1, 'theme_preference', $2)
|
|
ON CONFLICT
|
|
ON CONSTRAINT user_configs_pkey
|
|
DO UPDATE
|
|
SET
|
|
value = $2
|
|
WHERE user_configs.user_id = $1
|
|
AND user_configs.key = 'theme_preference'
|
|
RETURNING user_id, key, value
|
|
`
|
|
|
|
type UpdateUserThemePreferenceParams struct {
|
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
|
ThemePreference string `db:"theme_preference" json:"theme_preference"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateUserThemePreference(ctx context.Context, arg UpdateUserThemePreferenceParams) (UserConfig, error) {
|
|
row := q.db.QueryRowContext(ctx, updateUserThemePreference, arg.UserID, arg.ThemePreference)
|
|
var i UserConfig
|
|
err := row.Scan(&i.UserID, &i.Key, &i.Value)
|
|
return i, err
|
|
}
|
|
|
|
const updateUserThinkingDisplayMode = `-- name: UpdateUserThinkingDisplayMode :one
|
|
INSERT INTO
|
|
user_configs (user_id, key, value)
|
|
VALUES
|
|
($1, 'preference_thinking_display_mode', $2::text)
|
|
ON CONFLICT
|
|
ON CONSTRAINT user_configs_pkey
|
|
DO UPDATE
|
|
SET
|
|
value = $2
|
|
WHERE user_configs.user_id = $1
|
|
AND user_configs.key = 'preference_thinking_display_mode'
|
|
RETURNING value AS thinking_display_mode
|
|
`
|
|
|
|
type UpdateUserThinkingDisplayModeParams struct {
|
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
|
ThinkingDisplayMode string `db:"thinking_display_mode" json:"thinking_display_mode"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpdateUserThinkingDisplayMode(ctx context.Context, arg UpdateUserThinkingDisplayModeParams) (string, error) {
|
|
row := q.db.QueryRowContext(ctx, updateUserThinkingDisplayMode, arg.UserID, arg.ThinkingDisplayMode)
|
|
var thinking_display_mode string
|
|
err := row.Scan(&thinking_display_mode)
|
|
return thinking_display_mode, err
|
|
}
|
|
|
|
const upsertUserChatDebugLoggingEnabled = `-- name: UpsertUserChatDebugLoggingEnabled :exec
|
|
INSERT INTO user_configs (user_id, key, value)
|
|
VALUES (
|
|
$1,
|
|
'chat_debug_logging_enabled',
|
|
CASE
|
|
WHEN $2::bool THEN 'true'
|
|
ELSE 'false'
|
|
END
|
|
)
|
|
ON CONFLICT ON CONSTRAINT user_configs_pkey
|
|
DO UPDATE SET value = CASE
|
|
WHEN $2::bool THEN 'true'
|
|
ELSE 'false'
|
|
END
|
|
WHERE user_configs.user_id = $1
|
|
AND user_configs.key = 'chat_debug_logging_enabled'
|
|
`
|
|
|
|
type UpsertUserChatDebugLoggingEnabledParams struct {
|
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
|
DebugLoggingEnabled bool `db:"debug_logging_enabled" json:"debug_logging_enabled"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpsertUserChatDebugLoggingEnabled(ctx context.Context, arg UpsertUserChatDebugLoggingEnabledParams) error {
|
|
_, err := q.db.ExecContext(ctx, upsertUserChatDebugLoggingEnabled, arg.UserID, arg.DebugLoggingEnabled)
|
|
return err
|
|
}
|
|
|
|
const upsertUserChatPersonalModelOverride = `-- name: UpsertUserChatPersonalModelOverride :exec
|
|
INSERT INTO user_configs (user_id, key, value)
|
|
VALUES ($1::uuid, $2::text, $3::text)
|
|
ON CONFLICT ON CONSTRAINT user_configs_pkey
|
|
DO UPDATE SET value = $3::text
|
|
`
|
|
|
|
type UpsertUserChatPersonalModelOverrideParams struct {
|
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
|
Key string `db:"key" json:"key"`
|
|
Value string `db:"value" json:"value"`
|
|
}
|
|
|
|
func (q *sqlQuerier) UpsertUserChatPersonalModelOverride(ctx context.Context, arg UpsertUserChatPersonalModelOverrideParams) error {
|
|
_, err := q.db.ExecContext(ctx, upsertUserChatPersonalModelOverride, arg.UserID, arg.Key, arg.Value)
|
|
return err
|
|
}
|
|
|
|
const validateUserIDs = `-- name: ValidateUserIDs :one
|
|
WITH input AS (
|
|
SELECT
|
|
unnest($1::uuid[]) AS id
|
|
)
|
|
SELECT
|
|
array_agg(input.id)::uuid[] as invalid_user_ids,
|
|
COUNT(*) = 0 as ok
|
|
FROM
|
|
-- Preserve rows where there is not a matching left (users) row for each
|
|
-- right (input) row...
|
|
users
|
|
RIGHT JOIN input ON users.id = input.id
|
|
WHERE
|
|
-- ...so that we can retain exactly those rows where an input ID does not
|
|
-- match an existing user...
|
|
users.id IS NULL OR
|
|
-- ...or that only matches a user that was deleted.
|
|
users.deleted = true
|
|
`
|
|
|
|
type ValidateUserIDsRow struct {
|
|
InvalidUserIds []uuid.UUID `db:"invalid_user_ids" json:"invalid_user_ids"`
|
|
Ok bool `db:"ok" json:"ok"`
|
|
}
|
|
|
|
func (q *sqlQuerier) ValidateUserIDs(ctx context.Context, userIds []uuid.UUID) (ValidateUserIDsRow, error) {
|
|
row := q.db.QueryRowContext(ctx, validateUserIDs, pq.Array(userIds))
|
|
var i ValidateUserIDsRow
|
|
err := row.Scan(pq.Array(&i.InvalidUserIds), &i.Ok)
|
|
return i, err
|
|
}
|
|
|
|
const 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 getExternalAgentTokensByTemplateID = `-- name: GetExternalAgentTokensByTemplateID :many
|
|
SELECT
|
|
workspaces.id AS workspace_id,
|
|
workspaces.name AS workspace_name,
|
|
workspace_agents.id AS agent_id,
|
|
workspace_agents.name AS agent_name,
|
|
workspace_agents.auth_token AS agent_token
|
|
FROM
|
|
workspaces
|
|
JOIN (
|
|
-- latest build per workspace
|
|
SELECT DISTINCT ON (workspace_id)
|
|
id, workspace_id, job_id, transition, has_external_agent
|
|
FROM
|
|
workspace_builds
|
|
ORDER BY
|
|
workspace_id, build_number DESC
|
|
) AS latest_builds
|
|
ON
|
|
latest_builds.workspace_id = workspaces.id
|
|
JOIN
|
|
provisioner_jobs
|
|
ON
|
|
provisioner_jobs.id = latest_builds.job_id
|
|
JOIN
|
|
workspace_resources
|
|
ON
|
|
workspace_resources.job_id = latest_builds.job_id
|
|
JOIN
|
|
workspace_agents
|
|
ON
|
|
workspace_agents.resource_id = workspace_resources.id
|
|
WHERE
|
|
workspaces.template_id = $1
|
|
AND (
|
|
$2 :: uuid = '00000000-0000-0000-0000-000000000000' :: uuid
|
|
OR workspaces.owner_id = $2
|
|
)
|
|
AND workspaces.deleted = FALSE
|
|
AND latest_builds.has_external_agent = TRUE
|
|
AND latest_builds.transition = 'start' :: workspace_transition
|
|
AND provisioner_jobs.job_status = 'succeeded' :: provisioner_job_status
|
|
AND workspace_agents.deleted = FALSE
|
|
AND workspace_agents.auth_instance_id IS NULL
|
|
`
|
|
|
|
type GetExternalAgentTokensByTemplateIDParams struct {
|
|
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
|
|
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
|
|
}
|
|
|
|
type GetExternalAgentTokensByTemplateIDRow struct {
|
|
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
|
|
WorkspaceName string `db:"workspace_name" json:"workspace_name"`
|
|
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
|
|
AgentName string `db:"agent_name" json:"agent_name"`
|
|
AgentToken uuid.UUID `db:"agent_token" json:"agent_token"`
|
|
}
|
|
|
|
// GetExternalAgentTokensByTemplateID returns the auth tokens for all
|
|
// non-deleted external agents on the latest build of every running workspace
|
|
// of the given template. "Running" means the latest build has
|
|
// transition=start and job_status=succeeded (matches the workspace-status
|
|
// definition used by coderd/database/queries/workspaces.sql).
|
|
// An owner_id of '00000000-0000-0000-0000-000000000000' (uuid.Nil) means
|
|
// "all owners"; any other value restricts results to workspaces owned by
|
|
// that user.
|
|
func (q *sqlQuerier) GetExternalAgentTokensByTemplateID(ctx context.Context, arg GetExternalAgentTokensByTemplateIDParams) ([]GetExternalAgentTokensByTemplateIDRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, getExternalAgentTokensByTemplateID, arg.TemplateID, arg.OwnerID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []GetExternalAgentTokensByTemplateIDRow
|
|
for rows.Next() {
|
|
var i GetExternalAgentTokensByTemplateIDRow
|
|
if err := rows.Scan(
|
|
&i.WorkspaceID,
|
|
&i.WorkspaceName,
|
|
&i.AgentID,
|
|
&i.AgentName,
|
|
&i.AgentToken,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getWorkspaceAgentAndWorkspaceByID = `-- name: GetWorkspaceAgentAndWorkspaceByID :one
|
|
SELECT
|
|
workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order, workspace_agents.parent_id, workspace_agents.api_key_scope, workspace_agents.deleted,
|
|
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at, workspaces.group_acl, workspaces.user_acl,
|
|
users.username as owner_username
|
|
FROM
|
|
workspace_agents
|
|
JOIN
|
|
workspace_resources ON workspace_agents.resource_id = workspace_resources.id
|
|
JOIN
|
|
provisioner_jobs ON workspace_resources.job_id = provisioner_jobs.id
|
|
JOIN
|
|
workspace_builds ON provisioner_jobs.id = workspace_builds.job_id
|
|
JOIN
|
|
workspaces ON workspace_builds.workspace_id = workspaces.id
|
|
JOIN
|
|
users ON workspaces.owner_id = users.id
|
|
WHERE
|
|
workspace_agents.id = $1
|
|
AND workspace_agents.deleted = FALSE
|
|
AND provisioner_jobs.type = 'workspace_build'::provisioner_job_type
|
|
AND workspaces.deleted = FALSE
|
|
AND users.deleted = FALSE
|
|
LIMIT 1
|
|
`
|
|
|
|
type GetWorkspaceAgentAndWorkspaceByIDRow struct {
|
|
WorkspaceAgent WorkspaceAgent `db:"workspace_agent" json:"workspace_agent"`
|
|
WorkspaceTable WorkspaceTable `db:"workspace_table" json:"workspace_table"`
|
|
OwnerUsername string `db:"owner_username" json:"owner_username"`
|
|
}
|
|
|
|
func (q *sqlQuerier) GetWorkspaceAgentAndWorkspaceByID(ctx context.Context, id uuid.UUID) (GetWorkspaceAgentAndWorkspaceByIDRow, error) {
|
|
row := q.db.QueryRowContext(ctx, getWorkspaceAgentAndWorkspaceByID, id)
|
|
var i GetWorkspaceAgentAndWorkspaceByIDRow
|
|
err := row.Scan(
|
|
&i.WorkspaceAgent.ID,
|
|
&i.WorkspaceAgent.CreatedAt,
|
|
&i.WorkspaceAgent.UpdatedAt,
|
|
&i.WorkspaceAgent.Name,
|
|
&i.WorkspaceAgent.FirstConnectedAt,
|
|
&i.WorkspaceAgent.LastConnectedAt,
|
|
&i.WorkspaceAgent.DisconnectedAt,
|
|
&i.WorkspaceAgent.ResourceID,
|
|
&i.WorkspaceAgent.AuthToken,
|
|
&i.WorkspaceAgent.AuthInstanceID,
|
|
&i.WorkspaceAgent.Architecture,
|
|
&i.WorkspaceAgent.EnvironmentVariables,
|
|
&i.WorkspaceAgent.OperatingSystem,
|
|
&i.WorkspaceAgent.InstanceMetadata,
|
|
&i.WorkspaceAgent.ResourceMetadata,
|
|
&i.WorkspaceAgent.Directory,
|
|
&i.WorkspaceAgent.Version,
|
|
&i.WorkspaceAgent.LastConnectedReplicaID,
|
|
&i.WorkspaceAgent.ConnectionTimeoutSeconds,
|
|
&i.WorkspaceAgent.TroubleshootingURL,
|
|
&i.WorkspaceAgent.MOTDFile,
|
|
&i.WorkspaceAgent.LifecycleState,
|
|
&i.WorkspaceAgent.ExpandedDirectory,
|
|
&i.WorkspaceAgent.LogsLength,
|
|
&i.WorkspaceAgent.LogsOverflowed,
|
|
&i.WorkspaceAgent.StartedAt,
|
|
&i.WorkspaceAgent.ReadyAt,
|
|
pq.Array(&i.WorkspaceAgent.Subsystems),
|
|
pq.Array(&i.WorkspaceAgent.DisplayApps),
|
|
&i.WorkspaceAgent.APIVersion,
|
|
&i.WorkspaceAgent.DisplayOrder,
|
|
&i.WorkspaceAgent.ParentID,
|
|
&i.WorkspaceAgent.APIKeyScope,
|
|
&i.WorkspaceAgent.Deleted,
|
|
&i.WorkspaceTable.ID,
|
|
&i.WorkspaceTable.CreatedAt,
|
|
&i.WorkspaceTable.UpdatedAt,
|
|
&i.WorkspaceTable.OwnerID,
|
|
&i.WorkspaceTable.OrganizationID,
|
|
&i.WorkspaceTable.TemplateID,
|
|
&i.WorkspaceTable.Deleted,
|
|
&i.WorkspaceTable.Name,
|
|
&i.WorkspaceTable.AutostartSchedule,
|
|
&i.WorkspaceTable.Ttl,
|
|
&i.WorkspaceTable.LastUsedAt,
|
|
&i.WorkspaceTable.DormantAt,
|
|
&i.WorkspaceTable.DeletingAt,
|
|
&i.WorkspaceTable.AutomaticUpdates,
|
|
&i.WorkspaceTable.Favorite,
|
|
&i.WorkspaceTable.NextStartAt,
|
|
&i.WorkspaceTable.GroupACL,
|
|
&i.WorkspaceTable.UserACL,
|
|
&i.OwnerUsername,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getWorkspaceAgentByID = `-- name: GetWorkspaceAgentByID :one
|
|
SELECT
|
|
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope, deleted
|
|
FROM
|
|
workspace_agents
|
|
WHERE
|
|
id = $1
|
|
-- Filter out deleted sub agents.
|
|
AND deleted = FALSE
|
|
`
|
|
|
|
func (q *sqlQuerier) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (WorkspaceAgent, error) {
|
|
row := q.db.QueryRowContext(ctx, getWorkspaceAgentByID, id)
|
|
var i WorkspaceAgent
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Name,
|
|
&i.FirstConnectedAt,
|
|
&i.LastConnectedAt,
|
|
&i.DisconnectedAt,
|
|
&i.ResourceID,
|
|
&i.AuthToken,
|
|
&i.AuthInstanceID,
|
|
&i.Architecture,
|
|
&i.EnvironmentVariables,
|
|
&i.OperatingSystem,
|
|
&i.InstanceMetadata,
|
|
&i.ResourceMetadata,
|
|
&i.Directory,
|
|
&i.Version,
|
|
&i.LastConnectedReplicaID,
|
|
&i.ConnectionTimeoutSeconds,
|
|
&i.TroubleshootingURL,
|
|
&i.MOTDFile,
|
|
&i.LifecycleState,
|
|
&i.ExpandedDirectory,
|
|
&i.LogsLength,
|
|
&i.LogsOverflowed,
|
|
&i.StartedAt,
|
|
&i.ReadyAt,
|
|
pq.Array(&i.Subsystems),
|
|
pq.Array(&i.DisplayApps),
|
|
&i.APIVersion,
|
|
&i.DisplayOrder,
|
|
&i.ParentID,
|
|
&i.APIKeyScope,
|
|
&i.Deleted,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getWorkspaceAgentLifecycleStateByID = `-- name: GetWorkspaceAgentLifecycleStateByID :one
|
|
SELECT
|
|
lifecycle_state,
|
|
started_at,
|
|
ready_at
|
|
FROM
|
|
workspace_agents
|
|
WHERE
|
|
id = $1
|
|
`
|
|
|
|
type GetWorkspaceAgentLifecycleStateByIDRow struct {
|
|
LifecycleState WorkspaceAgentLifecycleState `db:"lifecycle_state" json:"lifecycle_state"`
|
|
StartedAt sql.NullTime `db:"started_at" json:"started_at"`
|
|
ReadyAt sql.NullTime `db:"ready_at" json:"ready_at"`
|
|
}
|
|
|
|
func (q *sqlQuerier) GetWorkspaceAgentLifecycleStateByID(ctx context.Context, id uuid.UUID) (GetWorkspaceAgentLifecycleStateByIDRow, error) {
|
|
row := q.db.QueryRowContext(ctx, getWorkspaceAgentLifecycleStateByID, id)
|
|
var i GetWorkspaceAgentLifecycleStateByIDRow
|
|
err := row.Scan(&i.LifecycleState, &i.StartedAt, &i.ReadyAt)
|
|
return i, err
|
|
}
|
|
|
|
const getWorkspaceAgentLogSourcesByAgentIDs = `-- name: GetWorkspaceAgentLogSourcesByAgentIDs :many
|
|
SELECT workspace_agent_id, id, created_at, display_name, icon FROM workspace_agent_log_sources WHERE workspace_agent_id = ANY($1 :: uuid [ ])
|
|
`
|
|
|
|
func (q *sqlQuerier) GetWorkspaceAgentLogSourcesByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentLogSource, error) {
|
|
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentLogSourcesByAgentIDs, pq.Array(ids))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []WorkspaceAgentLogSource
|
|
for rows.Next() {
|
|
var i WorkspaceAgentLogSource
|
|
if err := rows.Scan(
|
|
&i.WorkspaceAgentID,
|
|
&i.ID,
|
|
&i.CreatedAt,
|
|
&i.DisplayName,
|
|
&i.Icon,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getWorkspaceAgentLogsAfter = `-- name: GetWorkspaceAgentLogsAfter :many
|
|
SELECT
|
|
agent_id, created_at, output, id, level, log_source_id
|
|
FROM
|
|
workspace_agent_logs
|
|
WHERE
|
|
agent_id = $1
|
|
AND (
|
|
id > $2
|
|
) ORDER BY id ASC
|
|
`
|
|
|
|
type GetWorkspaceAgentLogsAfterParams struct {
|
|
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
|
|
CreatedAfter int64 `db:"created_after" json:"created_after"`
|
|
}
|
|
|
|
func (q *sqlQuerier) GetWorkspaceAgentLogsAfter(ctx context.Context, arg GetWorkspaceAgentLogsAfterParams) ([]WorkspaceAgentLog, error) {
|
|
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentLogsAfter, arg.AgentID, arg.CreatedAfter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []WorkspaceAgentLog
|
|
for rows.Next() {
|
|
var i WorkspaceAgentLog
|
|
if err := rows.Scan(
|
|
&i.AgentID,
|
|
&i.CreatedAt,
|
|
&i.Output,
|
|
&i.ID,
|
|
&i.Level,
|
|
&i.LogSourceID,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getWorkspaceAgentMetadata = `-- name: GetWorkspaceAgentMetadata :many
|
|
SELECT
|
|
workspace_agent_id, display_name, key, script, value, error, timeout, interval, collected_at, display_order
|
|
FROM
|
|
workspace_agent_metadata
|
|
WHERE
|
|
workspace_agent_id = $1
|
|
AND CASE WHEN COALESCE(array_length($2::text[], 1), 0) > 0 THEN key = ANY($2::text[]) ELSE TRUE END
|
|
`
|
|
|
|
type GetWorkspaceAgentMetadataParams struct {
|
|
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
|
|
Keys []string `db:"keys" json:"keys"`
|
|
}
|
|
|
|
func (q *sqlQuerier) GetWorkspaceAgentMetadata(ctx context.Context, arg GetWorkspaceAgentMetadataParams) ([]WorkspaceAgentMetadatum, error) {
|
|
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentMetadata, arg.WorkspaceAgentID, pq.Array(arg.Keys))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []WorkspaceAgentMetadatum
|
|
for rows.Next() {
|
|
var i WorkspaceAgentMetadatum
|
|
if err := rows.Scan(
|
|
&i.WorkspaceAgentID,
|
|
&i.DisplayName,
|
|
&i.Key,
|
|
&i.Script,
|
|
&i.Value,
|
|
&i.Error,
|
|
&i.Timeout,
|
|
&i.Interval,
|
|
&i.CollectedAt,
|
|
&i.DisplayOrder,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getWorkspaceAgentScriptTimingsByBuildID = `-- name: GetWorkspaceAgentScriptTimingsByBuildID :many
|
|
SELECT
|
|
DISTINCT ON (workspace_agent_script_timings.script_id) workspace_agent_script_timings.script_id, workspace_agent_script_timings.started_at, workspace_agent_script_timings.ended_at, workspace_agent_script_timings.exit_code, workspace_agent_script_timings.stage, workspace_agent_script_timings.status,
|
|
workspace_agent_scripts.display_name,
|
|
workspace_agents.id as workspace_agent_id,
|
|
workspace_agents.name as workspace_agent_name
|
|
FROM workspace_agent_script_timings
|
|
INNER JOIN workspace_agent_scripts ON workspace_agent_scripts.id = workspace_agent_script_timings.script_id
|
|
INNER JOIN workspace_agents ON workspace_agents.id = workspace_agent_scripts.workspace_agent_id
|
|
INNER JOIN workspace_resources ON workspace_resources.id = workspace_agents.resource_id
|
|
INNER JOIN workspace_builds ON workspace_builds.job_id = workspace_resources.job_id
|
|
WHERE workspace_builds.id = $1
|
|
ORDER BY workspace_agent_script_timings.script_id, workspace_agent_script_timings.started_at
|
|
`
|
|
|
|
type GetWorkspaceAgentScriptTimingsByBuildIDRow struct {
|
|
ScriptID uuid.UUID `db:"script_id" json:"script_id"`
|
|
StartedAt time.Time `db:"started_at" json:"started_at"`
|
|
EndedAt time.Time `db:"ended_at" json:"ended_at"`
|
|
ExitCode int32 `db:"exit_code" json:"exit_code"`
|
|
Stage WorkspaceAgentScriptTimingStage `db:"stage" json:"stage"`
|
|
Status WorkspaceAgentScriptTimingStatus `db:"status" json:"status"`
|
|
DisplayName string `db:"display_name" json:"display_name"`
|
|
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
|
|
WorkspaceAgentName string `db:"workspace_agent_name" json:"workspace_agent_name"`
|
|
}
|
|
|
|
func (q *sqlQuerier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Context, id uuid.UUID) ([]GetWorkspaceAgentScriptTimingsByBuildIDRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentScriptTimingsByBuildID, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []GetWorkspaceAgentScriptTimingsByBuildIDRow
|
|
for rows.Next() {
|
|
var i GetWorkspaceAgentScriptTimingsByBuildIDRow
|
|
if err := rows.Scan(
|
|
&i.ScriptID,
|
|
&i.StartedAt,
|
|
&i.EndedAt,
|
|
&i.ExitCode,
|
|
&i.Stage,
|
|
&i.Status,
|
|
&i.DisplayName,
|
|
&i.WorkspaceAgentID,
|
|
&i.WorkspaceAgentName,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getWorkspaceAgentsByInstanceID = `-- name: GetWorkspaceAgentsByInstanceID :many
|
|
SELECT
|
|
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope, deleted
|
|
FROM
|
|
workspace_agents
|
|
WHERE
|
|
auth_instance_id = $1 :: TEXT
|
|
-- Filter out deleted agents.
|
|
AND deleted = FALSE
|
|
-- Filter out sub agents, they do not authenticate with auth_instance_id.
|
|
AND parent_id IS NULL
|
|
ORDER BY
|
|
created_at DESC
|
|
`
|
|
|
|
func (q *sqlQuerier) GetWorkspaceAgentsByInstanceID(ctx context.Context, authInstanceID string) ([]WorkspaceAgent, error) {
|
|
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsByInstanceID, authInstanceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []WorkspaceAgent
|
|
for rows.Next() {
|
|
var i WorkspaceAgent
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Name,
|
|
&i.FirstConnectedAt,
|
|
&i.LastConnectedAt,
|
|
&i.DisconnectedAt,
|
|
&i.ResourceID,
|
|
&i.AuthToken,
|
|
&i.AuthInstanceID,
|
|
&i.Architecture,
|
|
&i.EnvironmentVariables,
|
|
&i.OperatingSystem,
|
|
&i.InstanceMetadata,
|
|
&i.ResourceMetadata,
|
|
&i.Directory,
|
|
&i.Version,
|
|
&i.LastConnectedReplicaID,
|
|
&i.ConnectionTimeoutSeconds,
|
|
&i.TroubleshootingURL,
|
|
&i.MOTDFile,
|
|
&i.LifecycleState,
|
|
&i.ExpandedDirectory,
|
|
&i.LogsLength,
|
|
&i.LogsOverflowed,
|
|
&i.StartedAt,
|
|
&i.ReadyAt,
|
|
pq.Array(&i.Subsystems),
|
|
pq.Array(&i.DisplayApps),
|
|
&i.APIVersion,
|
|
&i.DisplayOrder,
|
|
&i.ParentID,
|
|
&i.APIKeyScope,
|
|
&i.Deleted,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getWorkspaceAgentsByParentID = `-- name: GetWorkspaceAgentsByParentID :many
|
|
SELECT
|
|
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope, deleted
|
|
FROM
|
|
workspace_agents
|
|
WHERE
|
|
parent_id = $1::uuid
|
|
AND deleted = FALSE
|
|
`
|
|
|
|
func (q *sqlQuerier) GetWorkspaceAgentsByParentID(ctx context.Context, parentID uuid.UUID) ([]WorkspaceAgent, error) {
|
|
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsByParentID, parentID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []WorkspaceAgent
|
|
for rows.Next() {
|
|
var i WorkspaceAgent
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Name,
|
|
&i.FirstConnectedAt,
|
|
&i.LastConnectedAt,
|
|
&i.DisconnectedAt,
|
|
&i.ResourceID,
|
|
&i.AuthToken,
|
|
&i.AuthInstanceID,
|
|
&i.Architecture,
|
|
&i.EnvironmentVariables,
|
|
&i.OperatingSystem,
|
|
&i.InstanceMetadata,
|
|
&i.ResourceMetadata,
|
|
&i.Directory,
|
|
&i.Version,
|
|
&i.LastConnectedReplicaID,
|
|
&i.ConnectionTimeoutSeconds,
|
|
&i.TroubleshootingURL,
|
|
&i.MOTDFile,
|
|
&i.LifecycleState,
|
|
&i.ExpandedDirectory,
|
|
&i.LogsLength,
|
|
&i.LogsOverflowed,
|
|
&i.StartedAt,
|
|
&i.ReadyAt,
|
|
pq.Array(&i.Subsystems),
|
|
pq.Array(&i.DisplayApps),
|
|
&i.APIVersion,
|
|
&i.DisplayOrder,
|
|
&i.ParentID,
|
|
&i.APIKeyScope,
|
|
&i.Deleted,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getWorkspaceAgentsByResourceIDs = `-- name: GetWorkspaceAgentsByResourceIDs :many
|
|
SELECT
|
|
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope, deleted
|
|
FROM
|
|
workspace_agents
|
|
WHERE
|
|
resource_id = ANY($1 :: uuid [ ])
|
|
-- Filter out deleted sub agents.
|
|
AND deleted = FALSE
|
|
`
|
|
|
|
func (q *sqlQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgent, error) {
|
|
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsByResourceIDs, pq.Array(ids))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []WorkspaceAgent
|
|
for rows.Next() {
|
|
var i WorkspaceAgent
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Name,
|
|
&i.FirstConnectedAt,
|
|
&i.LastConnectedAt,
|
|
&i.DisconnectedAt,
|
|
&i.ResourceID,
|
|
&i.AuthToken,
|
|
&i.AuthInstanceID,
|
|
&i.Architecture,
|
|
&i.EnvironmentVariables,
|
|
&i.OperatingSystem,
|
|
&i.InstanceMetadata,
|
|
&i.ResourceMetadata,
|
|
&i.Directory,
|
|
&i.Version,
|
|
&i.LastConnectedReplicaID,
|
|
&i.ConnectionTimeoutSeconds,
|
|
&i.TroubleshootingURL,
|
|
&i.MOTDFile,
|
|
&i.LifecycleState,
|
|
&i.ExpandedDirectory,
|
|
&i.LogsLength,
|
|
&i.LogsOverflowed,
|
|
&i.StartedAt,
|
|
&i.ReadyAt,
|
|
pq.Array(&i.Subsystems),
|
|
pq.Array(&i.DisplayApps),
|
|
&i.APIVersion,
|
|
&i.DisplayOrder,
|
|
&i.ParentID,
|
|
&i.APIKeyScope,
|
|
&i.Deleted,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getWorkspaceAgentsByWorkspaceAndBuildNumber = `-- name: GetWorkspaceAgentsByWorkspaceAndBuildNumber :many
|
|
SELECT
|
|
workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order, workspace_agents.parent_id, workspace_agents.api_key_scope, workspace_agents.deleted
|
|
FROM
|
|
workspace_agents
|
|
JOIN
|
|
workspace_resources ON workspace_agents.resource_id = workspace_resources.id
|
|
JOIN
|
|
workspace_builds ON workspace_resources.job_id = workspace_builds.job_id
|
|
WHERE
|
|
workspace_builds.workspace_id = $1 :: uuid AND
|
|
workspace_builds.build_number = $2 :: int
|
|
-- Filter out deleted sub agents.
|
|
AND workspace_agents.deleted = FALSE
|
|
`
|
|
|
|
type GetWorkspaceAgentsByWorkspaceAndBuildNumberParams struct {
|
|
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
|
|
BuildNumber int32 `db:"build_number" json:"build_number"`
|
|
}
|
|
|
|
func (q *sqlQuerier) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Context, arg GetWorkspaceAgentsByWorkspaceAndBuildNumberParams) ([]WorkspaceAgent, error) {
|
|
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsByWorkspaceAndBuildNumber, arg.WorkspaceID, arg.BuildNumber)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []WorkspaceAgent
|
|
for rows.Next() {
|
|
var i WorkspaceAgent
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Name,
|
|
&i.FirstConnectedAt,
|
|
&i.LastConnectedAt,
|
|
&i.DisconnectedAt,
|
|
&i.ResourceID,
|
|
&i.AuthToken,
|
|
&i.AuthInstanceID,
|
|
&i.Architecture,
|
|
&i.EnvironmentVariables,
|
|
&i.OperatingSystem,
|
|
&i.InstanceMetadata,
|
|
&i.ResourceMetadata,
|
|
&i.Directory,
|
|
&i.Version,
|
|
&i.LastConnectedReplicaID,
|
|
&i.ConnectionTimeoutSeconds,
|
|
&i.TroubleshootingURL,
|
|
&i.MOTDFile,
|
|
&i.LifecycleState,
|
|
&i.ExpandedDirectory,
|
|
&i.LogsLength,
|
|
&i.LogsOverflowed,
|
|
&i.StartedAt,
|
|
&i.ReadyAt,
|
|
pq.Array(&i.Subsystems),
|
|
pq.Array(&i.DisplayApps),
|
|
&i.APIVersion,
|
|
&i.DisplayOrder,
|
|
&i.ParentID,
|
|
&i.APIKeyScope,
|
|
&i.Deleted,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getWorkspaceAgentsCreatedAfter = `-- name: GetWorkspaceAgentsCreatedAfter :many
|
|
SELECT id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope, deleted FROM workspace_agents
|
|
WHERE
|
|
created_at > $1
|
|
-- Filter out deleted sub agents.
|
|
AND deleted = FALSE
|
|
`
|
|
|
|
func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceAgent, error) {
|
|
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsCreatedAfter, createdAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []WorkspaceAgent
|
|
for rows.Next() {
|
|
var i WorkspaceAgent
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Name,
|
|
&i.FirstConnectedAt,
|
|
&i.LastConnectedAt,
|
|
&i.DisconnectedAt,
|
|
&i.ResourceID,
|
|
&i.AuthToken,
|
|
&i.AuthInstanceID,
|
|
&i.Architecture,
|
|
&i.EnvironmentVariables,
|
|
&i.OperatingSystem,
|
|
&i.InstanceMetadata,
|
|
&i.ResourceMetadata,
|
|
&i.Directory,
|
|
&i.Version,
|
|
&i.LastConnectedReplicaID,
|
|
&i.ConnectionTimeoutSeconds,
|
|
&i.TroubleshootingURL,
|
|
&i.MOTDFile,
|
|
&i.LifecycleState,
|
|
&i.ExpandedDirectory,
|
|
&i.LogsLength,
|
|
&i.LogsOverflowed,
|
|
&i.StartedAt,
|
|
&i.ReadyAt,
|
|
pq.Array(&i.Subsystems),
|
|
pq.Array(&i.DisplayApps),
|
|
&i.APIVersion,
|
|
&i.DisplayOrder,
|
|
&i.ParentID,
|
|
&i.APIKeyScope,
|
|
&i.Deleted,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getWorkspaceAgentsForMetrics = `-- name: GetWorkspaceAgentsForMetrics :many
|
|
SELECT
|
|
w.id as workspace_id,
|
|
w.name as workspace_name,
|
|
u.username as owner_username,
|
|
t.name as template_name,
|
|
tv.name as template_version_name,
|
|
workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order, workspace_agents.parent_id, workspace_agents.api_key_scope, workspace_agents.deleted
|
|
FROM workspaces w
|
|
JOIN users u ON w.owner_id = u.id
|
|
JOIN templates t ON w.template_id = t.id
|
|
JOIN workspace_builds wb ON w.id = wb.workspace_id
|
|
LEFT JOIN template_versions tv ON wb.template_version_id = tv.id
|
|
JOIN workspace_resources wr ON wb.job_id = wr.job_id
|
|
JOIN workspace_agents ON wr.id = workspace_agents.resource_id
|
|
WHERE w.deleted = false
|
|
AND wb.build_number = (
|
|
SELECT MAX(wb2.build_number)
|
|
FROM workspace_builds wb2
|
|
WHERE wb2.workspace_id = w.id
|
|
)
|
|
AND workspace_agents.deleted = FALSE
|
|
`
|
|
|
|
type GetWorkspaceAgentsForMetricsRow struct {
|
|
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
|
|
WorkspaceName string `db:"workspace_name" json:"workspace_name"`
|
|
OwnerUsername string `db:"owner_username" json:"owner_username"`
|
|
TemplateName string `db:"template_name" json:"template_name"`
|
|
TemplateVersionName sql.NullString `db:"template_version_name" json:"template_version_name"`
|
|
WorkspaceAgent WorkspaceAgent `db:"workspace_agent" json:"workspace_agent"`
|
|
}
|
|
|
|
func (q *sqlQuerier) GetWorkspaceAgentsForMetrics(ctx context.Context) ([]GetWorkspaceAgentsForMetricsRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsForMetrics)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []GetWorkspaceAgentsForMetricsRow
|
|
for rows.Next() {
|
|
var i GetWorkspaceAgentsForMetricsRow
|
|
if err := rows.Scan(
|
|
&i.WorkspaceID,
|
|
&i.WorkspaceName,
|
|
&i.OwnerUsername,
|
|
&i.TemplateName,
|
|
&i.TemplateVersionName,
|
|
&i.WorkspaceAgent.ID,
|
|
&i.WorkspaceAgent.CreatedAt,
|
|
&i.WorkspaceAgent.UpdatedAt,
|
|
&i.WorkspaceAgent.Name,
|
|
&i.WorkspaceAgent.FirstConnectedAt,
|
|
&i.WorkspaceAgent.LastConnectedAt,
|
|
&i.WorkspaceAgent.DisconnectedAt,
|
|
&i.WorkspaceAgent.ResourceID,
|
|
&i.WorkspaceAgent.AuthToken,
|
|
&i.WorkspaceAgent.AuthInstanceID,
|
|
&i.WorkspaceAgent.Architecture,
|
|
&i.WorkspaceAgent.EnvironmentVariables,
|
|
&i.WorkspaceAgent.OperatingSystem,
|
|
&i.WorkspaceAgent.InstanceMetadata,
|
|
&i.WorkspaceAgent.ResourceMetadata,
|
|
&i.WorkspaceAgent.Directory,
|
|
&i.WorkspaceAgent.Version,
|
|
&i.WorkspaceAgent.LastConnectedReplicaID,
|
|
&i.WorkspaceAgent.ConnectionTimeoutSeconds,
|
|
&i.WorkspaceAgent.TroubleshootingURL,
|
|
&i.WorkspaceAgent.MOTDFile,
|
|
&i.WorkspaceAgent.LifecycleState,
|
|
&i.WorkspaceAgent.ExpandedDirectory,
|
|
&i.WorkspaceAgent.LogsLength,
|
|
&i.WorkspaceAgent.LogsOverflowed,
|
|
&i.WorkspaceAgent.StartedAt,
|
|
&i.WorkspaceAgent.ReadyAt,
|
|
pq.Array(&i.WorkspaceAgent.Subsystems),
|
|
pq.Array(&i.WorkspaceAgent.DisplayApps),
|
|
&i.WorkspaceAgent.APIVersion,
|
|
&i.WorkspaceAgent.DisplayOrder,
|
|
&i.WorkspaceAgent.ParentID,
|
|
&i.WorkspaceAgent.APIKeyScope,
|
|
&i.WorkspaceAgent.Deleted,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getWorkspaceAgentsInLatestBuildByWorkspaceID = `-- name: GetWorkspaceAgentsInLatestBuildByWorkspaceID :many
|
|
SELECT
|
|
workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order, workspace_agents.parent_id, workspace_agents.api_key_scope, workspace_agents.deleted
|
|
FROM
|
|
workspace_agents
|
|
JOIN
|
|
workspace_resources ON workspace_agents.resource_id = workspace_resources.id
|
|
JOIN
|
|
workspace_builds ON workspace_resources.job_id = workspace_builds.job_id
|
|
WHERE
|
|
workspace_builds.workspace_id = $1 :: uuid AND
|
|
workspace_builds.build_number = (
|
|
SELECT
|
|
MAX(build_number)
|
|
FROM
|
|
workspace_builds AS wb
|
|
WHERE
|
|
wb.workspace_id = $1 :: uuid
|
|
)
|
|
-- Filter out deleted sub agents.
|
|
AND workspace_agents.deleted = FALSE
|
|
`
|
|
|
|
func (q *sqlQuerier) GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgent, error) {
|
|
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsInLatestBuildByWorkspaceID, workspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []WorkspaceAgent
|
|
for rows.Next() {
|
|
var i WorkspaceAgent
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Name,
|
|
&i.FirstConnectedAt,
|
|
&i.LastConnectedAt,
|
|
&i.DisconnectedAt,
|
|
&i.ResourceID,
|
|
&i.AuthToken,
|
|
&i.AuthInstanceID,
|
|
&i.Architecture,
|
|
&i.EnvironmentVariables,
|
|
&i.OperatingSystem,
|
|
&i.InstanceMetadata,
|
|
&i.ResourceMetadata,
|
|
&i.Directory,
|
|
&i.Version,
|
|
&i.LastConnectedReplicaID,
|
|
&i.ConnectionTimeoutSeconds,
|
|
&i.TroubleshootingURL,
|
|
&i.MOTDFile,
|
|
&i.LifecycleState,
|
|
&i.ExpandedDirectory,
|
|
&i.LogsLength,
|
|
&i.LogsOverflowed,
|
|
&i.StartedAt,
|
|
&i.ReadyAt,
|
|
pq.Array(&i.Subsystems),
|
|
pq.Array(&i.DisplayApps),
|
|
&i.APIVersion,
|
|
&i.DisplayOrder,
|
|
&i.ParentID,
|
|
&i.APIKeyScope,
|
|
&i.Deleted,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const 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
|
|
}
|