feat(backend): add projects and pr_enrichment tables to SQLite store
Migration 0002 adds two tables off the canonical CDC path: - projects: durable registry of managed repos (the twin of the old YAML config). Soft-deletable via archived_at so a session's project_id always resolves; ListProjects returns active rows only, GetProject resolves any. - pr_enrichment: per-session cache of rich SCM facts (CI summary, review decision, mergeability, pending comments, CI log tail) that do not live in the canonical lifecycle. 1:1 with a session, cascades on session delete. Both are written outside the LCM write path: no revision bump, no change_log/outbox event. Store methods mirror the reaction_trackers adapter pattern with storage-local row structs.
This commit is contained in:
parent
f5bc4c7b8c
commit
23b8fe43cf
|
|
@ -34,6 +34,31 @@ type Outbox struct {
|
|||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type PrEnrichment struct {
|
||||
SessionID string
|
||||
CiSummary string
|
||||
ReviewDecision string
|
||||
Mergeability string
|
||||
PendingComments string
|
||||
CiLogTail string
|
||||
LastFetchedAt time.Time
|
||||
}
|
||||
|
||||
type Project struct {
|
||||
ID string
|
||||
Path string
|
||||
RepoOwner string
|
||||
RepoName string
|
||||
RepoPlatform string
|
||||
RepoOriginUrl string
|
||||
DefaultBranch string
|
||||
DisplayName string
|
||||
SessionPrefix string
|
||||
Source string
|
||||
RegisteredAt time.Time
|
||||
ArchivedAt sql.NullTime
|
||||
}
|
||||
|
||||
type ReactionTracker struct {
|
||||
SessionID string
|
||||
ReactionKey string
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.31.1
|
||||
// source: pr_enrichment.sql
|
||||
|
||||
package gen
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
const deletePREnrichment = `-- name: DeletePREnrichment :exec
|
||||
DELETE FROM pr_enrichment WHERE session_id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) DeletePREnrichment(ctx context.Context, sessionID string) error {
|
||||
_, err := q.db.ExecContext(ctx, deletePREnrichment, sessionID)
|
||||
return err
|
||||
}
|
||||
|
||||
const getPREnrichment = `-- name: GetPREnrichment :one
|
||||
SELECT session_id, ci_summary, review_decision, mergeability, pending_comments, ci_log_tail, last_fetched_at
|
||||
FROM pr_enrichment
|
||||
WHERE session_id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) GetPREnrichment(ctx context.Context, sessionID string) (PrEnrichment, error) {
|
||||
row := q.db.QueryRowContext(ctx, getPREnrichment, sessionID)
|
||||
var i PrEnrichment
|
||||
err := row.Scan(
|
||||
&i.SessionID,
|
||||
&i.CiSummary,
|
||||
&i.ReviewDecision,
|
||||
&i.Mergeability,
|
||||
&i.PendingComments,
|
||||
&i.CiLogTail,
|
||||
&i.LastFetchedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const upsertPREnrichment = `-- name: UpsertPREnrichment :exec
|
||||
INSERT INTO pr_enrichment (session_id, ci_summary, review_decision, mergeability, pending_comments, ci_log_tail, last_fetched_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (session_id) DO UPDATE SET
|
||||
ci_summary = excluded.ci_summary,
|
||||
review_decision = excluded.review_decision,
|
||||
mergeability = excluded.mergeability,
|
||||
pending_comments = excluded.pending_comments,
|
||||
ci_log_tail = excluded.ci_log_tail,
|
||||
last_fetched_at = excluded.last_fetched_at
|
||||
`
|
||||
|
||||
type UpsertPREnrichmentParams struct {
|
||||
SessionID string
|
||||
CiSummary string
|
||||
ReviewDecision string
|
||||
Mergeability string
|
||||
PendingComments string
|
||||
CiLogTail string
|
||||
LastFetchedAt time.Time
|
||||
}
|
||||
|
||||
func (q *Queries) UpsertPREnrichment(ctx context.Context, arg UpsertPREnrichmentParams) error {
|
||||
_, err := q.db.ExecContext(ctx, upsertPREnrichment,
|
||||
arg.SessionID,
|
||||
arg.CiSummary,
|
||||
arg.ReviewDecision,
|
||||
arg.Mergeability,
|
||||
arg.PendingComments,
|
||||
arg.CiLogTail,
|
||||
arg.LastFetchedAt,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.31.1
|
||||
// source: projects.sql
|
||||
|
||||
package gen
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
const archiveProject = `-- name: ArchiveProject :exec
|
||||
UPDATE projects SET archived_at = ? WHERE id = ?
|
||||
`
|
||||
|
||||
type ArchiveProjectParams struct {
|
||||
ArchivedAt sql.NullTime
|
||||
ID string
|
||||
}
|
||||
|
||||
func (q *Queries) ArchiveProject(ctx context.Context, arg ArchiveProjectParams) error {
|
||||
_, err := q.db.ExecContext(ctx, archiveProject, arg.ArchivedAt, arg.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteProject = `-- name: DeleteProject :exec
|
||||
DELETE FROM projects WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteProject(ctx context.Context, id string) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteProject, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const getProject = `-- name: GetProject :one
|
||||
SELECT id, path, repo_owner, repo_name, repo_platform, repo_origin_url, default_branch, display_name, session_prefix, source, registered_at, archived_at
|
||||
FROM projects
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) GetProject(ctx context.Context, id string) (Project, error) {
|
||||
row := q.db.QueryRowContext(ctx, getProject, id)
|
||||
var i Project
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Path,
|
||||
&i.RepoOwner,
|
||||
&i.RepoName,
|
||||
&i.RepoPlatform,
|
||||
&i.RepoOriginUrl,
|
||||
&i.DefaultBranch,
|
||||
&i.DisplayName,
|
||||
&i.SessionPrefix,
|
||||
&i.Source,
|
||||
&i.RegisteredAt,
|
||||
&i.ArchivedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listProjects = `-- name: ListProjects :many
|
||||
SELECT id, path, repo_owner, repo_name, repo_platform, repo_origin_url, default_branch, display_name, session_prefix, source, registered_at, archived_at
|
||||
FROM projects
|
||||
WHERE archived_at IS NULL
|
||||
ORDER BY id
|
||||
`
|
||||
|
||||
func (q *Queries) ListProjects(ctx context.Context) ([]Project, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listProjects)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []Project{}
|
||||
for rows.Next() {
|
||||
var i Project
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Path,
|
||||
&i.RepoOwner,
|
||||
&i.RepoName,
|
||||
&i.RepoPlatform,
|
||||
&i.RepoOriginUrl,
|
||||
&i.DefaultBranch,
|
||||
&i.DisplayName,
|
||||
&i.SessionPrefix,
|
||||
&i.Source,
|
||||
&i.RegisteredAt,
|
||||
&i.ArchivedAt,
|
||||
); 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 upsertProject = `-- name: UpsertProject :exec
|
||||
INSERT INTO projects (id, path, repo_owner, repo_name, repo_platform, repo_origin_url, default_branch, display_name, session_prefix, source, registered_at, archived_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
path = excluded.path,
|
||||
repo_owner = excluded.repo_owner,
|
||||
repo_name = excluded.repo_name,
|
||||
repo_platform = excluded.repo_platform,
|
||||
repo_origin_url = excluded.repo_origin_url,
|
||||
default_branch = excluded.default_branch,
|
||||
display_name = excluded.display_name,
|
||||
session_prefix = excluded.session_prefix,
|
||||
source = excluded.source,
|
||||
registered_at = excluded.registered_at,
|
||||
archived_at = excluded.archived_at
|
||||
`
|
||||
|
||||
type UpsertProjectParams struct {
|
||||
ID string
|
||||
Path string
|
||||
RepoOwner string
|
||||
RepoName string
|
||||
RepoPlatform string
|
||||
RepoOriginUrl string
|
||||
DefaultBranch string
|
||||
DisplayName string
|
||||
SessionPrefix string
|
||||
Source string
|
||||
RegisteredAt time.Time
|
||||
ArchivedAt sql.NullTime
|
||||
}
|
||||
|
||||
func (q *Queries) UpsertProject(ctx context.Context, arg UpsertProjectParams) error {
|
||||
_, err := q.db.ExecContext(ctx, upsertProject,
|
||||
arg.ID,
|
||||
arg.Path,
|
||||
arg.RepoOwner,
|
||||
arg.RepoName,
|
||||
arg.RepoPlatform,
|
||||
arg.RepoOriginUrl,
|
||||
arg.DefaultBranch,
|
||||
arg.DisplayName,
|
||||
arg.SessionPrefix,
|
||||
arg.Source,
|
||||
arg.RegisteredAt,
|
||||
arg.ArchivedAt,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
|
@ -9,11 +9,16 @@ import (
|
|||
)
|
||||
|
||||
type Querier interface {
|
||||
ArchiveProject(ctx context.Context, arg ArchiveProjectParams) error
|
||||
DeletePREnrichment(ctx context.Context, sessionID string) error
|
||||
DeleteProject(ctx context.Context, id string) error
|
||||
DeleteReactionTracker(ctx context.Context, arg DeleteReactionTrackerParams) error
|
||||
DeleteSentOutboxBelow(ctx context.Context, changeLogSeq int64) (int64, error)
|
||||
DeleteSessionReactionTrackers(ctx context.Context, sessionID string) error
|
||||
GetConsumerOffset(ctx context.Context, consumer string) (int64, error)
|
||||
GetMetadata(ctx context.Context, sessionID string) ([]GetMetadataRow, error)
|
||||
GetPREnrichment(ctx context.Context, sessionID string) (PrEnrichment, error)
|
||||
GetProject(ctx context.Context, id string) (Project, error)
|
||||
GetSession(ctx context.Context, id string) (Session, error)
|
||||
GetSessionRevision(ctx context.Context, id string) (int64, error)
|
||||
// Appends a canonical-write record and returns its monotonic seq so the same
|
||||
|
|
@ -24,6 +29,7 @@ type Querier interface {
|
|||
// the row is persisted at revision 1.
|
||||
InsertSession(ctx context.Context, arg InsertSessionParams) (int64, error)
|
||||
ListAllSessions(ctx context.Context) ([]Session, error)
|
||||
ListProjects(ctx context.Context) ([]Project, error)
|
||||
ListReactionTrackers(ctx context.Context) ([]ReactionTracker, error)
|
||||
ListSessionsByProject(ctx context.Context, projectID string) ([]Session, error)
|
||||
ListUnsentOutbox(ctx context.Context, limit int64) ([]ListUnsentOutboxRow, error)
|
||||
|
|
@ -36,6 +42,8 @@ type Querier interface {
|
|||
UpdateSessionCAS(ctx context.Context, arg UpdateSessionCASParams) (int64, error)
|
||||
UpsertConsumerOffset(ctx context.Context, arg UpsertConsumerOffsetParams) error
|
||||
UpsertMetadata(ctx context.Context, arg UpsertMetadataParams) error
|
||||
UpsertPREnrichment(ctx context.Context, arg UpsertPREnrichmentParams) error
|
||||
UpsertProject(ctx context.Context, arg UpsertProjectParams) error
|
||||
UpsertReactionTracker(ctx context.Context, arg UpsertReactionTrackerParams) error
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
|
||||
-- projects is the durable registry of repos AO manages, the SQLite twin of the
|
||||
-- old YAML config (global config.yaml + per-repo agent-orchestrator.yaml). id is
|
||||
-- the {basename}_{sha256(path:originUrl)[:10]} key the session layer references
|
||||
-- via sessions.project_id. The relationship is app-enforced, NOT a hard FK:
|
||||
-- SQLite cannot ALTER ADD a FK without a table rebuild, and an existing-session
|
||||
-- backfill may land sessions before their project row.
|
||||
CREATE TABLE projects (
|
||||
id TEXT PRIMARY KEY,
|
||||
path TEXT NOT NULL,
|
||||
repo_owner TEXT NOT NULL DEFAULT '',
|
||||
repo_name TEXT NOT NULL DEFAULT '',
|
||||
repo_platform TEXT NOT NULL DEFAULT '',
|
||||
repo_origin_url TEXT NOT NULL DEFAULT '',
|
||||
default_branch TEXT NOT NULL DEFAULT '',
|
||||
display_name TEXT NOT NULL DEFAULT '',
|
||||
session_prefix TEXT NOT NULL DEFAULT '',
|
||||
source TEXT NOT NULL DEFAULT '',
|
||||
registered_at TIMESTAMP NOT NULL,
|
||||
|
||||
-- soft delete: NULL = active. Archiving keeps the row so a session's
|
||||
-- project_id always resolves (there is no FK to enforce it), avoiding
|
||||
-- dangling references; active-only reads filter archived_at IS NULL.
|
||||
archived_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- pr_enrichment is the SCM observer's per-session cache of the rich PR facts that
|
||||
-- do NOT live in the canonical lifecycle (which keeps only pr_state/reason/number/
|
||||
-- url). It is 1:1 with a session (a PR is always tied to a session by its branch),
|
||||
-- written by the SCM observer OFF the canonical CDC path (no revision bump, no
|
||||
-- change_log/outbox event), and cascades away with its session.
|
||||
CREATE TABLE pr_enrichment (
|
||||
session_id TEXT PRIMARY KEY REFERENCES sessions (id) ON DELETE CASCADE,
|
||||
ci_summary TEXT NOT NULL DEFAULT '',
|
||||
review_decision TEXT NOT NULL DEFAULT '',
|
||||
mergeability TEXT NOT NULL DEFAULT '',
|
||||
pending_comments TEXT NOT NULL DEFAULT '',
|
||||
ci_log_tail TEXT NOT NULL DEFAULT '',
|
||||
last_fetched_at TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE pr_enrichment;
|
||||
DROP TABLE projects;
|
||||
-- +goose StatementEnd
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aoagents/agent-orchestrator/backend/internal/ports"
|
||||
)
|
||||
|
||||
func TestProjectUpsertGetListDelete(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
now := time.Now().UTC().Truncate(time.Second)
|
||||
|
||||
if _, ok, err := s.GetProject(ctx, "p1"); err != nil || ok {
|
||||
t.Fatalf("get missing: ok=%v err=%v", ok, err)
|
||||
}
|
||||
|
||||
p := ProjectRow{
|
||||
ID: "p1", Path: "/repo", RepoOwner: "acme", RepoName: "widget",
|
||||
RepoPlatform: "github", RepoOriginURL: "git@github.com:acme/widget.git",
|
||||
DefaultBranch: "main", DisplayName: "Widget", SessionPrefix: "wid",
|
||||
Source: "local", RegisteredAt: now,
|
||||
}
|
||||
if err := s.UpsertProject(ctx, p); err != nil {
|
||||
t.Fatalf("upsert: %v", err)
|
||||
}
|
||||
|
||||
got, ok, err := s.GetProject(ctx, "p1")
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("get: ok=%v err=%v", ok, err)
|
||||
}
|
||||
if got != p {
|
||||
t.Fatalf("round-trip mismatch:\n got %+v\nwant %+v", got, p)
|
||||
}
|
||||
|
||||
// Upsert again with a changed field updates in place (no duplicate).
|
||||
p.DisplayName = "Widget 2"
|
||||
if err := s.UpsertProject(ctx, p); err != nil {
|
||||
t.Fatalf("re-upsert: %v", err)
|
||||
}
|
||||
list, err := s.ListProjects(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("list: %v", err)
|
||||
}
|
||||
if len(list) != 1 || list[0].DisplayName != "Widget 2" {
|
||||
t.Fatalf("list after re-upsert = %+v", list)
|
||||
}
|
||||
|
||||
if err := s.DeleteProject(ctx, "p1"); err != nil {
|
||||
t.Fatalf("delete: %v", err)
|
||||
}
|
||||
if _, ok, _ := s.GetProject(ctx, "p1"); ok {
|
||||
t.Fatal("project should be gone after delete")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveProjectHidesFromListButGetResolves(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
now := time.Now().UTC().Truncate(time.Second)
|
||||
|
||||
if err := s.UpsertProject(ctx, ProjectRow{ID: "p1", Path: "/repo", RegisteredAt: now}); err != nil {
|
||||
t.Fatalf("upsert: %v", err)
|
||||
}
|
||||
if err := s.ArchiveProject(ctx, "p1", now); err != nil {
|
||||
t.Fatalf("archive: %v", err)
|
||||
}
|
||||
|
||||
// Active-only list hides it.
|
||||
list, err := s.ListProjects(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("list: %v", err)
|
||||
}
|
||||
if len(list) != 0 {
|
||||
t.Fatalf("archived project should not appear in ListProjects, got %+v", list)
|
||||
}
|
||||
|
||||
// Get still resolves it (a session's project_id must not dangle) and reports
|
||||
// the archived marker.
|
||||
got, ok, err := s.GetProject(ctx, "p1")
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("get archived: ok=%v err=%v", ok, err)
|
||||
}
|
||||
if got.ArchivedAt.IsZero() {
|
||||
t.Fatal("archived project should carry a non-zero ArchivedAt")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPREnrichmentUpsertGetDelete(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
now := time.Now().UTC().Truncate(time.Second)
|
||||
|
||||
// pr_enrichment FKs sessions(id); seed the session first.
|
||||
if err := s.Upsert(ctx, sampleRecord("s1"), ports.EventSessionCreated); err != nil {
|
||||
t.Fatalf("seed session: %v", err)
|
||||
}
|
||||
|
||||
if _, ok, err := s.GetPREnrichment(ctx, "s1"); err != nil || ok {
|
||||
t.Fatalf("get missing: ok=%v err=%v", ok, err)
|
||||
}
|
||||
|
||||
e := PREnrichmentRow{
|
||||
SessionID: "s1", CISummary: "3 passing, 1 failing", ReviewDecision: "changes_requested",
|
||||
Mergeability: "blocked", PendingComments: `[{"path":"a.go"}]`, CILogTail: "FAIL TestX",
|
||||
LastFetchedAt: now,
|
||||
}
|
||||
if err := s.UpsertPREnrichment(ctx, e); err != nil {
|
||||
t.Fatalf("upsert: %v", err)
|
||||
}
|
||||
|
||||
got, ok, err := s.GetPREnrichment(ctx, "s1")
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("get: ok=%v err=%v", ok, err)
|
||||
}
|
||||
if got != e {
|
||||
t.Fatalf("round-trip mismatch:\n got %+v\nwant %+v", got, e)
|
||||
}
|
||||
|
||||
if err := s.DeletePREnrichment(ctx, "s1"); err != nil {
|
||||
t.Fatalf("delete: %v", err)
|
||||
}
|
||||
if _, ok, _ := s.GetPREnrichment(ctx, "s1"); ok {
|
||||
t.Fatal("enrichment should be gone after delete")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/aoagents/agent-orchestrator/backend/internal/storage/sqlite/gen"
|
||||
)
|
||||
|
||||
// PREnrichmentRow is the SCM observer's cache of the rich PR facts that do not
|
||||
// live in the canonical lifecycle (which keeps only pr_state/reason/number/url).
|
||||
// It is 1:1 with a session and written OFF the canonical CDC path: upserting it
|
||||
// never bumps revision and never emits a change_log/outbox event. pending_comments
|
||||
// and ci_log_tail are opaque blobs the SCM observer serializes.
|
||||
type PREnrichmentRow struct {
|
||||
SessionID string
|
||||
CISummary string
|
||||
ReviewDecision string
|
||||
Mergeability string
|
||||
PendingComments string
|
||||
CILogTail string
|
||||
LastFetchedAt time.Time
|
||||
}
|
||||
|
||||
// UpsertPREnrichment inserts or replaces the cached PR facts for one session.
|
||||
func (s *Store) UpsertPREnrichment(ctx context.Context, r PREnrichmentRow) error {
|
||||
return s.q.UpsertPREnrichment(ctx, gen.UpsertPREnrichmentParams{
|
||||
SessionID: r.SessionID,
|
||||
CiSummary: r.CISummary,
|
||||
ReviewDecision: r.ReviewDecision,
|
||||
Mergeability: r.Mergeability,
|
||||
PendingComments: r.PendingComments,
|
||||
CiLogTail: r.CILogTail,
|
||||
LastFetchedAt: r.LastFetchedAt,
|
||||
})
|
||||
}
|
||||
|
||||
// GetPREnrichment returns the cached PR facts for one session. ok is false when
|
||||
// no row exists (the SCM observer has not yet fetched, or the session has no PR).
|
||||
func (s *Store) GetPREnrichment(ctx context.Context, sessionID string) (PREnrichmentRow, bool, error) {
|
||||
e, err := s.q.GetPREnrichment(ctx, sessionID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return PREnrichmentRow{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return PREnrichmentRow{}, false, fmt.Errorf("get pr enrichment: %w", err)
|
||||
}
|
||||
return PREnrichmentRow{
|
||||
SessionID: e.SessionID,
|
||||
CISummary: e.CiSummary,
|
||||
ReviewDecision: e.ReviewDecision,
|
||||
Mergeability: e.Mergeability,
|
||||
PendingComments: e.PendingComments,
|
||||
CILogTail: e.CiLogTail,
|
||||
LastFetchedAt: e.LastFetchedAt,
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
// DeletePREnrichment drops the cached PR facts for one session. Normally
|
||||
// unnecessary (the FK cascades on session delete), exposed for explicit eviction.
|
||||
func (s *Store) DeletePREnrichment(ctx context.Context, sessionID string) error {
|
||||
return s.q.DeletePREnrichment(ctx, sessionID)
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/aoagents/agent-orchestrator/backend/internal/storage/sqlite/gen"
|
||||
)
|
||||
|
||||
// ProjectRow is one registered repo, the durable twin of the old YAML config
|
||||
// entry. It is the unit the registration path upserts and cross-project readers
|
||||
// list. Off the canonical CDC path: writing a project never emits a change_log
|
||||
// or outbox event.
|
||||
type ProjectRow struct {
|
||||
ID string
|
||||
Path string
|
||||
RepoOwner string
|
||||
RepoName string
|
||||
RepoPlatform string
|
||||
RepoOriginURL string
|
||||
DefaultBranch string
|
||||
DisplayName string
|
||||
SessionPrefix string
|
||||
Source string
|
||||
RegisteredAt time.Time
|
||||
// ArchivedAt is the soft-delete marker; zero means active. GetProject returns
|
||||
// it regardless of state (so a session can resolve its archived project);
|
||||
// ListProjects returns only rows where it is zero.
|
||||
ArchivedAt time.Time
|
||||
}
|
||||
|
||||
// UpsertProject inserts or updates one registered project.
|
||||
func (s *Store) UpsertProject(ctx context.Context, r ProjectRow) error {
|
||||
return s.q.UpsertProject(ctx, gen.UpsertProjectParams{
|
||||
ID: r.ID,
|
||||
Path: r.Path,
|
||||
RepoOwner: r.RepoOwner,
|
||||
RepoName: r.RepoName,
|
||||
RepoPlatform: r.RepoPlatform,
|
||||
RepoOriginUrl: r.RepoOriginURL,
|
||||
DefaultBranch: r.DefaultBranch,
|
||||
DisplayName: r.DisplayName,
|
||||
SessionPrefix: r.SessionPrefix,
|
||||
Source: r.Source,
|
||||
RegisteredAt: r.RegisteredAt,
|
||||
ArchivedAt: nullTime(r.ArchivedAt),
|
||||
})
|
||||
}
|
||||
|
||||
// ArchiveProject soft-deletes one project, keeping the row so a session's
|
||||
// project_id still resolves. Active-only reads (ListProjects) then hide it.
|
||||
func (s *Store) ArchiveProject(ctx context.Context, id string, t time.Time) error {
|
||||
return s.q.ArchiveProject(ctx, gen.ArchiveProjectParams{
|
||||
ArchivedAt: nullTime(t),
|
||||
ID: id,
|
||||
})
|
||||
}
|
||||
|
||||
// GetProject returns one project by id. ok is false when no row exists.
|
||||
func (s *Store) GetProject(ctx context.Context, id string) (ProjectRow, bool, error) {
|
||||
p, err := s.q.GetProject(ctx, id)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ProjectRow{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return ProjectRow{}, false, fmt.Errorf("get project: %w", err)
|
||||
}
|
||||
return projectRowFromGen(p), true, nil
|
||||
}
|
||||
|
||||
// ListProjects returns every registered project, ordered by id.
|
||||
func (s *Store) ListProjects(ctx context.Context) ([]ProjectRow, error) {
|
||||
rows, err := s.q.ListProjects(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list projects: %w", err)
|
||||
}
|
||||
out := make([]ProjectRow, 0, len(rows))
|
||||
for _, p := range rows {
|
||||
out = append(out, projectRowFromGen(p))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// DeleteProject removes one project by id.
|
||||
func (s *Store) DeleteProject(ctx context.Context, id string) error {
|
||||
return s.q.DeleteProject(ctx, id)
|
||||
}
|
||||
|
||||
func projectRowFromGen(p gen.Project) ProjectRow {
|
||||
return ProjectRow{
|
||||
ID: p.ID,
|
||||
Path: p.Path,
|
||||
RepoOwner: p.RepoOwner,
|
||||
RepoName: p.RepoName,
|
||||
RepoPlatform: p.RepoPlatform,
|
||||
RepoOriginURL: p.RepoOriginUrl,
|
||||
DefaultBranch: p.DefaultBranch,
|
||||
DisplayName: p.DisplayName,
|
||||
SessionPrefix: p.SessionPrefix,
|
||||
Source: p.Source,
|
||||
RegisteredAt: p.RegisteredAt,
|
||||
ArchivedAt: p.ArchivedAt.Time,
|
||||
}
|
||||
}
|
||||
|
||||
// nullTime maps a zero time.Time to a NULL column, else a valid timestamp.
|
||||
func nullTime(t time.Time) sql.NullTime {
|
||||
if t.IsZero() {
|
||||
return sql.NullTime{}
|
||||
}
|
||||
return sql.NullTime{Time: t, Valid: true}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
-- name: UpsertPREnrichment :exec
|
||||
INSERT INTO pr_enrichment (session_id, ci_summary, review_decision, mergeability, pending_comments, ci_log_tail, last_fetched_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (session_id) DO UPDATE SET
|
||||
ci_summary = excluded.ci_summary,
|
||||
review_decision = excluded.review_decision,
|
||||
mergeability = excluded.mergeability,
|
||||
pending_comments = excluded.pending_comments,
|
||||
ci_log_tail = excluded.ci_log_tail,
|
||||
last_fetched_at = excluded.last_fetched_at;
|
||||
|
||||
-- name: GetPREnrichment :one
|
||||
SELECT session_id, ci_summary, review_decision, mergeability, pending_comments, ci_log_tail, last_fetched_at
|
||||
FROM pr_enrichment
|
||||
WHERE session_id = ?;
|
||||
|
||||
-- name: DeletePREnrichment :exec
|
||||
DELETE FROM pr_enrichment WHERE session_id = ?;
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
-- name: UpsertProject :exec
|
||||
INSERT INTO projects (id, path, repo_owner, repo_name, repo_platform, repo_origin_url, default_branch, display_name, session_prefix, source, registered_at, archived_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
path = excluded.path,
|
||||
repo_owner = excluded.repo_owner,
|
||||
repo_name = excluded.repo_name,
|
||||
repo_platform = excluded.repo_platform,
|
||||
repo_origin_url = excluded.repo_origin_url,
|
||||
default_branch = excluded.default_branch,
|
||||
display_name = excluded.display_name,
|
||||
session_prefix = excluded.session_prefix,
|
||||
source = excluded.source,
|
||||
registered_at = excluded.registered_at,
|
||||
archived_at = excluded.archived_at;
|
||||
|
||||
-- name: GetProject :one
|
||||
SELECT id, path, repo_owner, repo_name, repo_platform, repo_origin_url, default_branch, display_name, session_prefix, source, registered_at, archived_at
|
||||
FROM projects
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: ListProjects :many
|
||||
SELECT id, path, repo_owner, repo_name, repo_platform, repo_origin_url, default_branch, display_name, session_prefix, source, registered_at, archived_at
|
||||
FROM projects
|
||||
WHERE archived_at IS NULL
|
||||
ORDER BY id;
|
||||
|
||||
-- name: ArchiveProject :exec
|
||||
UPDATE projects SET archived_at = ? WHERE id = ?;
|
||||
|
||||
-- name: DeleteProject :exec
|
||||
DELETE FROM projects WHERE id = ?;
|
||||
Loading…
Reference in New Issue