1use std::collections::{BTreeMap, BTreeSet};
8
9use sbol_db_core::{
10 Direction, DomainError, EdgeInfo, EdgeObject, NeighborhoodQuery, NeighborhoodResult, NodeInfo,
11};
12use sqlx::Row;
13
14use crate::repo::db_err;
15use crate::PgPool;
16
17#[derive(Clone)]
18pub struct NeighborhoodRepository {
19 pool: PgPool,
20}
21
22impl NeighborhoodRepository {
23 pub fn new(pool: PgPool) -> Self {
24 Self { pool }
25 }
26
27 pub async fn walk(&self, query: &NeighborhoodQuery) -> Result<NeighborhoodResult, DomainError> {
28 let depth_cap = i32::try_from(query.depth).unwrap_or(i32::MAX);
29 let max_nodes_cap = query
30 .max_nodes
31 .map(|m| i32::try_from(m).unwrap_or(i32::MAX))
32 .unwrap_or(i32::MAX);
33 let predicate_filter: Option<Vec<String>> = if query.predicate_allowlist.is_empty() {
34 None
35 } else {
36 Some(
37 query
38 .predicate_allowlist
39 .iter()
40 .map(|i| i.as_str().to_owned())
41 .collect(),
42 )
43 };
44
45 let dir_tag = match query.direction {
49 Direction::Forward => "forward",
50 Direction::Backward => "backward",
51 Direction::Both => "both",
52 };
53
54 let rows = sqlx::query(SQL_WALK)
55 .bind(query.root_iri.as_str())
56 .bind(depth_cap)
57 .bind(predicate_filter.as_ref())
58 .bind(dir_tag)
59 .bind(max_nodes_cap)
60 .fetch_all(&self.pool)
61 .await
62 .map_err(db_err)?;
63
64 let mut nodes_by_id: BTreeMap<String, NodeInfo> = BTreeMap::new();
65 let mut edges: Vec<EdgeInfo> = Vec::new();
66 let mut seen_edges: BTreeSet<(String, String, String)> = BTreeSet::new();
67 let mut max_depth = 0u32;
68
69 for row in &rows {
73 let depth: i32 = row.try_get("depth").map_err(db_err)?;
74 let depth_u: u32 = depth.max(0) as u32;
75 max_depth = max_depth.max(depth_u);
76
77 let node_id: String = row.try_get("node_id").map_err(db_err)?;
78 let is_blank: bool = row.try_get("node_is_blank").map_err(db_err)?;
79 nodes_by_id
80 .entry(node_id.clone())
81 .or_insert_with(|| NodeInfo {
82 id: node_id.clone(),
83 depth: depth_u,
84 is_blank,
85 sbol_class: None,
86 display_id: None,
87 name: None,
88 });
89
90 let edge_subject: Option<String> = row.try_get("edge_subject").map_err(db_err)?;
92 if let Some(subject) = edge_subject {
93 let subject_blank: bool = row.try_get("edge_subject_is_blank").map_err(db_err)?;
94 let predicate: String = row.try_get("edge_predicate").map_err(db_err)?;
95 let object_iri: Option<String> = row.try_get("edge_object_iri").map_err(db_err)?;
96 let object_blank: Option<String> =
97 row.try_get("edge_object_blank").map_err(db_err)?;
98 let object_literal: Option<String> =
99 row.try_get("edge_object_literal").map_err(db_err)?;
100 let object_datatype: Option<String> =
101 row.try_get("edge_object_datatype").map_err(db_err)?;
102 let object_language: Option<String> =
103 row.try_get("edge_object_language").map_err(db_err)?;
104 let object = if let Some(iri) = object_iri.clone() {
105 EdgeObject::Iri { value: iri }
106 } else if let Some(blank) = object_blank.clone() {
107 EdgeObject::BlankNode { value: blank }
108 } else if let Some(lit) = object_literal.clone() {
109 EdgeObject::Literal {
110 value: lit,
111 datatype: object_datatype.unwrap_or_else(|| {
112 "http://www.w3.org/2001/XMLSchema#string".to_owned()
113 }),
114 language: object_language,
115 }
116 } else {
117 continue;
118 };
119 let object_key = match &object {
120 EdgeObject::Iri { value } | EdgeObject::BlankNode { value } => value.clone(),
121 EdgeObject::Literal { value, .. } => format!("\u{1}lit\u{1}{value}"),
122 };
123 if seen_edges.insert((subject.clone(), predicate.clone(), object_key)) {
124 edges.push(EdgeInfo {
125 subject,
126 subject_is_blank: subject_blank,
127 predicate,
128 object,
129 depth: depth_u,
130 });
131 }
132 }
133 }
134
135 if query.include_literals {
139 let ids: Vec<&str> = nodes_by_id.keys().map(String::as_str).collect();
140 let literal_rows = sqlx::query(SQL_LITERAL_EDGES)
141 .bind(&ids)
142 .fetch_all(&self.pool)
143 .await
144 .map_err(db_err)?;
145 for row in literal_rows {
146 let subject: String = row.try_get("subject").map_err(db_err)?;
147 let predicate: String = row.try_get("predicate").map_err(db_err)?;
148 let value: String = row.try_get("value").map_err(db_err)?;
149 let datatype: Option<String> = row.try_get("datatype").map_err(db_err)?;
150 let language: Option<String> = row.try_get("language").map_err(db_err)?;
151 let depth = nodes_by_id.get(&subject).map(|n| n.depth).unwrap_or(0);
152 let key = (
153 subject.clone(),
154 predicate.clone(),
155 format!("\u{1}lit\u{1}{value}"),
156 );
157 if seen_edges.insert(key) {
158 edges.push(EdgeInfo {
159 subject,
160 subject_is_blank: false,
161 predicate,
162 object: EdgeObject::Literal {
163 value,
164 datatype: datatype.unwrap_or_else(|| {
165 "http://www.w3.org/2001/XMLSchema#string".to_owned()
166 }),
167 language,
168 },
169 depth,
170 });
171 }
172 }
173 }
174
175 let iri_keys: Vec<&str> = nodes_by_id
177 .values()
178 .filter(|n| !n.is_blank)
179 .map(|n| n.id.as_str())
180 .collect();
181 if !iri_keys.is_empty() {
182 let meta_rows = sqlx::query(SQL_NODE_METADATA)
183 .bind(&iri_keys)
184 .fetch_all(&self.pool)
185 .await
186 .map_err(db_err)?;
187 for row in meta_rows {
188 let iri: String = row.try_get("iri").map_err(db_err)?;
189 if let Some(node) = nodes_by_id.get_mut(&iri) {
190 node.sbol_class = row.try_get("sbol_class").map_err(db_err)?;
191 node.display_id = row.try_get("display_id").map_err(db_err)?;
192 node.name = row.try_get("name").map_err(db_err)?;
193 }
194 }
195 }
196
197 let nodes: Vec<NodeInfo> = nodes_by_id.into_values().collect();
198 let truncated = nodes.len() as i32 >= max_nodes_cap;
199
200 Ok(NeighborhoodResult {
201 root_iri: query.root_iri.clone(),
202 nodes,
203 edges,
204 max_depth_reached: max_depth,
205 truncated,
206 })
207 }
208}
209
210const SQL_WALK: &str = r#"
219WITH RECURSIVE
220edges AS (
221 SELECT
222 COALESCE(subject_iri::text, subject_blank) AS from_id,
223 (subject_iri IS NULL AND subject_blank IS NOT NULL) AS from_is_blank,
224 predicate_iri::text AS predicate,
225 COALESCE(object_iri::text, object_blank) AS to_id,
226 (object_iri IS NULL AND object_blank IS NOT NULL) AS to_is_blank,
227 subject_iri::text AS edge_subject_iri,
228 subject_blank AS edge_subject_blank,
229 object_iri::text AS edge_object_iri,
230 object_blank AS edge_object_blank
231 FROM sbol_triples
232 WHERE $4 IN ('forward', 'both')
233 AND (object_iri IS NOT NULL OR object_blank IS NOT NULL)
234
235 UNION ALL
236
237 SELECT
238 COALESCE(object_iri::text, object_blank),
239 (object_iri IS NULL AND object_blank IS NOT NULL),
240 predicate_iri::text,
241 COALESCE(subject_iri::text, subject_blank),
242 (subject_iri IS NULL AND subject_blank IS NOT NULL),
243 subject_iri::text,
244 subject_blank,
245 object_iri::text,
246 object_blank
247 FROM sbol_triples
248 WHERE $4 IN ('backward', 'both')
249 AND (object_iri IS NOT NULL OR object_blank IS NOT NULL)
250),
251walk AS (
252 SELECT
253 $1::text AS node_id,
254 false AS node_is_blank,
255 0 AS depth,
256 ARRAY[$1::text] AS path,
257 NULL::text AS edge_subject,
258 false AS edge_subject_is_blank,
259 NULL::text AS edge_predicate,
260 NULL::text AS edge_object_iri,
261 NULL::text AS edge_object_blank,
262 NULL::text AS edge_object_literal,
263 NULL::text AS edge_object_datatype,
264 NULL::text AS edge_object_language
265
266 UNION ALL
267
268 SELECT
269 e.to_id,
270 e.to_is_blank,
271 w.depth + 1,
272 w.path || e.to_id,
273 COALESCE(e.edge_subject_iri, e.edge_subject_blank),
274 (e.edge_subject_iri IS NULL AND e.edge_subject_blank IS NOT NULL),
275 e.predicate,
276 e.edge_object_iri,
277 e.edge_object_blank,
278 NULL::text,
279 NULL::text,
280 NULL::text
281 FROM walk w
282 JOIN edges e
283 ON e.from_id = w.node_id
284 AND e.from_is_blank = w.node_is_blank
285 WHERE w.depth < $2
286 AND ($3::text[] IS NULL OR e.predicate = ANY($3))
287 AND NOT e.to_id = ANY(w.path)
288)
289SELECT *
290FROM walk
291LIMIT $5
292"#;
293
294const SQL_LITERAL_EDGES: &str = r#"
295SELECT
296 COALESCE(subject_iri::text, subject_blank) AS subject,
297 predicate_iri::text AS predicate,
298 object_literal AS value,
299 datatype_iri::text AS datatype,
300 language AS language
301FROM sbol_triples
302WHERE object_literal IS NOT NULL
303 AND COALESCE(subject_iri::text, subject_blank) = ANY($1::text[])
304"#;
305
306const SQL_NODE_METADATA: &str = r#"
307SELECT iri::text AS iri, sbol_class, display_id, name
308FROM sbol_objects
309WHERE iri::text = ANY($1::text[])
310 AND is_deleted = false
311"#;