Files
member-console/internal/integration/lifecycle_requests.sql.go
T
cgalo5758 083355bb12 Record a lifecycle request through named steps and one address read
RecordLifecycleRequest was one function (gocognit 32) that opened its
own transaction, validated the spec, read the content, took the lock,
decided new against repeat and answered the repeat. It is now a short
body over named steps, and the steps that run under the instance lock
take the caller's queries and open nothing. Measured scores of the
functions written: inOwnTx 1, checkAddress 2, content 4, lockLatest 2,
recordNew 6, answerAddress 4, heldAt 3. RecordLifecycleRequest goes
from 32 to 8, RecordFirstLifecycleRequest from 12 to 7 and
LookupLifecycleRequest from 3 to 2 (golangci-lint's gocognit, measured
at the parent and at this commit). The pinned complexity rule for
RecordLifecycleRequest leaves .golangci.yml.

heldAt answers both the recording's repeat and LookupLifecycleRequest
from the same read. Only the two exported entry points open a
transaction, through db.InTx, which begins with default options,
rolls back on an error and commits on nil, as the code did. The
statements, their order, the answers, the lock key and the error
texts at run time are unchanged. RecordFirstLifecycleRequest still
takes the lock and reads the latest generation, then calls
RecordLifecycleRequest, which does both again.

The comments of LockLifecycleInstance and InsertLifecycleRequest now
say what recording runs and that no row back from the insert means the
operation or key was refused; sqlc was regenerated and only comment
lines differ in lifecycle_requests.sql.go and querier.go. The content
method gains a doc comment.

How no output changed:
- go test over ./internal/integration and ./internal/integrations/
  fedwiki/... gives 302 test and subtest names with the same results at
  the parent and here (301 pass, 1 skip, 0 fail); the lifecycle answer
  golden is byte-equal. No flake appeared.
- The Temporal surface (activities, workflows, workflow bodies,
  payload types) is byte-equal to the parent's and to the baseline.
  TestFedWikiSitesWalkthrough passes at the parent and here.
- Literals of lifecycle.go: the two address formats "record lifecycle
  request: instance %q generation %d" and "record first lifecycle
  request: instance %q generation %d" become "%s: instance %q
  generation %d", with "record lifecycle request" and "record first
  lifecycle request" as literals of their own (the texts printed are
  the same). "lock lifecycle instance: %w", "latest lifecycle
  generation: %w" and "read lifecycle request: %w" are written once
  each where they were written twice. The empty string literal is
  written five times where it was six, and the import path of
  internal/db is new. Nothing else moves.
- 21 breaks per side, each caught: 12 run on both sides, 9 on the
  parent only and 9 here only, where the code the break edits moved.
  The lock key, sameLifecycleContent and the spec and answer types
  print the same at both sides; the only transaction opener left in
  lifecycle.go is the one in inOwnTx, and GetLifecycleRequestByAddress
  is called from heldAt only.
- go run . lint prints the same at both sides; the lint snapshot adds
  and removes nothing.

Not probed: a database error after the first statement (no test makes
one fail and nothing else); the order in which RecordFirstLifecycleRequest
reads the person (pinned by the answer golden, unchanged); the
*sql.Conn path, which inOwnTx treats as a transaction as the code did
and no caller uses.
2026-10-11 18:07:47 -05:00

485 lines
16 KiB
Go

// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// source: lifecycle_requests.sql
package integration
import (
"context"
"database/sql"
"encoding/json"
"time"
"github.com/google/uuid"
"github.com/lib/pq"
)
const acknowledgeLifecycleRequest = `-- name: AcknowledgeLifecycleRequest :execrows
UPDATE core.lifecycle_requests
SET acknowledged_at = NOW()
WHERE instance_id = $1::uuid
AND generation = $2::bigint
AND workspace_id = $3::uuid
AND status IN ('declined', 'failed')
AND acknowledged_at IS NULL
`
type AcknowledgeLifecycleRequestParams struct {
InstanceID string `json:"instance_id"`
Generation int64 `json:"generation"`
WorkspaceID string `json:"workspace_id"`
}
// The member dismissed a declined or failed outcome in their workspace.
func (q *Queries) AcknowledgeLifecycleRequest(ctx context.Context, arg AcknowledgeLifecycleRequestParams) (int64, error) {
result, err := q.db.ExecContext(ctx, acknowledgeLifecycleRequest, arg.InstanceID, arg.Generation, arg.WorkspaceID)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const deleteUnstartedLifecycleRequest = `-- name: DeleteUnstartedLifecycleRequest :execrows
DELETE FROM core.lifecycle_requests
WHERE instance_id = $1::uuid
AND generation = $2::bigint
AND workspace_id = $3::uuid
AND status = 'accepted'
`
type DeleteUnstartedLifecycleRequestParams struct {
InstanceID string `json:"instance_id"`
Generation int64 `json:"generation"`
WorkspaceID string `json:"workspace_id"`
}
// A handler whose workflow start was refused with NotFound deletes the row it
// inserted, so the member's retry is a fresh insert. Only an accepted row in
// the caller's workspace is deleted.
func (q *Queries) DeleteUnstartedLifecycleRequest(ctx context.Context, arg DeleteUnstartedLifecycleRequestParams) (int64, error) {
result, err := q.db.ExecContext(ctx, deleteUnstartedLifecycleRequest, arg.InstanceID, arg.Generation, arg.WorkspaceID)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const finishLifecycleRequest = `-- name: FinishLifecycleRequest :one
UPDATE core.lifecycle_requests
SET status = $1::text,
reason_code = $2::text,
finished_at = CASE WHEN status = 'accepted' THEN $3::timestamptz ELSE finished_at END
WHERE instance_id = $4::uuid
AND generation = $5::bigint
AND (status = 'accepted'
OR (status = $1::text AND reason_code IS NOT DISTINCT FROM $2::text))
RETURNING request_id
`
type FinishLifecycleRequestParams struct {
Status string `json:"status"`
ReasonCode sql.NullString `json:"reason_code"`
FinishedAt time.Time `json:"finished_at"`
InstanceID string `json:"instance_id"`
Generation int64 `json:"generation"`
}
// Record a request's outcome. Repeating the same outcome (a retried local
// activity whose first attempt committed) matches again and keeps the first
// finish time; a missing request, or one finished with another outcome,
// matches nothing and returns sql.ErrNoRows, which fails the activity.
func (q *Queries) FinishLifecycleRequest(ctx context.Context, arg FinishLifecycleRequestParams) (string, error) {
row := q.db.QueryRowContext(ctx, finishLifecycleRequest,
arg.Status,
arg.ReasonCode,
arg.FinishedAt,
arg.InstanceID,
arg.Generation,
)
var request_id string
err := row.Scan(&request_id)
return request_id, err
}
const getFirstLifecycleRequest = `-- name: GetFirstLifecycleRequest :one
SELECT request_id, instance_id, generation, connection_id, resource_key, workspace_id, operation, parameters, status, reason_code, workflow_id, requested_by_person_id, created_at, updated_at, finished_at, acknowledged_at FROM core.lifecycle_requests
WHERE instance_id = $1::uuid
ORDER BY generation
LIMIT 1
`
// An instance's first request. A create is always its instance's first
// request, so the JSON API finds a create by its instance alone (design D25).
func (q *Queries) GetFirstLifecycleRequest(ctx context.Context, instanceID string) (LifecycleRequest, error) {
row := q.db.QueryRowContext(ctx, getFirstLifecycleRequest, instanceID)
var i LifecycleRequest
err := row.Scan(
&i.RequestID,
&i.InstanceID,
&i.Generation,
&i.ConnectionID,
&i.ResourceKey,
&i.WorkspaceID,
&i.Operation,
&i.Parameters,
&i.Status,
&i.ReasonCode,
&i.WorkflowID,
&i.RequestedByPersonID,
&i.CreatedAt,
&i.UpdatedAt,
&i.FinishedAt,
&i.AcknowledgedAt,
)
return i, err
}
const getLifecycleRequestByAddress = `-- name: GetLifecycleRequestByAddress :one
SELECT request_id, instance_id, generation, connection_id, resource_key, workspace_id, operation, parameters, status, reason_code, workflow_id, requested_by_person_id, created_at, updated_at, finished_at, acknowledged_at FROM core.lifecycle_requests
WHERE instance_id = $1::uuid AND generation = $2::bigint
`
type GetLifecycleRequestByAddressParams struct {
InstanceID string `json:"instance_id"`
Generation int64 `json:"generation"`
}
func (q *Queries) GetLifecycleRequestByAddress(ctx context.Context, arg GetLifecycleRequestByAddressParams) (LifecycleRequest, error) {
row := q.db.QueryRowContext(ctx, getLifecycleRequestByAddress, arg.InstanceID, arg.Generation)
var i LifecycleRequest
err := row.Scan(
&i.RequestID,
&i.InstanceID,
&i.Generation,
&i.ConnectionID,
&i.ResourceKey,
&i.WorkspaceID,
&i.Operation,
&i.Parameters,
&i.Status,
&i.ReasonCode,
&i.WorkflowID,
&i.RequestedByPersonID,
&i.CreatedAt,
&i.UpdatedAt,
&i.FinishedAt,
&i.AcknowledgedAt,
)
return i, err
}
const getLifecycleRequesterSubject = `-- name: GetLifecycleRequesterSubject :one
SELECT u.oidc_subject
FROM core.lifecycle_requests lr
JOIN core.persons p ON p.person_id = lr.requested_by_person_id
JOIN core.users u ON u.user_id = p.user_id
WHERE lr.instance_id = $1::uuid
AND lr.generation = $2::bigint
`
type GetLifecycleRequesterSubjectParams struct {
InstanceID string `json:"instance_id"`
Generation int64 `json:"generation"`
}
// The identity-provider subject of the person who made a request, read when
// a workflow must compare it with an application's owner record without
// carrying it in its input (design D27's create reconcile).
func (q *Queries) GetLifecycleRequesterSubject(ctx context.Context, arg GetLifecycleRequesterSubjectParams) (string, error) {
row := q.db.QueryRowContext(ctx, getLifecycleRequesterSubject, arg.InstanceID, arg.Generation)
var oidc_subject string
err := row.Scan(&oidc_subject)
return oidc_subject, err
}
const insertLifecycleRequest = `-- name: InsertLifecycleRequest :one
INSERT INTO core.lifecycle_requests
(instance_id, generation, connection_id, resource_key, workspace_id,
operation, parameters, workflow_id, requested_by_person_id)
SELECT $1::uuid,
$2::bigint,
pc.connection_id,
rk.resource_key,
$3::uuid,
po.operation,
$4::jsonb,
$5::text,
$6::uuid
FROM core.provider_connections pc
JOIN core.provider_operations po
ON po.provider = pc.provider AND po.operation = $7::text
JOIN core.resource_keys rk
ON rk.provider = pc.provider AND rk.resource_key = $8::text
WHERE pc.connection_id = $9::uuid
ON CONFLICT (instance_id, generation) DO NOTHING
RETURNING request_id
`
type InsertLifecycleRequestParams struct {
InstanceID string `json:"instance_id"`
Generation int64 `json:"generation"`
WorkspaceID string `json:"workspace_id"`
Parameters json.RawMessage `json:"parameters"`
WorkflowID string `json:"workflow_id"`
RequestedByPersonID uuid.NullUUID `json:"requested_by_person_id"`
Operation string `json:"operation"`
ResourceKey string `json:"resource_key"`
ConnectionID string `json:"connection_id"`
}
// Insert a request at its address. The operation must be one the
// connection's provider declares in core.provider_operations, and the
// provider must own the resource key; otherwise nothing is inserted.
// A held address also inserts nothing (ON CONFLICT on the address only). The
// recording inserts only above the instance's latest generation, under its
// lock, so there no row back means the operation or key was refused. A second
// accepted request on the instance under another generation raises a unique
// violation on uq_lifecycle_requests_one_open.
func (q *Queries) InsertLifecycleRequest(ctx context.Context, arg InsertLifecycleRequestParams) (string, error) {
row := q.db.QueryRowContext(ctx, insertLifecycleRequest,
arg.InstanceID,
arg.Generation,
arg.WorkspaceID,
arg.Parameters,
arg.WorkflowID,
arg.RequestedByPersonID,
arg.Operation,
arg.ResourceKey,
arg.ConnectionID,
)
var request_id string
err := row.Scan(&request_id)
return request_id, err
}
const latestLifecycleGeneration = `-- name: LatestLifecycleGeneration :one
SELECT COALESCE(MAX(generation), 0)::bigint AS latest_generation
FROM core.lifecycle_requests
WHERE instance_id = $1::uuid
`
// The highest generation recorded for an instance, 0 when it has none.
func (q *Queries) LatestLifecycleGeneration(ctx context.Context, instanceID string) (int64, error) {
row := q.db.QueryRowContext(ctx, latestLifecycleGeneration, instanceID)
var latest_generation int64
err := row.Scan(&latest_generation)
return latest_generation, err
}
const listAcceptedLifecycleRequestsOlderThan = `-- name: ListAcceptedLifecycleRequestsOlderThan :many
SELECT request_id, instance_id, generation, connection_id, resource_key, workspace_id, operation, parameters, status, reason_code, workflow_id, requested_by_person_id, created_at, updated_at, finished_at, acknowledged_at FROM core.lifecycle_requests
WHERE connection_id = $1::uuid
AND status = 'accepted'
AND created_at < NOW() - ($2::bigint * INTERVAL '1 second')
ORDER BY created_at, request_id
`
type ListAcceptedLifecycleRequestsOlderThanParams struct {
ConnectionID string `json:"connection_id"`
AgeSeconds int64 `json:"age_seconds"`
}
// The boot pass (design D27): accepted requests at one connection older than
// the given age, whose workflow may never have started or may have closed
// without recording an outcome.
func (q *Queries) ListAcceptedLifecycleRequestsOlderThan(ctx context.Context, arg ListAcceptedLifecycleRequestsOlderThanParams) ([]LifecycleRequest, error) {
rows, err := q.db.QueryContext(ctx, listAcceptedLifecycleRequestsOlderThan, arg.ConnectionID, arg.AgeSeconds)
if err != nil {
return nil, err
}
defer rows.Close()
items := []LifecycleRequest{}
for rows.Next() {
var i LifecycleRequest
if err := rows.Scan(
&i.RequestID,
&i.InstanceID,
&i.Generation,
&i.ConnectionID,
&i.ResourceKey,
&i.WorkspaceID,
&i.Operation,
&i.Parameters,
&i.Status,
&i.ReasonCode,
&i.WorkflowID,
&i.RequestedByPersonID,
&i.CreatedAt,
&i.UpdatedAt,
&i.FinishedAt,
&i.AcknowledgedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const lockLifecycleInstance = `-- name: LockLifecycleInstance :exec
SELECT pg_advisory_xact_lock(hashtextextended('lifecycle:' || $1::uuid::text, 0))
`
// Lifecycle requests (design D24 to D28). Recording a request runs, in one
// transaction: LockLifecycleInstance, LatestLifecycleGeneration, then for a
// generation above the latest GenerationCeiling, OpenLifecycleGeneration and
// InsertLifecycleRequest, and otherwise GetLifecycleRequestByAddress, so the
// latest-generation check and the insert are one step per instance.
func (q *Queries) LockLifecycleInstance(ctx context.Context, instanceID string) error {
_, err := q.db.ExecContext(ctx, lockLifecycleInstance, instanceID)
return err
}
const mintInstanceID = `-- name: MintInstanceID :one
SELECT uuidv7()::uuid AS instance_id
`
// A new instance's identifier comes from the database, the only source of
// UUIDs (design D25).
func (q *Queries) MintInstanceID(ctx context.Context) (string, error) {
row := q.db.QueryRowContext(ctx, mintInstanceID)
var instance_id string
err := row.Scan(&instance_id)
return instance_id, err
}
const mintLifecycleGeneration = `-- name: MintLifecycleGeneration :one
SELECT core.next_generation(NULL, NULL)::bigint AS generation
`
// A generation for a request on an instance that has no request yet,
// floored by the clock (design D25).
func (q *Queries) MintLifecycleGeneration(ctx context.Context) (int64, error) {
row := q.db.QueryRowContext(ctx, mintLifecycleGeneration)
var generation int64
err := row.Scan(&generation)
return generation, err
}
const nextLifecycleGenerations = `-- name: NextLifecycleGenerations :many
SELECT ids.instance_id::uuid AS instance_id,
core.next_generation(MAX(lr.generation), NULL)::bigint AS next_generation
FROM unnest($1::uuid[]) AS ids(instance_id)
LEFT JOIN core.lifecycle_requests lr ON lr.instance_id = ids.instance_id
GROUP BY ids.instance_id
`
type NextLifecycleGenerationsRow struct {
InstanceID string `json:"instance_id"`
NextGeneration int64 `json:"next_generation"`
}
// The generation each instance's next request carries: above its latest
// request and at least the clock, one row per instance asked for.
func (q *Queries) NextLifecycleGenerations(ctx context.Context, instanceIds []string) ([]NextLifecycleGenerationsRow, error) {
rows, err := q.db.QueryContext(ctx, nextLifecycleGenerations, pq.Array(instanceIds))
if err != nil {
return nil, err
}
defer rows.Close()
items := []NextLifecycleGenerationsRow{}
for rows.Next() {
var i NextLifecycleGenerationsRow
if err := rows.Scan(&i.InstanceID, &i.NextGeneration); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const openLifecycleGeneration = `-- name: OpenLifecycleGeneration :one
SELECT COALESCE(MAX(generation), 0)::bigint AS open_generation
FROM core.lifecycle_requests
WHERE instance_id = $1::uuid AND status = 'accepted'
`
// The generation of the instance's accepted request, 0 when none is open.
// Read under LockLifecycleInstance, so the one-open rule is answered before
// the insert instead of by a unique violation that would abort the caller's
// transaction.
func (q *Queries) OpenLifecycleGeneration(ctx context.Context, instanceID string) (int64, error) {
row := q.db.QueryRowContext(ctx, openLifecycleGeneration, instanceID)
var open_generation int64
err := row.Scan(&open_generation)
return open_generation, err
}
const openLifecycleRequestsByWorkspace = `-- name: OpenLifecycleRequestsByWorkspace :many
SELECT lr.request_id, lr.instance_id, lr.generation, lr.connection_id, lr.resource_key, lr.workspace_id, lr.operation, lr.parameters, lr.status, lr.reason_code, lr.workflow_id, lr.requested_by_person_id, lr.created_at, lr.updated_at, lr.finished_at, lr.acknowledged_at FROM core.lifecycle_requests lr
WHERE lr.workspace_id = $1::uuid
AND lr.connection_id = $2::uuid
AND (lr.status = 'accepted'
OR (lr.status IN ('declined', 'failed')
AND lr.acknowledged_at IS NULL
AND NOT EXISTS (
SELECT 1 FROM core.lifecycle_requests later
WHERE later.instance_id = lr.instance_id
AND later.generation > lr.generation)))
ORDER BY lr.created_at, lr.request_id
`
type OpenLifecycleRequestsByWorkspaceParams struct {
WorkspaceID string `json:"workspace_id"`
ConnectionID string `json:"connection_id"`
}
// What a workspace's list shows for one connection: every accepted request,
// and per instance the latest request when it is declined or failed and not
// acknowledged (a later request on the instance supersedes it).
func (q *Queries) OpenLifecycleRequestsByWorkspace(ctx context.Context, arg OpenLifecycleRequestsByWorkspaceParams) ([]LifecycleRequest, error) {
rows, err := q.db.QueryContext(ctx, openLifecycleRequestsByWorkspace, arg.WorkspaceID, arg.ConnectionID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []LifecycleRequest{}
for rows.Next() {
var i LifecycleRequest
if err := rows.Scan(
&i.RequestID,
&i.InstanceID,
&i.Generation,
&i.ConnectionID,
&i.ResourceKey,
&i.WorkspaceID,
&i.Operation,
&i.Parameters,
&i.Status,
&i.ReasonCode,
&i.WorkflowID,
&i.RequestedByPersonID,
&i.CreatedAt,
&i.UpdatedAt,
&i.FinishedAt,
&i.AcknowledgedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}