Files
multica/server/pkg/db/generated/issue_property.sql.go
Lambda 76aaa4e3ee feat(server): custom issue properties — definitions, typed values, CLI (MUL-4463)
Workspace-level property definitions (issue_property table; 7 types:
text/number/select/multi_select/date/checkbox/url) plus a typed value bag
on each issue (issue.properties JSONB keyed by definition UUID, mirroring
the metadata machinery: single-key atomic writes, 16KB cap, GIN index).

- Definitions: owner/admin only; agent actors rejected (agents propose,
  humans confirm). 20 active per workspace, 50 options per select,
  reserved built-in names blocked, archive instead of delete.
- Values: any member or agent; per-type validation with self-correcting
  error messages that enumerate legal option ids.
- API: /api/properties CRUD + PUT/DELETE /api/issues/{id}/properties/{propertyId};
  issue responses always emit the properties bag.
- CLI: multica property list/get/create/update/archive/unarchive and
  multica issue property list/set/unset with name→id translation.
- Events: property:created/updated, issue_properties:changed.

Co-authored-by: multica-agent <github@multica.ai>
2026-07-14 17:29:00 +08:00

318 lines
8.9 KiB
Go

// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: issue_property.sql
package db
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const countActiveIssueProperties = `-- name: CountActiveIssueProperties :one
SELECT COUNT(*) FROM issue_property
WHERE workspace_id = $1 AND archived_at IS NULL
`
func (q *Queries) CountActiveIssueProperties(ctx context.Context, workspaceID pgtype.UUID) (int64, error) {
row := q.db.QueryRow(ctx, countActiveIssueProperties, workspaceID)
var count int64
err := row.Scan(&count)
return count, err
}
const createIssueProperty = `-- name: CreateIssueProperty :one
INSERT INTO issue_property (workspace_id, name, type, description, config, position)
SELECT $1::uuid,
$2::text,
$3::text,
$4::text,
$5::jsonb,
COALESCE((SELECT MAX(position) FROM issue_property WHERE workspace_id = $1::uuid), 0) + 1
RETURNING id, workspace_id, name, type, description, config, position, archived_at, created_at, updated_at
`
type CreateIssuePropertyParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
Name string `json:"name"`
Type string `json:"type"`
Description string `json:"description"`
Config []byte `json:"config"`
}
// New definitions append to the end of the catalog: position = max + 1.
func (q *Queries) CreateIssueProperty(ctx context.Context, arg CreateIssuePropertyParams) (IssueProperty, error) {
row := q.db.QueryRow(ctx, createIssueProperty,
arg.WorkspaceID,
arg.Name,
arg.Type,
arg.Description,
arg.Config,
)
var i IssueProperty
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.Name,
&i.Type,
&i.Description,
&i.Config,
&i.Position,
&i.ArchivedAt,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const deleteIssuePropertyValue = `-- name: DeleteIssuePropertyValue :one
UPDATE issue
SET properties = properties - $1::text,
updated_at = now()
WHERE id = $2::uuid AND workspace_id = $3::uuid
RETURNING id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at, start_date, metadata, stage, properties
`
type DeleteIssuePropertyValueParams struct {
Key string `json:"key"`
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
}
func (q *Queries) DeleteIssuePropertyValue(ctx context.Context, arg DeleteIssuePropertyValueParams) (Issue, error) {
row := q.db.QueryRow(ctx, deleteIssuePropertyValue, arg.Key, arg.ID, arg.WorkspaceID)
var i Issue
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.Title,
&i.Description,
&i.Status,
&i.Priority,
&i.AssigneeType,
&i.AssigneeID,
&i.CreatorType,
&i.CreatorID,
&i.ParentIssueID,
&i.AcceptanceCriteria,
&i.ContextRefs,
&i.Position,
&i.DueDate,
&i.CreatedAt,
&i.UpdatedAt,
&i.Number,
&i.ProjectID,
&i.OriginType,
&i.OriginID,
&i.FirstExecutedAt,
&i.StartDate,
&i.Metadata,
&i.Stage,
&i.Properties,
)
return i, err
}
const getIssueProperty = `-- name: GetIssueProperty :one
SELECT id, workspace_id, name, type, description, config, position, archived_at, created_at, updated_at FROM issue_property
WHERE id = $1 AND workspace_id = $2
`
type GetIssuePropertyParams struct {
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
}
func (q *Queries) GetIssueProperty(ctx context.Context, arg GetIssuePropertyParams) (IssueProperty, error) {
row := q.db.QueryRow(ctx, getIssueProperty, arg.ID, arg.WorkspaceID)
var i IssueProperty
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.Name,
&i.Type,
&i.Description,
&i.Config,
&i.Position,
&i.ArchivedAt,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const listIssueProperties = `-- name: ListIssueProperties :many
SELECT p.id, p.workspace_id, p.name, p.type, p.description, p.config, p.position, p.archived_at, p.created_at, p.updated_at,
(
SELECT COUNT(*) FROM issue i
WHERE i.workspace_id = p.workspace_id
AND i.properties ? p.id::text
)::bigint AS usage_count
FROM issue_property p
WHERE p.workspace_id = $1::uuid
AND ($2::bool OR p.archived_at IS NULL)
ORDER BY p.position ASC, LOWER(p.name) ASC
`
type ListIssuePropertiesParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
IncludeArchived bool `json:"include_archived"`
}
type ListIssuePropertiesRow struct {
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
Name string `json:"name"`
Type string `json:"type"`
Description string `json:"description"`
Config []byte `json:"config"`
Position float64 `json:"position"`
ArchivedAt pgtype.Timestamptz `json:"archived_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
UsageCount int64 `json:"usage_count"`
}
// usage_count = number of issues in the workspace that currently carry a
// value for this property. `properties ? id` is a seq scan today; fine at
// the 20-definition / small-workspace scale this feature targets.
func (q *Queries) ListIssueProperties(ctx context.Context, arg ListIssuePropertiesParams) ([]ListIssuePropertiesRow, error) {
rows, err := q.db.Query(ctx, listIssueProperties, arg.WorkspaceID, arg.IncludeArchived)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListIssuePropertiesRow{}
for rows.Next() {
var i ListIssuePropertiesRow
if err := rows.Scan(
&i.ID,
&i.WorkspaceID,
&i.Name,
&i.Type,
&i.Description,
&i.Config,
&i.Position,
&i.ArchivedAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.UsageCount,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const setIssuePropertyValue = `-- name: SetIssuePropertyValue :one
UPDATE issue
SET properties = jsonb_set(properties, ARRAY[$1::text], $2::jsonb, true),
updated_at = now()
WHERE id = $3::uuid AND workspace_id = $4::uuid
RETURNING id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at, start_date, metadata, stage, properties
`
type SetIssuePropertyValueParams struct {
Key string `json:"key"`
Value []byte `json:"value"`
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
}
// Single-key atomic write (mirror of SetIssueMetadataKey): concurrent writers
// on different property keys never clobber each other.
func (q *Queries) SetIssuePropertyValue(ctx context.Context, arg SetIssuePropertyValueParams) (Issue, error) {
row := q.db.QueryRow(ctx, setIssuePropertyValue,
arg.Key,
arg.Value,
arg.ID,
arg.WorkspaceID,
)
var i Issue
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.Title,
&i.Description,
&i.Status,
&i.Priority,
&i.AssigneeType,
&i.AssigneeID,
&i.CreatorType,
&i.CreatorID,
&i.ParentIssueID,
&i.AcceptanceCriteria,
&i.ContextRefs,
&i.Position,
&i.DueDate,
&i.CreatedAt,
&i.UpdatedAt,
&i.Number,
&i.ProjectID,
&i.OriginType,
&i.OriginID,
&i.FirstExecutedAt,
&i.StartDate,
&i.Metadata,
&i.Stage,
&i.Properties,
)
return i, err
}
const updateIssueProperty = `-- name: UpdateIssueProperty :one
UPDATE issue_property SET
name = COALESCE($3, name),
description = COALESCE($4, description),
config = COALESCE($5, config),
archived_at = CASE WHEN $6::bool THEN $7 ELSE archived_at END,
updated_at = now()
WHERE id = $1 AND workspace_id = $2
RETURNING id, workspace_id, name, type, description, config, position, archived_at, created_at, updated_at
`
type UpdateIssuePropertyParams struct {
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
Name pgtype.Text `json:"name"`
Description pgtype.Text `json:"description"`
Config []byte `json:"config"`
ArchivedSet bool `json:"archived_set"`
ArchivedAt pgtype.Timestamptz `json:"archived_at"`
}
// `type` is deliberately immutable — changing it would silently invalidate
// existing values. archived_set/archived_at implement tri-state semantics:
// archived_set=false leaves archived_at untouched.
func (q *Queries) UpdateIssueProperty(ctx context.Context, arg UpdateIssuePropertyParams) (IssueProperty, error) {
row := q.db.QueryRow(ctx, updateIssueProperty,
arg.ID,
arg.WorkspaceID,
arg.Name,
arg.Description,
arg.Config,
arg.ArchivedSet,
arg.ArchivedAt,
)
var i IssueProperty
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.Name,
&i.Type,
&i.Description,
&i.Config,
&i.Position,
&i.ArchivedAt,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}