Skip to main content

objectiveai_cli/db/
laboratory_attachments.rs

1//! Laboratory IDs attached to an agent target, backed by the postgres
2//! `laboratory_attachments` table.
3//!
4//! A row attaches one `laboratory_id` to EITHER an
5//! `agent_instance_hierarchy` (AIH) OR a `tag` — never both (a CHECK
6//! constraint enforces exclusivity). A given laboratory is attached at
7//! most once per target (a partial unique index per target column).
8//! `laboratory_id` is an opaque external identifier.
9
10use sqlx::Row as _;
11
12use super::{Error, Pool};
13
14/// Which target column a row is keyed on.
15pub enum Target {
16    /// Keyed on `tag`.
17    Tag(String),
18    /// Keyed on `agent_instance_hierarchy`.
19    Aih(String),
20}
21
22impl Target {
23    /// `(tag, agent_instance_hierarchy)` bind values — exactly one is
24    /// `Some`, the other `None` (bound as SQL NULL).
25    fn columns(&self) -> (Option<&str>, Option<&str>) {
26        match self {
27            Target::Tag(tag) => (Some(tag.as_str()), None),
28            Target::Aih(aih) => (None, Some(aih.as_str())),
29        }
30    }
31}
32
33fn now() -> i64 {
34    std::time::SystemTime::now()
35        .duration_since(std::time::UNIX_EPOCH)
36        .map(|d| d.as_secs() as i64)
37        .unwrap_or(0)
38}
39
40/// Attach `laboratory_id` to `target`. Returns `true` if a row was
41/// inserted, `false` if it was already attached. Caller holds the
42/// agent lock, so the select-then-insert is race-free.
43pub async fn attach(
44    pool: &Pool,
45    target: &Target,
46    laboratory_id: &str,
47) -> Result<bool, Error> {
48    let (tag, aih) = target.columns();
49    let existing = sqlx::query(
50        "SELECT 1 FROM objectiveai.laboratory_attachments \
51         WHERE tag IS NOT DISTINCT FROM $1 \
52           AND agent_instance_hierarchy IS NOT DISTINCT FROM $2 \
53           AND laboratory_id = $3",
54    )
55    .bind(tag)
56    .bind(aih)
57    .bind(laboratory_id)
58    .fetch_optional(&**pool)
59    .await?;
60    if existing.is_some() {
61        return Ok(false);
62    }
63    sqlx::query(
64        "INSERT INTO objectiveai.laboratory_attachments \
65         (tag, agent_instance_hierarchy, laboratory_id, created_at) \
66         VALUES ($1, $2, $3, $4)",
67    )
68    .bind(tag)
69    .bind(aih)
70    .bind(laboratory_id)
71    .bind(now())
72    .execute(&**pool)
73    .await?;
74    Ok(true)
75}
76
77/// Detach `laboratory_id` from `target`. Returns `true` if a row was
78/// deleted, `false` if there was nothing to delete. Caller holds the
79/// agent lock.
80pub async fn detach(
81    pool: &Pool,
82    target: &Target,
83    laboratory_id: &str,
84) -> Result<bool, Error> {
85    let (tag, aih) = target.columns();
86    let result = sqlx::query(
87        "DELETE FROM objectiveai.laboratory_attachments \
88         WHERE tag IS NOT DISTINCT FROM $1 \
89           AND agent_instance_hierarchy IS NOT DISTINCT FROM $2 \
90           AND laboratory_id = $3",
91    )
92    .bind(tag)
93    .bind(aih)
94    .bind(laboratory_id)
95    .execute(&**pool)
96    .await?;
97    Ok(result.rows_affected() > 0)
98}
99
100/// All laboratory ids attached to `target`, oldest-attached first.
101pub async fn list(pool: &Pool, target: &Target) -> Result<Vec<String>, Error> {
102    let (tag, aih) = target.columns();
103    let rows = sqlx::query(
104        "SELECT laboratory_id FROM objectiveai.laboratory_attachments \
105         WHERE tag IS NOT DISTINCT FROM $1 \
106           AND agent_instance_hierarchy IS NOT DISTINCT FROM $2 \
107         ORDER BY created_at",
108    )
109    .bind(tag)
110    .bind(aih)
111    .fetch_all(&**pool)
112    .await?;
113    let mut out = Vec::with_capacity(rows.len());
114    for row in rows {
115        out.push(row.try_get::<String, _>(0)?);
116    }
117    Ok(out)
118}