The Rules section is one record table grouped by kind, Limit then Boolean, on fixed columns, edited in place: Edit opens a row's controls in their columns, Add rule opens a dense row above the table, and every change is staged into a tray that lists the deltas with Undo and applies them as one rule-change act. The reduction policy is a column of the rule beside its limit. History shows counts only. Group rows are a quiet heading rather than a divider, the maintainer's pick from four rounds of outside-model ideation. Dense rows align to the top and render each error under its control in every form family (design D16), replacing the below-row error block; the forms library gains the batch form (rows plus one tray) and the RowField dense and label-hidden options. Migration 00019 records the governing reduction policy on effect rows. Archive staged-rule-changes with its spec updates (entitlement-set- management, entitlement-set-history, entitlements, form-library, form-conventions, ui-quality-gate). Screens accepted 2026-09-19.
780 lines
27 KiB
Go
780 lines
27 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: entitlement_set_changes.sql
|
|
|
|
package entitlements
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/lib/pq"
|
|
"github.com/sqlc-dev/pqtype"
|
|
)
|
|
|
|
const commitRuleChange = `-- name: CommitRuleChange :one
|
|
|
|
SELECT (r).change_id::uuid AS change_id,
|
|
(r).rule_id::uuid AS rule_id,
|
|
(r).sync_path::varchar AS sync_path,
|
|
(r).pool_count::integer AS pool_count,
|
|
(r).org_count::integer AS org_count,
|
|
(r).suspended_only_count::integer AS suspended_only_count
|
|
FROM core.commit_rule_change(
|
|
$1::uuid,
|
|
$2::varchar,
|
|
$3::uuid,
|
|
$4::jsonb,
|
|
$5::varchar,
|
|
$6::uuid,
|
|
$7::uuid,
|
|
$8::text,
|
|
$9::varchar,
|
|
$10::timestamptz,
|
|
$11::integer
|
|
) AS r
|
|
`
|
|
|
|
type CommitRuleChangeParams struct {
|
|
SetID string `json:"set_id"`
|
|
ChangeKind string `json:"change_kind"`
|
|
RuleID uuid.NullUUID `json:"rule_id"`
|
|
Rule json.RawMessage `json:"rule"`
|
|
ActorType string `json:"actor_type"`
|
|
ActorPersonID uuid.NullUUID `json:"actor_person_id"`
|
|
ActorServiceAccountID uuid.NullUUID `json:"actor_service_account_id"`
|
|
Note sql.NullString `json:"note"`
|
|
RequestID sql.NullString `json:"request_id"`
|
|
EffectiveAt time.Time `json:"effective_at"`
|
|
SyncCap int32 `json:"sync_cap"`
|
|
}
|
|
|
|
type CommitRuleChangeRow struct {
|
|
ChangeID string `json:"change_id"`
|
|
RuleID string `json:"rule_id"`
|
|
SyncPath string `json:"sync_path"`
|
|
PoolCount int32 `json:"pool_count"`
|
|
OrgCount int32 `json:"org_count"`
|
|
SuspendedOnlyCount int32 `json:"suspended_only_count"`
|
|
}
|
|
|
|
// The act rows (core.entitlement_set_changes) and the effect rows
|
|
// (core.entitlement_set_change_effects) are append-only: nothing in this file
|
|
// updates or deletes one. Both are written by core.commit_rule_change and
|
|
// core.settle_obligation, so no Create query exists for any of the three
|
|
// tables. The obligations are the one mutable table, a work list rather than
|
|
// history, and only its status columns move.
|
|
// The only write path to core.entitlement_set_rules. The calling transaction
|
|
// must hold the exclusive materialization rendezvous, which the function
|
|
// asserts: open it with BeginRuleChange.
|
|
func (q *Queries) CommitRuleChange(ctx context.Context, arg CommitRuleChangeParams) (CommitRuleChangeRow, error) {
|
|
row := q.db.QueryRowContext(ctx, commitRuleChange,
|
|
arg.SetID,
|
|
arg.ChangeKind,
|
|
arg.RuleID,
|
|
arg.Rule,
|
|
arg.ActorType,
|
|
arg.ActorPersonID,
|
|
arg.ActorServiceAccountID,
|
|
arg.Note,
|
|
arg.RequestID,
|
|
arg.EffectiveAt,
|
|
arg.SyncCap,
|
|
)
|
|
var i CommitRuleChangeRow
|
|
err := row.Scan(
|
|
&i.ChangeID,
|
|
&i.RuleID,
|
|
&i.SyncPath,
|
|
&i.PoolCount,
|
|
&i.OrgCount,
|
|
&i.SuspendedOnlyCount,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const countEntitlementSetChangeEffectsByOrg = `-- name: CountEntitlementSetChangeEffectsByOrg :one
|
|
SELECT COUNT(*)::BIGINT AS effect_count
|
|
FROM core.entitlement_set_change_effects
|
|
WHERE org_id = $1
|
|
`
|
|
|
|
func (q *Queries) CountEntitlementSetChangeEffectsByOrg(ctx context.Context, orgID string) (int64, error) {
|
|
row := q.db.QueryRowContext(ctx, countEntitlementSetChangeEffectsByOrg, orgID)
|
|
var effect_count int64
|
|
err := row.Scan(&effect_count)
|
|
return effect_count, err
|
|
}
|
|
|
|
const countEntitlementSetChanges = `-- name: CountEntitlementSetChanges :one
|
|
SELECT COUNT(*)::BIGINT AS change_count
|
|
FROM core.entitlement_set_changes
|
|
WHERE set_id = $1
|
|
`
|
|
|
|
func (q *Queries) CountEntitlementSetChanges(ctx context.Context, setID string) (int64, error) {
|
|
row := q.db.QueryRowContext(ctx, countEntitlementSetChanges, setID)
|
|
var change_count int64
|
|
err := row.Scan(&change_count)
|
|
return change_count, err
|
|
}
|
|
|
|
const countFailedObligationsBySets = `-- name: CountFailedObligationsBySets :many
|
|
SELECT c.set_id, COUNT(*)::BIGINT AS failed_count
|
|
FROM core.entitlement_set_change_obligations o
|
|
JOIN core.entitlement_set_changes c ON c.change_id = o.change_id
|
|
WHERE c.set_id = ANY($1::uuid[]) AND o.status = 'failed'
|
|
GROUP BY c.set_id
|
|
ORDER BY c.set_id ASC
|
|
`
|
|
|
|
type CountFailedObligationsBySetsRow struct {
|
|
SetID string `json:"set_id"`
|
|
FailedCount int64 `json:"failed_count"`
|
|
}
|
|
|
|
// The same count for a page of sets in one query rather than one per row.
|
|
func (q *Queries) CountFailedObligationsBySets(ctx context.Context, setIds []string) ([]CountFailedObligationsBySetsRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, countFailedObligationsBySets, pq.Array(setIds))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []CountFailedObligationsBySetsRow{}
|
|
for rows.Next() {
|
|
var i CountFailedObligationsBySetsRow
|
|
if err := rows.Scan(&i.SetID, &i.FailedCount); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const countUnsettledObligationsBySet = `-- name: CountUnsettledObligationsBySet :one
|
|
SELECT
|
|
COUNT(*) FILTER (WHERE o.status = 'pending')::BIGINT AS pending_count,
|
|
COUNT(*) FILTER (WHERE o.status = 'failed')::BIGINT AS failed_count
|
|
FROM core.entitlement_set_change_obligations o
|
|
JOIN core.entitlement_set_changes c ON c.change_id = o.change_id
|
|
WHERE c.set_id = $1
|
|
`
|
|
|
|
type CountUnsettledObligationsBySetRow struct {
|
|
PendingCount int64 `json:"pending_count"`
|
|
FailedCount int64 `json:"failed_count"`
|
|
}
|
|
|
|
// What the set's Rules section reads: still draining and failed, separately.
|
|
func (q *Queries) CountUnsettledObligationsBySet(ctx context.Context, setID string) (CountUnsettledObligationsBySetRow, error) {
|
|
row := q.db.QueryRowContext(ctx, countUnsettledObligationsBySet, setID)
|
|
var i CountUnsettledObligationsBySetRow
|
|
err := row.Scan(&i.PendingCount, &i.FailedCount)
|
|
return i, err
|
|
}
|
|
|
|
const getObligationStatus = `-- name: GetObligationStatus :one
|
|
SELECT status, attempts
|
|
FROM core.entitlement_set_change_obligations
|
|
WHERE change_id = $1 AND pool_id = $2
|
|
`
|
|
|
|
type GetObligationStatusParams struct {
|
|
ChangeID string `json:"change_id"`
|
|
PoolID string `json:"pool_id"`
|
|
}
|
|
|
|
type GetObligationStatusRow struct {
|
|
Status string `json:"status"`
|
|
Attempts int32 `json:"attempts"`
|
|
}
|
|
|
|
// One obligation's state, which the drain reads before it does any work: an
|
|
// obligation already settled is returned to a redispatched drain untouched.
|
|
func (q *Queries) GetObligationStatus(ctx context.Context, arg GetObligationStatusParams) (GetObligationStatusRow, error) {
|
|
row := q.db.QueryRowContext(ctx, getObligationStatus, arg.ChangeID, arg.PoolID)
|
|
var i GetObligationStatusRow
|
|
err := row.Scan(&i.Status, &i.Attempts)
|
|
return i, err
|
|
}
|
|
|
|
const getRuleChangeStamp = `-- name: GetRuleChangeStamp :one
|
|
SELECT r.resource_key, r.tier_reduction_policy
|
|
FROM core.entitlement_set_changes c
|
|
JOIN core.entitlement_set_rules r ON r.rule_id = c.rule_id
|
|
WHERE c.change_id = $1
|
|
`
|
|
|
|
type GetRuleChangeStampRow struct {
|
|
ResourceKey sql.NullString `json:"resource_key"`
|
|
TierReductionPolicy string `json:"tier_reduction_policy"`
|
|
}
|
|
|
|
// The changed rule's resource key and reduction policy. The policy is
|
|
// stamped on that key's effect row alone (doc-47 §5.2: the reduction policy
|
|
// is the changed rule's, where the key is the rule's), so the key comes back
|
|
// with it. The drain reads the pair once per change.
|
|
func (q *Queries) GetRuleChangeStamp(ctx context.Context, changeID string) (GetRuleChangeStampRow, error) {
|
|
row := q.db.QueryRowContext(ctx, getRuleChangeStamp, changeID)
|
|
var i GetRuleChangeStampRow
|
|
err := row.Scan(&i.ResourceKey, &i.TierReductionPolicy)
|
|
return i, err
|
|
}
|
|
|
|
const listChangesSharingRequest = `-- name: ListChangesSharingRequest :many
|
|
SELECT c.change_id, r.resource_key, r.tier_reduction_policy
|
|
FROM core.entitlement_set_changes c
|
|
JOIN core.entitlement_set_rules r ON r.rule_id = c.rule_id
|
|
JOIN core.entitlement_set_changes named ON named.change_id = $1::uuid
|
|
WHERE c.set_id = named.set_id
|
|
AND (c.change_id = named.change_id
|
|
OR (named.request_id IS NOT NULL AND c.request_id = named.request_id))
|
|
ORDER BY r.resource_key COLLATE "C" ASC NULLS FIRST, c.change_id ASC
|
|
`
|
|
|
|
type ListChangesSharingRequestRow struct {
|
|
ChangeID string `json:"change_id"`
|
|
ResourceKey sql.NullString `json:"resource_key"`
|
|
TierReductionPolicy string `json:"tier_reduction_policy"`
|
|
}
|
|
|
|
// The acts of one applied batch: the change named and every other act on the
|
|
// same set carrying the same request identifier, which is what relates them
|
|
// (entitlement-set-history, "Every committed act writes one change row and
|
|
// one effect row per pool and key that moved"). Each comes back with its
|
|
// rule's key and reduction policy, so the drain stamps and routes from one
|
|
// read. The order is the commit's own, resource key ascending under the C
|
|
// collation, which is the byte order Go sorted the deltas by, so the drain
|
|
// re-derives the order the acts were filed in whatever the database's own
|
|
// collation is; the first row is then the batch's first act, the one a
|
|
// movement on a key no act names hangs off. A change with no request
|
|
// identifier returns itself alone.
|
|
func (q *Queries) ListChangesSharingRequest(ctx context.Context, changeID string) ([]ListChangesSharingRequestRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, listChangesSharingRequest, changeID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListChangesSharingRequestRow{}
|
|
for rows.Next() {
|
|
var i ListChangesSharingRequestRow
|
|
if err := rows.Scan(&i.ChangeID, &i.ResourceKey, &i.TierReductionPolicy); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listChangesWithUnsettledObligations = `-- name: ListChangesWithUnsettledObligations :many
|
|
SELECT DISTINCT o.change_id, c.set_id, MIN(o.created_at)::timestamptz AS oldest_created_at
|
|
FROM core.entitlement_set_change_obligations o
|
|
JOIN core.entitlement_set_changes c ON c.change_id = o.change_id
|
|
WHERE o.status = 'pending'
|
|
GROUP BY o.change_id, c.set_id
|
|
ORDER BY oldest_created_at ASC, o.change_id ASC
|
|
LIMIT $1
|
|
`
|
|
|
|
type ListChangesWithUnsettledObligationsRow struct {
|
|
ChangeID string `json:"change_id"`
|
|
SetID string `json:"set_id"`
|
|
OldestCreatedAt time.Time `json:"oldest_created_at"`
|
|
}
|
|
|
|
// The poller's entry point, oldest act first: changes that still owe a
|
|
// pending pool. A change whose remaining rows are all failed owes the drain
|
|
// nothing until an operator's Retry requeues them.
|
|
func (q *Queries) ListChangesWithUnsettledObligations(ctx context.Context, limit int32) ([]ListChangesWithUnsettledObligationsRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, listChangesWithUnsettledObligations, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListChangesWithUnsettledObligationsRow{}
|
|
for rows.Next() {
|
|
var i ListChangesWithUnsettledObligationsRow
|
|
if err := rows.Scan(&i.ChangeID, &i.SetID, &i.OldestCreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listEntitlementSetChangeEffects = `-- name: ListEntitlementSetChangeEffects :many
|
|
SELECT e.effect_id, e.change_id, e.pool_id, e.org_id, e.org_name,
|
|
e.resource_key, e.resource_label,
|
|
e.limit_before, e.limit_after, e.granted_before, e.granted_after,
|
|
e.usage_at_effect, e.over_usage, e.was_over_before, e.reduction_policy, e.created_at,
|
|
e.governing_policy
|
|
FROM core.entitlement_set_change_effects e
|
|
WHERE e.change_id = $1
|
|
ORDER BY e.org_name ASC, e.resource_key ASC
|
|
LIMIT $2
|
|
`
|
|
|
|
type ListEntitlementSetChangeEffectsParams struct {
|
|
ChangeID string `json:"change_id"`
|
|
Limit int32 `json:"limit"`
|
|
}
|
|
|
|
// The History row's expander: what one change did, per organization and key.
|
|
func (q *Queries) ListEntitlementSetChangeEffects(ctx context.Context, arg ListEntitlementSetChangeEffectsParams) ([]EntitlementSetChangeEffect, error) {
|
|
rows, err := q.db.QueryContext(ctx, listEntitlementSetChangeEffects, arg.ChangeID, arg.Limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []EntitlementSetChangeEffect{}
|
|
for rows.Next() {
|
|
var i EntitlementSetChangeEffect
|
|
if err := rows.Scan(
|
|
&i.EffectID,
|
|
&i.ChangeID,
|
|
&i.PoolID,
|
|
&i.OrgID,
|
|
&i.OrgName,
|
|
&i.ResourceKey,
|
|
&i.ResourceLabel,
|
|
&i.LimitBefore,
|
|
&i.LimitAfter,
|
|
&i.GrantedBefore,
|
|
&i.GrantedAfter,
|
|
&i.UsageAtEffect,
|
|
&i.OverUsage,
|
|
&i.WasOverBefore,
|
|
&i.ReductionPolicy,
|
|
&i.CreatedAt,
|
|
&i.GoverningPolicy,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listEntitlementSetChangeEffectsByOrg = `-- name: ListEntitlementSetChangeEffectsByOrg :many
|
|
SELECT e.effect_id, e.change_id, e.pool_id, e.org_id, e.org_name,
|
|
e.resource_key, e.resource_label,
|
|
e.limit_before, e.limit_after, e.granted_before, e.granted_after,
|
|
e.usage_at_effect, e.over_usage, e.was_over_before,
|
|
e.reduction_policy, e.governing_policy, e.created_at,
|
|
c.set_id, c.set_name, c.change_kind, c.effective_at,
|
|
c.note, c.actor_type, c.actor_person_id, c.actor_service_account_id
|
|
FROM core.entitlement_set_change_effects e
|
|
JOIN core.entitlement_set_changes c ON c.change_id = e.change_id
|
|
WHERE e.org_id = $1
|
|
ORDER BY e.created_at DESC, e.effect_id DESC
|
|
LIMIT $2 OFFSET $3
|
|
`
|
|
|
|
type ListEntitlementSetChangeEffectsByOrgParams struct {
|
|
OrgID string `json:"org_id"`
|
|
Limit int32 `json:"limit"`
|
|
Offset int32 `json:"offset"`
|
|
}
|
|
|
|
type ListEntitlementSetChangeEffectsByOrgRow struct {
|
|
EffectID string `json:"effect_id"`
|
|
ChangeID string `json:"change_id"`
|
|
PoolID string `json:"pool_id"`
|
|
OrgID string `json:"org_id"`
|
|
OrgName string `json:"org_name"`
|
|
ResourceKey string `json:"resource_key"`
|
|
ResourceLabel string `json:"resource_label"`
|
|
LimitBefore sql.NullInt64 `json:"limit_before"`
|
|
LimitAfter sql.NullInt64 `json:"limit_after"`
|
|
GrantedBefore sql.NullBool `json:"granted_before"`
|
|
GrantedAfter sql.NullBool `json:"granted_after"`
|
|
UsageAtEffect sql.NullInt64 `json:"usage_at_effect"`
|
|
OverUsage bool `json:"over_usage"`
|
|
WasOverBefore bool `json:"was_over_before"`
|
|
ReductionPolicy sql.NullString `json:"reduction_policy"`
|
|
GoverningPolicy sql.NullString `json:"governing_policy"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
SetID string `json:"set_id"`
|
|
SetName string `json:"set_name"`
|
|
ChangeKind string `json:"change_kind"`
|
|
EffectiveAt time.Time `json:"effective_at"`
|
|
Note sql.NullString `json:"note"`
|
|
ActorType string `json:"actor_type"`
|
|
ActorPersonID uuid.NullUUID `json:"actor_person_id"`
|
|
ActorServiceAccountID uuid.NullUUID `json:"actor_service_account_id"`
|
|
}
|
|
|
|
// The organization's trail: every rule change that moved one of its pools.
|
|
// This is the effect row's audit projection (entitlement-set-history, "The
|
|
// change row and every effect row project onto the audit view shape"): the
|
|
// pool as the resource, the act's actor triple and occurrence reached through
|
|
// the foreign key, and the payload's fields, which carry both policies, the
|
|
// rule's own and the one that governed the pool and key.
|
|
func (q *Queries) ListEntitlementSetChangeEffectsByOrg(ctx context.Context, arg ListEntitlementSetChangeEffectsByOrgParams) ([]ListEntitlementSetChangeEffectsByOrgRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, listEntitlementSetChangeEffectsByOrg, arg.OrgID, arg.Limit, arg.Offset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListEntitlementSetChangeEffectsByOrgRow{}
|
|
for rows.Next() {
|
|
var i ListEntitlementSetChangeEffectsByOrgRow
|
|
if err := rows.Scan(
|
|
&i.EffectID,
|
|
&i.ChangeID,
|
|
&i.PoolID,
|
|
&i.OrgID,
|
|
&i.OrgName,
|
|
&i.ResourceKey,
|
|
&i.ResourceLabel,
|
|
&i.LimitBefore,
|
|
&i.LimitAfter,
|
|
&i.GrantedBefore,
|
|
&i.GrantedAfter,
|
|
&i.UsageAtEffect,
|
|
&i.OverUsage,
|
|
&i.WasOverBefore,
|
|
&i.ReductionPolicy,
|
|
&i.GoverningPolicy,
|
|
&i.CreatedAt,
|
|
&i.SetID,
|
|
&i.SetName,
|
|
&i.ChangeKind,
|
|
&i.EffectiveAt,
|
|
&i.Note,
|
|
&i.ActorType,
|
|
&i.ActorPersonID,
|
|
&i.ActorServiceAccountID,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listEntitlementSetChanges = `-- name: ListEntitlementSetChanges :many
|
|
SELECT c.change_id, c.set_id, c.set_name, c.change_kind, c.rule_id,
|
|
c.resource_key, c.resource_label, c.rule_before, c.rule_after, c.rule_type,
|
|
c.value_before, c.value_after, c.per_unit_before, c.per_unit_after,
|
|
c.actor_type, c.actor_person_id, c.actor_service_account_id,
|
|
c.note, c.request_id, c.sync_path, c.effective_at, c.created_at,
|
|
(SELECT COUNT(*) FROM core.entitlement_set_change_obligations o
|
|
WHERE o.change_id = c.change_id)::BIGINT AS pools_enumerated,
|
|
(SELECT COUNT(*) FROM core.entitlement_set_change_obligations o
|
|
WHERE o.change_id = c.change_id AND o.status = 'settled')::BIGINT AS pools_settled,
|
|
(SELECT COUNT(*) FROM core.entitlement_set_change_obligations o
|
|
WHERE o.change_id = c.change_id AND o.status = 'failed')::BIGINT AS pools_failed,
|
|
(SELECT COUNT(DISTINCT e.pool_id) FROM core.entitlement_set_change_effects e
|
|
WHERE e.change_id = c.change_id)::BIGINT AS pools_with_effects,
|
|
(SELECT COUNT(DISTINCT e.pool_id) FROM core.entitlement_set_change_effects e
|
|
WHERE e.change_id = c.change_id AND e.over_usage)::BIGINT AS pools_over_usage
|
|
FROM core.entitlement_set_changes c
|
|
WHERE c.set_id = $1
|
|
ORDER BY c.effective_at DESC, c.created_at DESC, c.change_id DESC
|
|
LIMIT $2 OFFSET $3
|
|
`
|
|
|
|
type ListEntitlementSetChangesParams struct {
|
|
SetID string `json:"set_id"`
|
|
Limit int32 `json:"limit"`
|
|
Offset int32 `json:"offset"`
|
|
}
|
|
|
|
type ListEntitlementSetChangesRow struct {
|
|
ChangeID string `json:"change_id"`
|
|
SetID string `json:"set_id"`
|
|
SetName string `json:"set_name"`
|
|
ChangeKind string `json:"change_kind"`
|
|
RuleID string `json:"rule_id"`
|
|
ResourceKey sql.NullString `json:"resource_key"`
|
|
ResourceLabel sql.NullString `json:"resource_label"`
|
|
RuleBefore pqtype.NullRawMessage `json:"rule_before"`
|
|
RuleAfter json.RawMessage `json:"rule_after"`
|
|
RuleType sql.NullString `json:"rule_type"`
|
|
ValueBefore sql.NullInt64 `json:"value_before"`
|
|
ValueAfter sql.NullInt64 `json:"value_after"`
|
|
PerUnitBefore sql.NullBool `json:"per_unit_before"`
|
|
PerUnitAfter sql.NullBool `json:"per_unit_after"`
|
|
ActorType string `json:"actor_type"`
|
|
ActorPersonID uuid.NullUUID `json:"actor_person_id"`
|
|
ActorServiceAccountID uuid.NullUUID `json:"actor_service_account_id"`
|
|
Note sql.NullString `json:"note"`
|
|
RequestID sql.NullString `json:"request_id"`
|
|
SyncPath string `json:"sync_path"`
|
|
EffectiveAt time.Time `json:"effective_at"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
PoolsEnumerated int64 `json:"pools_enumerated"`
|
|
PoolsSettled int64 `json:"pools_settled"`
|
|
PoolsFailed int64 `json:"pools_failed"`
|
|
PoolsWithEffects int64 `json:"pools_with_effects"`
|
|
PoolsOverUsage int64 `json:"pools_over_usage"`
|
|
}
|
|
|
|
// The paged History of one set. The per-change counts are correlated
|
|
// subqueries over the obligations and the effects, not stored columns.
|
|
func (q *Queries) ListEntitlementSetChanges(ctx context.Context, arg ListEntitlementSetChangesParams) ([]ListEntitlementSetChangesRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, listEntitlementSetChanges, arg.SetID, arg.Limit, arg.Offset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListEntitlementSetChangesRow{}
|
|
for rows.Next() {
|
|
var i ListEntitlementSetChangesRow
|
|
if err := rows.Scan(
|
|
&i.ChangeID,
|
|
&i.SetID,
|
|
&i.SetName,
|
|
&i.ChangeKind,
|
|
&i.RuleID,
|
|
&i.ResourceKey,
|
|
&i.ResourceLabel,
|
|
&i.RuleBefore,
|
|
&i.RuleAfter,
|
|
&i.RuleType,
|
|
&i.ValueBefore,
|
|
&i.ValueAfter,
|
|
&i.PerUnitBefore,
|
|
&i.PerUnitAfter,
|
|
&i.ActorType,
|
|
&i.ActorPersonID,
|
|
&i.ActorServiceAccountID,
|
|
&i.Note,
|
|
&i.RequestID,
|
|
&i.SyncPath,
|
|
&i.EffectiveAt,
|
|
&i.CreatedAt,
|
|
&i.PoolsEnumerated,
|
|
&i.PoolsSettled,
|
|
&i.PoolsFailed,
|
|
&i.PoolsWithEffects,
|
|
&i.PoolsOverUsage,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listFailedObligations = `-- name: ListFailedObligations :many
|
|
SELECT o.change_id, o.pool_id, o.attempts, o.last_attempt_at, o.last_error,
|
|
rp.org_id, org.name AS org_name, c.resource_key, c.resource_label
|
|
FROM core.entitlement_set_change_obligations o
|
|
JOIN core.entitlement_set_changes c ON c.change_id = o.change_id
|
|
JOIN core.resource_pools rp ON rp.pool_id = o.pool_id
|
|
JOIN core.organizations org ON org.org_id = rp.org_id
|
|
WHERE c.set_id = $1 AND o.status = 'failed'
|
|
ORDER BY org.name ASC, o.pool_id ASC
|
|
LIMIT $2
|
|
`
|
|
|
|
type ListFailedObligationsParams struct {
|
|
SetID string `json:"set_id"`
|
|
Limit int32 `json:"limit"`
|
|
}
|
|
|
|
type ListFailedObligationsRow struct {
|
|
ChangeID string `json:"change_id"`
|
|
PoolID string `json:"pool_id"`
|
|
Attempts int32 `json:"attempts"`
|
|
LastAttemptAt sql.NullTime `json:"last_attempt_at"`
|
|
LastError sql.NullString `json:"last_error"`
|
|
OrgID string `json:"org_id"`
|
|
OrgName string `json:"org_name"`
|
|
ResourceKey sql.NullString `json:"resource_key"`
|
|
ResourceLabel sql.NullString `json:"resource_label"`
|
|
}
|
|
|
|
func (q *Queries) ListFailedObligations(ctx context.Context, arg ListFailedObligationsParams) ([]ListFailedObligationsRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, listFailedObligations, arg.SetID, arg.Limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListFailedObligationsRow{}
|
|
for rows.Next() {
|
|
var i ListFailedObligationsRow
|
|
if err := rows.Scan(
|
|
&i.ChangeID,
|
|
&i.PoolID,
|
|
&i.Attempts,
|
|
&i.LastAttemptAt,
|
|
&i.LastError,
|
|
&i.OrgID,
|
|
&i.OrgName,
|
|
&i.ResourceKey,
|
|
&i.ResourceLabel,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listPendingObligations = `-- name: ListPendingObligations :many
|
|
SELECT change_id, pool_id, status, attempts, last_attempt_at, last_error, settled_at, created_at
|
|
FROM core.entitlement_set_change_obligations
|
|
WHERE change_id = $1 AND status = 'pending'
|
|
ORDER BY pool_id ASC
|
|
LIMIT $2
|
|
`
|
|
|
|
type ListPendingObligationsParams struct {
|
|
ChangeID string `json:"change_id"`
|
|
Limit int32 `json:"limit"`
|
|
}
|
|
|
|
// The drain's work list for one change. failed is the dead letter: the drain
|
|
// never picks one up again, and only RequeueFailedObligations, which the
|
|
// operator's Retry calls, returns it to pending.
|
|
func (q *Queries) ListPendingObligations(ctx context.Context, arg ListPendingObligationsParams) ([]EntitlementSetChangeObligation, error) {
|
|
rows, err := q.db.QueryContext(ctx, listPendingObligations, arg.ChangeID, arg.Limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []EntitlementSetChangeObligation{}
|
|
for rows.Next() {
|
|
var i EntitlementSetChangeObligation
|
|
if err := rows.Scan(
|
|
&i.ChangeID,
|
|
&i.PoolID,
|
|
&i.Status,
|
|
&i.Attempts,
|
|
&i.LastAttemptAt,
|
|
&i.LastError,
|
|
&i.SettledAt,
|
|
&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 markObligationFailed = `-- name: MarkObligationFailed :exec
|
|
SELECT core.settle_obligation(
|
|
$1::uuid,
|
|
$2::uuid,
|
|
'[]'::jsonb,
|
|
$3::text,
|
|
$4::integer
|
|
)
|
|
`
|
|
|
|
type MarkObligationFailedParams struct {
|
|
ChangeID string `json:"change_id"`
|
|
PoolID string `json:"pool_id"`
|
|
Failure string `json:"failure"`
|
|
MaxAttempts int32 `json:"max_attempts"`
|
|
}
|
|
|
|
// The failure half of the same function: no effect rows, the attempt and its
|
|
// error recorded, and the row failed once attempts reach max_attempts.
|
|
func (q *Queries) MarkObligationFailed(ctx context.Context, arg MarkObligationFailedParams) error {
|
|
_, err := q.db.ExecContext(ctx, markObligationFailed,
|
|
arg.ChangeID,
|
|
arg.PoolID,
|
|
arg.Failure,
|
|
arg.MaxAttempts,
|
|
)
|
|
return err
|
|
}
|
|
|
|
const requeueFailedObligations = `-- name: RequeueFailedObligations :exec
|
|
UPDATE core.entitlement_set_change_obligations o
|
|
SET status = 'pending', attempts = 0, last_error = NULL
|
|
FROM core.entitlement_set_changes c
|
|
WHERE c.change_id = o.change_id AND c.set_id = $1 AND o.status = 'failed'
|
|
`
|
|
|
|
// The Retry control: failed rows go back on the work list with their attempt
|
|
// count reset, so the drain gives each a fresh run of recomputeMaxAttempts.
|
|
func (q *Queries) RequeueFailedObligations(ctx context.Context, setID string) error {
|
|
_, err := q.db.ExecContext(ctx, requeueFailedObligations, setID)
|
|
return err
|
|
}
|
|
|
|
const settleObligation = `-- name: SettleObligation :one
|
|
SELECT core.settle_obligation(
|
|
$1::uuid,
|
|
$2::uuid,
|
|
$3::jsonb,
|
|
NULL
|
|
)::integer AS effects_written
|
|
`
|
|
|
|
type SettleObligationParams struct {
|
|
ChangeID string `json:"change_id"`
|
|
PoolID string `json:"pool_id"`
|
|
Effects json.RawMessage `json:"effects"`
|
|
}
|
|
|
|
// Records what one pool's recomputation did and marks the obligation. The
|
|
// calling transaction must hold the rendezvous in either mode.
|
|
func (q *Queries) SettleObligation(ctx context.Context, arg SettleObligationParams) (int32, error) {
|
|
row := q.db.QueryRowContext(ctx, settleObligation, arg.ChangeID, arg.PoolID, arg.Effects)
|
|
var effects_written int32
|
|
err := row.Scan(&effects_written)
|
|
return effects_written, err
|
|
}
|