refactor: sqlc vs raw SQL — hybrid approach (R3)

Deleted 8 genuinely unused sqlc queries (no inline equivalent):
- UpsertCurrentRelationship, ListEntitiesCapped, ListEntityStatus,
  UpdateSignalState, InsertClassification, InsertFeedback, InsertSkill,
  UpsertCurrentRelationship — all had zero call sites.

Migrated 9 inline raw SQL sites to use sqlc queries:
- GetOntology (impl.go): ListEntityTypes, ListRelationshipTypes,
  ListLifecycleDefs — replaces 3 raw pool.Query blocks with typed sqlcgen
  calls, eliminating manual row scanning.
- EndRelationship (phase3.go): EndCurrentRelationship — replaces tx.Exec
  with sqlcgen.New(tx).EndCurrentRelationship.
- checkPrecondition (impl.go): GetEntityStatus — replaces tx.QueryRow +
  manual Scan with sqlcgen.New(tx).GetEntityStatus.
- GetEntityRelations (impl.go): ListEntityRelations — replaces raw pool.Query
  + scanRelationships helper (now deleted).
- GetGraph (impl.go): ListGraphEdges — replaces raw pool.Query +
  scanRelationships.
- resolveEntityID (impl.go): GetEntityBySlug/GetEntityByID — replaces
  raw pool.QueryRow + Scan.
- createApproval (mcp/server.go): InsertApproval — replaces raw pool.Exec
  with sqlcgen.InsertApproval.

Deleted scanRelationships helper (was only used by the two migrated
graph queries above).

Regenerated sqlcgen — also picks up stale model updates (AgentSession,
SessionPlanStep, SessionQuestion, etc. from recent migrations).

Documented the carve-out in .agents/dev/CONTRIBUTING.md §SQL conventions:
sqlc is the default; raw pool.Query/Exec is reserved for LISTEN/NOTIFY,
dynamic WHERE builders, blast_radius(), and COPY.

go vet, build, httpapi/mcp/db tests all pass. -383/+170 lines.
This commit is contained in:
2026-07-17 22:24:23 +02:00
parent 0a3654b08f
commit d2950dd09d
12 changed files with 171 additions and 384 deletions

View File

@@ -60,19 +60,17 @@ func clampLimit(l *int) int {
// resolveEntityID resolves a UUID-or-slug path/query value to the entity UUID.
func (s *Server) resolveEntityID(ctx context.Context, idOrSlug string) (uuid.UUID, error) {
if id, err := uuid.Parse(idOrSlug); err == nil {
var found uuid.UUID
err := s.pool.QueryRow(ctx, "SELECT id FROM entities WHERE id = $1", id).Scan(&found)
if err == pgx.ErrNoRows {
entity, err := sqlcgen.New(s.pool).GetEntityByID(ctx, id)
if err != nil {
return uuid.Nil, fmt.Errorf("%w: %s", domain.ErrNotFound, idOrSlug)
}
return found, err
return entity.ID, nil
}
var id uuid.UUID
err := s.pool.QueryRow(ctx, "SELECT id FROM entities WHERE slug = $1", idOrSlug).Scan(&id)
if err == pgx.ErrNoRows {
entity, err := sqlcgen.New(s.pool).GetEntityBySlug(ctx, idOrSlug)
if err != nil {
return uuid.Nil, fmt.Errorf("%w: %s", domain.ErrNotFound, idOrSlug)
}
return id, err
return entity.ID, nil
}
// entityCols requires the entities table to be aliased as `e`, with
@@ -188,46 +186,37 @@ func (s *Server) GetEntityRelations(ctx context.Context, req gen.GetEntityRelati
if req.Params.Direction != nil {
dir = string(*req.Params.Direction)
}
rows, err := s.pool.Query(ctx, `
SELECT se.slug, te.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 (($3 IN ('out','both') AND r.source_id = $1)
OR ($3 IN ('in','both') AND r.target_id = $1))
AND ($2::text IS NULL OR r.type = $2)
ORDER BY r.type, se.slug, te.slug`,
id, req.Params.RelType, dir)
relType := req.Params.RelType
rows, err := sqlcgen.New(s.pool).ListEntityRelations(ctx, sqlcgen.ListEntityRelationsParams{
Direction: dir,
ID: id,
RelType: relType,
})
if err != nil {
return nil, err
}
items, err := scanRelationships(rows)
if err != nil {
return nil, err
items := []gen.Relationship{}
for _, r := range rows {
var attrs *map[string]any
if len(r.Attributes) > 0 {
var m map[string]any
if json.Unmarshal(r.Attributes, &m) == nil && len(m) > 0 {
attrs = &m
}
}
validTo := r.ValidTo
items = append(items, gen.Relationship{
Source: r.SourceSlug,
Target: r.TargetSlug,
Type: r.Type,
Attributes: attrs,
ValidFrom: r.ValidFrom,
ValidTo: validTo,
})
}
return gen.GetEntityRelations200JSONResponse{Items: items}, nil
}
func scanRelationships(rows pgx.Rows) ([]gen.Relationship, error) {
defer rows.Close()
items := []gen.Relationship{}
for rows.Next() {
var rel gen.Relationship
var attrsJSON []byte
if err := rows.Scan(&rel.Source, &rel.Target, &rel.Type,
&attrsJSON, &rel.ValidFrom, &rel.ValidTo); err != nil {
return nil, err
}
var attrs map[string]any
if len(attrsJSON) > 0 && json.Unmarshal(attrsJSON, &attrs) == nil && len(attrs) > 0 {
rel.Attributes = &attrs
}
items = append(items, rel)
}
return items, rows.Err()
}
func (s *Server) GetBlastRadius(ctx context.Context, req gen.GetBlastRadiusRequestObject) (gen.GetBlastRadiusResponseObject, error) {
id, err := s.resolveEntityID(ctx, req.Id)
if err != nil {
@@ -336,21 +325,31 @@ func (s *Server) GetGraph(ctx context.Context, req gen.GetGraphRequestObject) (g
for i, n := range nodes {
ids[i] = uuid.UUID(n.Id)
}
rows, err := s.pool.Query(ctx, `
SELECT se.slug, te.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) AND r.target_id = ANY($1)
AND ($2::text[] IS NULL OR r.type = ANY($2))
ORDER BY r.type, se.slug, te.slug`, ids, req.Params.RelType)
edgeRows, err := sqlcgen.New(s.pool).ListGraphEdges(ctx, sqlcgen.ListGraphEdgesParams{
Ids: ids,
RelTypes: *req.Params.RelType,
})
if err != nil {
return nil, err
}
edges, err := scanRelationships(rows)
if err != nil {
return nil, err
edges := []gen.Relationship{}
for _, r := range edgeRows {
var attrs *map[string]any
if len(r.Attributes) > 0 {
var m map[string]any
if json.Unmarshal(r.Attributes, &m) == nil && len(m) > 0 {
attrs = &m
}
}
validTo := r.ValidTo
edges = append(edges, gen.Relationship{
Source: r.SourceSlug,
Target: r.TargetSlug,
Type: r.Type,
Attributes: attrs,
ValidFrom: r.ValidFrom,
ValidTo: validTo,
})
}
resp := gen.GetGraph200JSONResponse{Nodes: nodes, Edges: edges}
@@ -421,78 +420,70 @@ func (s *Server) GetOntology(ctx context.Context, req gen.GetOntologyRequestObje
Lifecycles: []gen.LifecycleDef{},
}
rows, err := s.pool.Query(ctx, `
SELECT name, parent_type, is_abstract, domain, layer, description,
lifecycle_id, attribute_schema, schema_version, status
FROM entity_types ORDER BY name`)
q := sqlcgen.New(s.pool)
etRows, err := q.ListEntityTypes(ctx)
if err != nil {
return nil, err
}
for rows.Next() {
var et gen.EntityType
var schemaVersion int
var schemaJSON []byte
if err := rows.Scan(&et.Name, &et.ParentType, &et.IsAbstract, &et.Domain,
&et.Layer, &et.Description, &et.LifecycleId, &schemaJSON,
&schemaVersion, &et.Status); err != nil {
rows.Close()
return nil, err
for _, et := range etRows {
schemaVersion := int(et.SchemaVersion)
var schema *map[string]any
if len(et.AttributeSchema) > 0 {
var s map[string]any
if json.Unmarshal(et.AttributeSchema, &s) == nil && s != nil {
schema = &s
}
}
et.SchemaVersion = &schemaVersion
var schema map[string]any
if len(schemaJSON) > 0 && json.Unmarshal(schemaJSON, &schema) == nil && schema != nil {
et.AttributeSchema = &schema
}
resp.EntityTypes = append(resp.EntityTypes, et)
}
rows.Close()
if rows.Err() != nil {
return nil, rows.Err()
resp.EntityTypes = append(resp.EntityTypes, gen.EntityType{
Name: et.Name,
ParentType: et.ParentType,
IsAbstract: et.IsAbstract,
Domain: et.Domain,
Layer: gen.EntityTypeLayer(et.Layer),
Description: et.Description,
LifecycleId: et.LifecycleID,
SchemaVersion: &schemaVersion,
AttributeSchema: schema,
Status: gen.EntityTypeStatus(et.Status),
})
}
rows, err = s.pool.Query(ctx, `
SELECT name, inverse, source_type, target_type, cardinality, description
FROM relationship_types ORDER BY name`)
rtRows, err := q.ListRelationshipTypes(ctx)
if err != nil {
return nil, err
}
for rows.Next() {
var rt gen.RelationshipType
if err := rows.Scan(&rt.Name, &rt.Inverse, &rt.SourceType, &rt.TargetType,
&rt.Cardinality, &rt.Description); err != nil {
rows.Close()
return nil, err
}
resp.RelationshipTypes = append(resp.RelationshipTypes, rt)
}
rows.Close()
if rows.Err() != nil {
return nil, rows.Err()
for _, rt := range rtRows {
resp.RelationshipTypes = append(resp.RelationshipTypes, gen.RelationshipType{
Name: rt.Name,
Inverse: rt.Inverse,
SourceType: rt.SourceType,
TargetType: rt.TargetType,
Cardinality: gen.RelationshipTypeCardinality(rt.Cardinality),
Description: rt.Description,
})
}
rows, err = s.pool.Query(ctx, `
SELECT id, states, default_state, terminal_states, transitions
FROM lifecycle_defs ORDER BY id`)
lcRows, err := q.ListLifecycleDefs(ctx)
if err != nil {
return nil, err
}
for rows.Next() {
var lc gen.LifecycleDef
var terminal []string
var transJSON []byte
if err := rows.Scan(&lc.Id, &lc.States, &lc.DefaultState, &terminal, &transJSON); err != nil {
rows.Close()
return nil, err
for _, lc := range lcRows {
terminal := lc.TerminalStates
var transitions map[string]any
if err := json.Unmarshal(lc.Transitions, &transitions); err != nil {
return nil, fmt.Errorf("lifecycle %s transitions: %w", lc.ID, err)
}
lc.TerminalStates = &terminal
if err := json.Unmarshal(transJSON, &lc.Transitions); err != nil {
rows.Close()
return nil, fmt.Errorf("lifecycle %s transitions: %w", lc.Id, err)
}
resp.Lifecycles = append(resp.Lifecycles, lc)
resp.Lifecycles = append(resp.Lifecycles, gen.LifecycleDef{
Id: lc.ID,
States: lc.States,
DefaultState: lc.DefaultState,
TerminalStates: &terminal,
Transitions: transitions,
})
}
rows.Close()
return resp, rows.Err()
return resp, nil
}
// ─── Signals ──────────────────────────────────────────────────────────
@@ -1605,10 +1596,9 @@ func checkPrecondition(ctx context.Context, tx pgx.Tx, entityID uuid.UUID, entit
}
}
case "health-check-answering":
var health string
err := tx.QueryRow(ctx, "SELECT health FROM entity_status WHERE entity_id = $1", entityID).Scan(&health)
if err != nil || health == "unknown" || health == "down" {
return fmt.Errorf("health check not answering (status: %s)", health)
st, err := sqlcgen.New(tx).GetEntityStatus(ctx, entityID)
if err != nil || st.Health == "unknown" || st.Health == "down" {
return fmt.Errorf("health check not answering (status: %s)", st.Health)
}
case "doc-page-complete":
var count int

View File

@@ -2177,15 +2177,15 @@ func (s *Server) EndRelationship(ctx context.Context, req gen.EndRelationshipReq
}
defer tx.Rollback(ctx)
result, err := tx.Exec(ctx, `
UPDATE relationships
SET valid_to = now()
WHERE source_id = $1 AND target_id = $2 AND type = $3 AND valid_to IS NULL`,
sourceID, targetID, req.Params.RelType)
result, err := sqlcgen.New(tx).EndCurrentRelationship(ctx, sqlcgen.EndCurrentRelationshipParams{
SourceID: sourceID,
TargetID: targetID,
Type: req.Params.RelType,
})
if err != nil {
return nil, err
}
if result.RowsAffected() == 0 {
if result == 0 {
return nil, fmt.Errorf("%w: active relationship %s:%s:%s",
domain.ErrNotFound, req.Params.Source, req.Params.RelType, req.Params.Target)
}