feat: Phase 2 — ports package, secrets port move, postgres adapter move
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled

Problem: the hexagon's Phase 2 (plans/2026-08-15-hexagonal-architecture.md)
must give the use-cases-to-be their contract surface: driven-port
interfaces, test fakes, the secrets interface moved into core, and the
postgres package inside the adapters tree — before the first vertical
slice (Phase 3) can wire a composition root.

Change:
- internal/core/ports: full driven-port catalog per plan §3.3 —
  repositories as transaction-scoped aggregates whose inputs carry
  derived checks, audit, and events (§3.6), plus CommandExecutor,
  TargetResolver, Checker, Secrets, EventPublisher, Provisioner.
  Port-local payload types (Event, AuditEntry, CheckDef, KnowledgeEntry,
  ExecResult) keep signatures off infrastructure; TypeTree aliases
  internal/ontology (pure over domain) until checkdefaults is absorbed.
  ReadModels intentionally not declared yet — it materializes with the
  Phase 3 slice and grows as report handlers rewire.
- secrets.Backend is now an alias of ports.Secrets; implementations
  (Infisical, SOPS, Manager) unchanged. mcp's local secretBackend
  subset is deleted; tool constructors take ports.Secrets.
- internal/db → internal/adapters/postgres (mechanical import rewrite;
  package identifier stays db until the Phase 3 repository split).
  sqlc.yaml, Makefile, golangci exclusions, and docs follow the move;
  make generate-check verified.
- internal/adapters/ssh: Executor implements ports.CommandExecutor over
  the actuator dial pool + RunStreaming (10-min default timeout carried
  over from the httpapi path).
- internal/adapters/remote: Resolver implements ports.TargetResolver
  delegating to internal/remote (still pool-based; drops onto
  ports.EntityRepository when repositories land in Phase 3 — documented
  transitional import).
- internal/core/ports/portstest: importable fakes — in-memory
  EntityRepo (with check-then-act SetState, side-effect recording),
  RecordingExecutor, FakeChecker, SpyPublisher; port-satisfaction
  guards; tests.

Risk: ports are declared ahead of implementations — signatures firm up
per phase as slices land (documented in the package doc); the
remote→postgres transitional import is explicit and dissolves in
Phase 3.

Verification: go vet, make test (race, 19 packages), generate-check,
golangci on core+adapters — 0 issues; full-repo baseline down
365→344.
This commit is contained in:
2026-08-15 22:56:56 +02:00
parent d4d99a7473
commit 64f7d54011
93 changed files with 1102 additions and 103 deletions

View File

@@ -0,0 +1,32 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
package sqlcgen
import (
"context"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
type DBTX interface {
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
QueryRow(context.Context, string, ...interface{}) pgx.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
return &Queries{
db: tx,
}
}

View File

@@ -0,0 +1,275 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// source: entities.sql
package sqlcgen
import (
"context"
"time"
"github.com/google/uuid"
)
const getEntityByID = `-- name: GetEntityByID :one
SELECT e.id, e.slug, e.type, e.name, e.state, e.attributes, e.maintenance_until, e.version, e.created_at, e.updated_at, e.enrolled_at, e.enrolled_by FROM entities e WHERE e.id = $1
`
// Entity read + mutation queries (API paths). Aliased `e` throughout to
// avoid ambiguity with joined tables.
func (q *Queries) GetEntityByID(ctx context.Context, id uuid.UUID) (Entity, error) {
row := q.db.QueryRow(ctx, getEntityByID, id)
var i Entity
err := row.Scan(
&i.ID,
&i.Slug,
&i.Type,
&i.Name,
&i.State,
&i.Attributes,
&i.MaintenanceUntil,
&i.Version,
&i.CreatedAt,
&i.UpdatedAt,
&i.EnrolledAt,
&i.EnrolledBy,
)
return i, err
}
const getEntityBySlug = `-- name: GetEntityBySlug :one
SELECT e.id, e.slug, e.type, e.name, e.state, e.attributes, e.maintenance_until, e.version, e.created_at, e.updated_at, e.enrolled_at, e.enrolled_by FROM entities e WHERE e.slug = $1
`
func (q *Queries) GetEntityBySlug(ctx context.Context, slug string) (Entity, error) {
row := q.db.QueryRow(ctx, getEntityBySlug, slug)
var i Entity
err := row.Scan(
&i.ID,
&i.Slug,
&i.Type,
&i.Name,
&i.State,
&i.Attributes,
&i.MaintenanceUntil,
&i.Version,
&i.CreatedAt,
&i.UpdatedAt,
&i.EnrolledAt,
&i.EnrolledBy,
)
return i, err
}
const insertEntity = `-- name: InsertEntity :one
INSERT INTO entities (id, slug, type, name, state, attributes)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id, slug, type, name, state, attributes, maintenance_until, version, created_at, updated_at, enrolled_at, enrolled_by
`
type InsertEntityParams struct {
ID uuid.UUID
Slug string
Type string
Name string
State *string
Attributes []byte
}
func (q *Queries) InsertEntity(ctx context.Context, arg InsertEntityParams) (Entity, error) {
row := q.db.QueryRow(ctx, insertEntity,
arg.ID,
arg.Slug,
arg.Type,
arg.Name,
arg.State,
arg.Attributes,
)
var i Entity
err := row.Scan(
&i.ID,
&i.Slug,
&i.Type,
&i.Name,
&i.State,
&i.Attributes,
&i.MaintenanceUntil,
&i.Version,
&i.CreatedAt,
&i.UpdatedAt,
&i.EnrolledAt,
&i.EnrolledBy,
)
return i, err
}
const listEntities = `-- name: ListEntities :many
WITH RECURSIVE tt AS (
SELECT name FROM entity_types WHERE $7::text IS NULL OR name = $7
UNION
SELECT et.name FROM entity_types et JOIN tt ON et.parent_type = tt.name
WHERE $7::text IS NOT NULL
)
SELECT e.id, e.slug, e.type, e.name, e.state, e.attributes, e.maintenance_until, e.version, e.created_at, e.updated_at, e.enrolled_at, e.enrolled_by FROM entities e
JOIN entity_types et ON et.name = e.type
WHERE e.type IN (SELECT name FROM tt)
AND ($1::text IS NULL OR e.state = $1)
AND ($2::text IS NULL OR et.domain = $2)
AND ($3::text IS NULL OR et.layer = $3)
AND ($4::text IS NULL
OR e.slug ILIKE '%'||$4||'%'
OR e.name ILIKE '%'||$4||'%')
AND ($5::text IS NULL OR e.slug > $5)
ORDER BY e.slug
LIMIT $6
`
type ListEntitiesParams struct {
State *string
Domain *string
Layer *string
Q *string
Cursor *string
Lim int32
Type *string
}
func (q *Queries) ListEntities(ctx context.Context, arg ListEntitiesParams) ([]Entity, error) {
rows, err := q.db.Query(ctx, listEntities,
arg.State,
arg.Domain,
arg.Layer,
arg.Q,
arg.Cursor,
arg.Lim,
arg.Type,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Entity
for rows.Next() {
var i Entity
if err := rows.Scan(
&i.ID,
&i.Slug,
&i.Type,
&i.Name,
&i.State,
&i.Attributes,
&i.MaintenanceUntil,
&i.Version,
&i.CreatedAt,
&i.UpdatedAt,
&i.EnrolledAt,
&i.EnrolledBy,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const mergeEntityAttributes = `-- name: MergeEntityAttributes :execrows
UPDATE entities SET
attributes = attributes || $1::jsonb,
updated_at = now()
WHERE slug = $2
`
type MergeEntityAttributesParams struct {
Patch []byte
Slug string
}
// Shallow-merge a JSON patch into an entity's attributes (the
// update_entity_attributes MCP/HTTP surface). Replaces the raw
// `attributes = attributes || $2::jsonb` used in entity_tools.go.
func (q *Queries) MergeEntityAttributes(ctx context.Context, arg MergeEntityAttributesParams) (int64, error) {
result, err := q.db.Exec(ctx, mergeEntityAttributes, arg.Patch, arg.Slug)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const setEntityState = `-- name: SetEntityState :execrows
UPDATE entities SET
state = $1,
updated_at = now()
WHERE id = $2
`
type SetEntityStateParams struct {
State *string
ID uuid.UUID
}
// Set an entity's lifecycle state by id (the set_entity_state surface, run
// after db.ValidateTransition). Replaces the raw
// `UPDATE entities SET state = $2 ... WHERE id = $1`.
func (q *Queries) SetEntityState(ctx context.Context, arg SetEntityStateParams) (int64, error) {
result, err := q.db.Exec(ctx, setEntityState, arg.State, arg.ID)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const updateEntity = `-- name: UpdateEntity :one
UPDATE entities SET
name = COALESCE($1, name),
state = COALESCE($2, state),
attributes = COALESCE($3, attributes),
maintenance_until = CASE WHEN $4::bool
THEN $5 ELSE maintenance_until END,
version = version + 1,
updated_at = now()
WHERE id = $6 AND version = $7
RETURNING id, slug, type, name, state, attributes, maintenance_until, version, created_at, updated_at, enrolled_at, enrolled_by
`
type UpdateEntityParams struct {
Name *string
State *string
Attributes []byte
SetMaintenance bool
MaintenanceUntil *time.Time
ID uuid.UUID
Version int32
}
func (q *Queries) UpdateEntity(ctx context.Context, arg UpdateEntityParams) (Entity, error) {
row := q.db.QueryRow(ctx, updateEntity,
arg.Name,
arg.State,
arg.Attributes,
arg.SetMaintenance,
arg.MaintenanceUntil,
arg.ID,
arg.Version,
)
var i Entity
err := row.Scan(
&i.ID,
&i.Slug,
&i.Type,
&i.Name,
&i.State,
&i.Attributes,
&i.MaintenanceUntil,
&i.Version,
&i.CreatedAt,
&i.UpdatedAt,
&i.EnrolledAt,
&i.EnrolledBy,
)
return i, err
}

View File

@@ -0,0 +1,460 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
package sqlcgen
import (
"time"
"github.com/google/uuid"
)
type AgentActivity struct {
ID int64
Ts time.Time
AgentID uuid.UUID
SessionID *string
ActivityType string
ToolName *string
EntityID *uuid.UUID
InputSummary *string
OutputSummary *string
DurationMs *int32
TokenCount *int32
Success *bool
CorrelationID *string
}
type AgentMessage struct {
ID uuid.UUID
SessionID uuid.UUID
Role string
Content []byte
CreatedAt time.Time
}
type AgentSession struct {
ID uuid.UUID
Title string
Actor string
CreatedAt time.Time
LastActiveAt time.Time
Goal string
Status string
Outcome *string
Summary string
EntityID *uuid.UUID
CompletionNudges int32
Blocker string
ClosedAt *time.Time
}
type Approval struct {
EntityID uuid.UUID
SubjectEntityID *uuid.UUID
Action string
RiskClass string
Kind string
Payload []byte
Status string
TokenHash *string
ExpiresAt time.Time
DecidedAt *time.Time
DecidedBy *uuid.UUID
CreatedAt time.Time
MatrixEventID *string
AlertSentAt *time.Time
}
type ApprovalRule struct {
ID uuid.UUID
EntityType *string
Action string
RiskClass string
AutonomyLevel string
ScopeEntity *uuid.UUID
Version int32
UpdatedAt time.Time
}
type AuditLog struct {
ID int64
Ts time.Time
ActorType string
ActorID *uuid.UUID
Action string
EntityID *uuid.UUID
Method *string
Path *string
StatusCode *int32
Detail []byte
SourceIp *string
CorrelationID *string
SessionID *uuid.UUID
}
type AutonomySetting struct {
Key string
Value string
Version int32
UpdatedAt time.Time
}
type CheckDef struct {
EntityID uuid.UUID
TargetID *uuid.UUID
TargetType *string
Kind string
Config []byte
IntervalS int32
TimeoutS int32
Zone *string
Enabled bool
UpdatedAt time.Time
// When this check last executed. NULL = never, due immediately. Compared against interval_s to decide due-ness.
LastRunAt *time.Time
// This check's own most recent verdict (healthy/degraded/down/unknown). entity_status.health is the worst of these across the target's enabled checks.
LastHealth *string
}
type Classification struct {
EntityID uuid.UUID
SignalEntityID *uuid.UUID
TargetEntityID *uuid.UUID
Action string
RecommendedAction []byte
RiskClass string
Route string
BlastRadius []uuid.UUID
PatternConfidence *float32
SkillID *uuid.UUID
AutonomyCheck *string
Reasoning []byte
CorrelationID string
CreatedAt time.Time
}
type ContextFile struct {
Path string
Hash string
LastChanged time.Time
}
type ContextVersion struct {
Singleton bool
Version int64
UpdatedAt time.Time
}
type Entity struct {
ID uuid.UUID
Slug string
Type string
Name string
State *string
Attributes []byte
MaintenanceUntil *time.Time
Version int32
CreatedAt time.Time
UpdatedAt time.Time
EnrolledAt *time.Time
EnrolledBy *uuid.UUID
}
type EntityStatus struct {
EntityID uuid.UUID
Health string
LastCheckAt *time.Time
Details []byte
UpdatedAt time.Time
}
type EntityType struct {
Name string
ParentType *string
IsAbstract bool
Domain string
Layer string
Description *string
LifecycleID *string
AttributeSchema []byte
SchemaVersion int32
Status string
CreatedAt time.Time
UpdatedAt time.Time
// Check kinds this type warrants, resolved through parent_type. NULL means undeclared (an ontology gap), [] means explicitly unmonitorable, ["http","resource"] means declared kinds. Populated from seeds/ontology.yaml.
MonitoringSpec []byte
}
type Event struct {
ID int64
Ts time.Time
Type string
EntityID *uuid.UUID
Severity string
Source string
Data []byte
CorrelationID *string
}
type Execution struct {
EntityID uuid.UUID
ClassificationID *uuid.UUID
SignalEntityID *uuid.UUID
TargetEntityID *uuid.UUID
Action string
RiskClass string
ApprovalID *uuid.UUID
AgentID *uuid.UUID
SkillID *uuid.UUID
SkillVersion *int32
Status string
Result []byte
DurationMs *int32
Verified bool
CorrelationID string
StartedAt *time.Time
CompletedAt *time.Time
CreatedAt time.Time
}
type ExecutionLog struct {
ExecutionID uuid.UUID
Ts time.Time
Seq int32
Stream string
Chunk string
}
type Feedback struct {
EntityID uuid.UUID
ExecutionID uuid.UUID
Outcome string
Observation *string
Lesson *string
UnexpectedSideEffects []string
Tags []string
CreatedAt time.Time
}
type IdempotencyKey struct {
Key string
Actor string
RequestHash string
ResponseCode *int32
ResponseBody []byte
CreatedAt time.Time
}
type KnowledgeEntity struct {
EntityID uuid.UUID
Title string
Content string
Source *string
Tags []string
CreatedAt time.Time
UpdatedAt time.Time
ContentHash *string
Search interface{}
EditedBy string
DeletedAt *time.Time
}
type KnowledgeRevision struct {
ID int64
EntityID uuid.UUID
Title string
Content string
Source *string
Tags []string
EditedBy string
VersionAt time.Time
RevisedAt time.Time
}
type Ledger struct {
Ts time.Time
ExecutionID uuid.UUID
TargetEntityID *uuid.UUID
Action string
RiskClass string
Status string
Verified bool
Route *string
Reasoning []byte
ApprovalStatus *string
DecidedBy *uuid.UUID
AgentID *uuid.UUID
CorrelationID string
}
type LifecycleDef struct {
ID string
States []string
DefaultState string
TerminalStates []string
Transitions []byte
CreatedAt time.Time
}
type MetricRollups1d struct {
Bucket interface{}
EntityID uuid.UUID
Metric string
AvgValue float64
MinValue interface{}
MaxValue interface{}
SampleCount int64
}
type MetricRollups1h struct {
Bucket interface{}
EntityID uuid.UUID
Metric string
AvgValue float64
MinValue interface{}
MaxValue interface{}
SampleCount int64
}
type MetricSample struct {
Ts time.Time
EntityID uuid.UUID
Metric string
Value float64
Tags []byte
}
type NomosPlanExecution struct {
ExecutionID uuid.UUID
SessionID uuid.UUID
ContinuedAt *time.Time
CreatedAt time.Time
}
type Pattern struct {
EntityID uuid.UUID
AppliesType string
Action string
Pattern string
Confidence float32
EvidenceCount int32
SuccessCount int32
FailureCount int32
Status string
Quarantined bool
Version int32
LastValidatedAt *time.Time
CreatedAt time.Time
}
type ProvisioningStep struct {
ID uuid.UUID
EntityID uuid.UUID
ExecutionID uuid.UUID
StepOrder int32
StepName string
Status string
StartedAt *time.Time
FinishedAt *time.Time
ErrorMessage *string
CreatedAt time.Time
UpdatedAt time.Time
}
type Relationship struct {
SourceID uuid.UUID
TargetID uuid.UUID
Type string
Attributes []byte
ValidFrom time.Time
ValidTo *time.Time
}
type RelationshipType struct {
Name string
Inverse *string
SourceType string
TargetType string
Cardinality string
Description *string
CreatedAt time.Time
// Which end of this edge depends on the other. forward = target depends on source. backward = source depends on target. none = no runtime dependency. Drives blast_radius().
BlastDirection string
}
type RiskClass struct {
Name string
Description *string
ApprovalRequired string
AutonomyAllowed bool
}
type SeedVersion struct {
File string
ContentHash string
AppliedAt time.Time
}
type SessionPlanStep struct {
ID uuid.UUID
SessionID uuid.UUID
Seq int32
Title string
Detail string
Status string
ExecutionID *uuid.UUID
TargetSlug *string
StartedAt *time.Time
FinishedAt *time.Time
CreatedAt time.Time
Generation int32
ReplacedReason *string
}
type SessionQuestion struct {
ID uuid.UUID
SessionID uuid.UUID
Prompt string
Context []byte
Status string
Answer *string
CreatedAt time.Time
AnsweredAt *time.Time
}
type Signal struct {
EntityID uuid.UUID
Kind string
Severity string
TargetEntityID *uuid.UUID
CheckID *uuid.UUID
Evidence *string
LikelyCause *string
State string
OccurrenceCount int32
FirstSeenAt time.Time
LastSeenAt time.Time
FlapCount int32
HoldDownUntil *time.Time
MuteUntil *time.Time
CreatedAt time.Time
UpdatedAt time.Time
}
type Skill struct {
EntityID uuid.UUID
Version int32
Name string
Procedure []byte
AppliesType *string
Action string
PatternIds []uuid.UUID
Status string
SuccessRate *float32
ChangedBy *uuid.UUID
ChangeReason *string
LastUsedAt *time.Time
CreatedAt time.Time
}

View File

@@ -0,0 +1,132 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// source: ontology.sql
package sqlcgen
import (
"context"
)
const getLifecycleForType = `-- name: GetLifecycleForType :one
SELECT ld.id, ld.states, ld.default_state, ld.terminal_states, ld.transitions, ld.created_at FROM lifecycle_defs ld
JOIN entity_types et ON et.lifecycle_id = ld.id
WHERE et.name = $1
`
func (q *Queries) GetLifecycleForType(ctx context.Context, name string) (LifecycleDef, error) {
row := q.db.QueryRow(ctx, getLifecycleForType, name)
var i LifecycleDef
err := row.Scan(
&i.ID,
&i.States,
&i.DefaultState,
&i.TerminalStates,
&i.Transitions,
&i.CreatedAt,
)
return i, err
}
const listEntityTypes = `-- name: ListEntityTypes :many
SELECT name, parent_type, is_abstract, domain, layer, description, lifecycle_id, attribute_schema, schema_version, status, created_at, updated_at, monitoring_spec FROM entity_types ORDER BY name
`
func (q *Queries) ListEntityTypes(ctx context.Context) ([]EntityType, error) {
rows, err := q.db.Query(ctx, listEntityTypes)
if err != nil {
return nil, err
}
defer rows.Close()
var items []EntityType
for rows.Next() {
var i EntityType
if err := rows.Scan(
&i.Name,
&i.ParentType,
&i.IsAbstract,
&i.Domain,
&i.Layer,
&i.Description,
&i.LifecycleID,
&i.AttributeSchema,
&i.SchemaVersion,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
&i.MonitoringSpec,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listLifecycleDefs = `-- name: ListLifecycleDefs :many
SELECT id, states, default_state, terminal_states, transitions, created_at FROM lifecycle_defs ORDER BY id
`
func (q *Queries) ListLifecycleDefs(ctx context.Context) ([]LifecycleDef, error) {
rows, err := q.db.Query(ctx, listLifecycleDefs)
if err != nil {
return nil, err
}
defer rows.Close()
var items []LifecycleDef
for rows.Next() {
var i LifecycleDef
if err := rows.Scan(
&i.ID,
&i.States,
&i.DefaultState,
&i.TerminalStates,
&i.Transitions,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listRelationshipTypes = `-- name: ListRelationshipTypes :many
SELECT name, inverse, source_type, target_type, cardinality, description, created_at, blast_direction FROM relationship_types ORDER BY name
`
func (q *Queries) ListRelationshipTypes(ctx context.Context) ([]RelationshipType, error) {
rows, err := q.db.Query(ctx, listRelationshipTypes)
if err != nil {
return nil, err
}
defer rows.Close()
var items []RelationshipType
for rows.Next() {
var i RelationshipType
if err := rows.Scan(
&i.Name,
&i.Inverse,
&i.SourceType,
&i.TargetType,
&i.Cardinality,
&i.Description,
&i.CreatedAt,
&i.BlastDirection,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,177 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// source: relationships.sql
package sqlcgen
import (
"context"
"time"
"github.com/google/uuid"
)
const endCurrentRelationship = `-- name: EndCurrentRelationship :execrows
UPDATE relationships SET valid_to = now()
WHERE source_id = $1 AND target_id = $2 AND type = $3 AND valid_to IS NULL
`
type EndCurrentRelationshipParams struct {
SourceID uuid.UUID
TargetID uuid.UUID
Type string
}
func (q *Queries) EndCurrentRelationship(ctx context.Context, arg EndCurrentRelationshipParams) (int64, error) {
result, err := q.db.Exec(ctx, endCurrentRelationship, arg.SourceID, arg.TargetID, arg.Type)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const insertRelationshipIfAbsent = `-- name: InsertRelationshipIfAbsent :execrows
INSERT INTO relationships (source_id, target_id, type, attributes, valid_from)
SELECT $1, $2, $3,
$4::jsonb, now()
WHERE NOT EXISTS (
SELECT 1 FROM relationships
WHERE source_id = $1
AND target_id = $2
AND type = $3
AND valid_to IS NULL
)
`
type InsertRelationshipIfAbsentParams struct {
SourceID uuid.UUID
TargetID uuid.UUID
Type string
Attributes []byte
}
// Idempotent relationship insert (the create_relationship surface): no-op if
// an active edge of the same source/target/type already exists. Replaces the
// raw INSERT...WHERE NOT EXISTS used in entity_tools.go.
func (q *Queries) InsertRelationshipIfAbsent(ctx context.Context, arg InsertRelationshipIfAbsentParams) (int64, error) {
result, err := q.db.Exec(ctx, insertRelationshipIfAbsent,
arg.SourceID,
arg.TargetID,
arg.Type,
arg.Attributes,
)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const listEntityRelations = `-- name: ListEntityRelations :many
SELECT se.slug AS source_slug, te.slug AS target_slug, r.type, r.attributes,
r.valid_from, r.valid_to
FROM relationships r
JOIN entities se ON se.id = r.source_id
JOIN entities te ON te.id = r.target_id
WHERE r.valid_to IS NULL
AND (($1::text IN ('out','both') AND r.source_id = $2)
OR ($1::text IN ('in','both') AND r.target_id = $2))
AND ($3::text IS NULL OR r.type = $3)
ORDER BY r.type, se.slug, te.slug
`
type ListEntityRelationsParams struct {
Direction string
ID uuid.UUID
RelType *string
}
type ListEntityRelationsRow struct {
SourceSlug string
TargetSlug string
Type string
Attributes []byte
ValidFrom time.Time
ValidTo *time.Time
}
func (q *Queries) ListEntityRelations(ctx context.Context, arg ListEntityRelationsParams) ([]ListEntityRelationsRow, error) {
rows, err := q.db.Query(ctx, listEntityRelations, arg.Direction, arg.ID, arg.RelType)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListEntityRelationsRow
for rows.Next() {
var i ListEntityRelationsRow
if err := rows.Scan(
&i.SourceSlug,
&i.TargetSlug,
&i.Type,
&i.Attributes,
&i.ValidFrom,
&i.ValidTo,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listGraphEdges = `-- name: ListGraphEdges :many
SELECT se.slug AS source_slug, te.slug AS target_slug, r.type, r.attributes,
r.valid_from, r.valid_to
FROM relationships r
JOIN entities se ON se.id = r.source_id
JOIN entities te ON te.id = r.target_id
WHERE r.valid_to IS NULL
AND r.source_id = ANY($1::uuid[])
AND r.target_id = ANY($1::uuid[])
AND ($2::text[] IS NULL OR r.type = ANY($2::text[]))
ORDER BY r.type, se.slug, te.slug
`
type ListGraphEdgesParams struct {
Ids []uuid.UUID
RelTypes []string
}
type ListGraphEdgesRow struct {
SourceSlug string
TargetSlug string
Type string
Attributes []byte
ValidFrom time.Time
ValidTo *time.Time
}
func (q *Queries) ListGraphEdges(ctx context.Context, arg ListGraphEdgesParams) ([]ListGraphEdgesRow, error) {
rows, err := q.db.Query(ctx, listGraphEdges, arg.Ids, arg.RelTypes)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListGraphEdgesRow
for rows.Next() {
var i ListGraphEdgesRow
if err := rows.Scan(
&i.SourceSlug,
&i.TargetSlug,
&i.Type,
&i.Attributes,
&i.ValidFrom,
&i.ValidTo,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}