Skip to main content

dk_engine/graph/
depgraph.rs

1use dk_core::{Dependency, RepoId, SymbolId};
2use sqlx::postgres::PgPool;
3use uuid::Uuid;
4
5/// Intermediate row type for mapping between database rows and `Dependency`.
6#[derive(sqlx::FromRow)]
7struct DependencyRow {
8    id: Uuid,
9    repo_id: Uuid,
10    package: String,
11    version_req: String,
12}
13
14impl DependencyRow {
15    fn into_dependency(self) -> Dependency {
16        Dependency {
17            id: self.id,
18            repo_id: self.repo_id,
19            package: self.package,
20            version_req: self.version_req,
21        }
22    }
23}
24
25/// PostgreSQL-backed store for external dependency tracking.
26#[derive(Clone)]
27pub struct DependencyStore {
28    pool: PgPool,
29}
30
31impl DependencyStore {
32    /// Create a new `DependencyStore` backed by the given connection pool.
33    pub fn new(pool: PgPool) -> Self {
34        Self { pool }
35    }
36
37    /// Insert or update a dependency.
38    ///
39    /// Uses `ON CONFLICT (repo_id, package) DO UPDATE` so that
40    /// re-parsing the same manifest file updates the version requirement.
41    pub async fn upsert_dependency(&self, dep: &Dependency) -> dk_core::Result<()> {
42        sqlx::query(
43            r#"
44            INSERT INTO dependencies (id, repo_id, package, version_req)
45            VALUES ($1, $2, $3, $4)
46            ON CONFLICT (repo_id, package) DO UPDATE SET
47                id = EXCLUDED.id,
48                version_req = EXCLUDED.version_req
49            "#,
50        )
51        .bind(dep.id)
52        .bind(dep.repo_id)
53        .bind(&dep.package)
54        .bind(&dep.version_req)
55        .execute(&self.pool)
56        .await?;
57
58        Ok(())
59    }
60
61    /// Find all dependencies for a given repository.
62    pub async fn find_by_repo(&self, repo_id: RepoId) -> dk_core::Result<Vec<Dependency>> {
63        let rows = sqlx::query_as::<_, DependencyRow>(
64            r#"
65            SELECT id, repo_id, package, version_req
66            FROM dependencies
67            WHERE repo_id = $1
68            ORDER BY package
69            "#,
70        )
71        .bind(repo_id)
72        .fetch_all(&self.pool)
73        .await?;
74
75        Ok(rows.into_iter().map(DependencyRow::into_dependency).collect())
76    }
77
78    /// Link a symbol to a dependency (records that the symbol uses/imports
79    /// something from the given external package).
80    pub async fn link_symbol_to_dep(
81        &self,
82        symbol_id: SymbolId,
83        dep_id: Uuid,
84    ) -> dk_core::Result<()> {
85        sqlx::query(
86            r#"
87            INSERT INTO symbol_dependencies (symbol_id, dependency_id)
88            VALUES ($1, $2)
89            ON CONFLICT (symbol_id, dependency_id) DO NOTHING
90            "#,
91        )
92        .bind(symbol_id)
93        .bind(dep_id)
94        .execute(&self.pool)
95        .await?;
96
97        Ok(())
98    }
99
100    /// Find all symbol IDs that are linked to a specific dependency.
101    pub async fn find_symbols_for_dep(
102        &self,
103        dep_id: Uuid,
104    ) -> dk_core::Result<Vec<SymbolId>> {
105        let rows: Vec<(Uuid,)> = sqlx::query_as(
106            "SELECT symbol_id FROM symbol_dependencies WHERE dependency_id = $1",
107        )
108        .bind(dep_id)
109        .fetch_all(&self.pool)
110        .await?;
111
112        Ok(rows.into_iter().map(|(id,)| id).collect())
113    }
114
115    /// Delete all dependencies for a repository. Returns the number of rows deleted.
116    ///
117    /// Note: this cascades to `symbol_dependencies` via foreign key constraints.
118    pub async fn delete_by_repo(&self, repo_id: RepoId) -> dk_core::Result<u64> {
119        let result = sqlx::query("DELETE FROM dependencies WHERE repo_id = $1")
120            .bind(repo_id)
121            .execute(&self.pool)
122            .await?;
123
124        Ok(result.rows_affected())
125    }
126}