Skip to main content

zeph_db/
instance_id.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Stable per-database instance identity (#5742).
5//!
6//! Autoincrementing primary keys (e.g. `conversation_id`) are unique only within one
7//! physical database. When multiple independent databases share a single external
8//! store (e.g. Qdrant), those keys collide. [`get_or_create_instance_id`] gives each
9//! database a random UUID, generated once and persisted, that callers combine with
10//! such keys to disambiguate them across databases.
11
12use crate::{DbPool, error::DbError, sql};
13
14/// Fetch this database's stable instance UUID, generating and persisting one on first call.
15///
16/// Race-safe: the insert is `ON CONFLICT DO NOTHING`, so the first writer's row wins and
17/// every caller — including concurrent ones racing on a fresh database — reads back the
18/// same persisted value afterward.
19///
20/// # Errors
21///
22/// Returns [`DbError`] if the insert or the follow-up select fails.
23///
24/// # Examples
25///
26/// ```rust,no_run
27/// # async fn example(pool: &zeph_db::DbPool) -> Result<(), zeph_db::DbError> {
28/// let instance_id = zeph_db::get_or_create_instance_id(pool).await?;
29/// # let _ = instance_id;
30/// # Ok(())
31/// # }
32/// ```
33pub async fn get_or_create_instance_id(pool: &DbPool) -> Result<String, DbError> {
34    let candidate = uuid::Uuid::new_v4().to_string();
35    sqlx::query(sql!(
36        "INSERT INTO db_instance (id, instance_id) VALUES (1, ?) ON CONFLICT (id) DO NOTHING"
37    ))
38    .bind(&candidate)
39    .execute(pool)
40    .await?;
41    sqlx::query_scalar(sql!("SELECT instance_id FROM db_instance WHERE id = 1"))
42        .fetch_one(pool)
43        .await
44        .map_err(Into::into)
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50    use crate::{DbConfig, run_migrations};
51
52    async fn setup_pool() -> DbPool {
53        let pool = DbConfig {
54            url: ":memory:".to_string(),
55            max_connections: 1,
56            pool_size: 1,
57        }
58        .connect()
59        .await
60        .expect("connect");
61        run_migrations(&pool).await.expect("migrate");
62        pool
63    }
64
65    #[tokio::test]
66    async fn generates_and_persists_id() {
67        let pool = setup_pool().await;
68        let id1 = get_or_create_instance_id(&pool).await.expect("first call");
69        assert!(!id1.is_empty());
70        let id2 = get_or_create_instance_id(&pool).await.expect("second call");
71        assert_eq!(id1, id2, "instance id must be stable across calls");
72    }
73
74    #[tokio::test]
75    async fn distinct_pools_get_distinct_ids() {
76        let pool_a = setup_pool().await;
77        let pool_b = setup_pool().await;
78        let id_a = get_or_create_instance_id(&pool_a).await.expect("pool a");
79        let id_b = get_or_create_instance_id(&pool_b).await.expect("pool b");
80        assert_ne!(id_a, id_b, "distinct databases must get distinct ids");
81    }
82}