1use sbol_db_core::{
15 kmer::{
16 canonical, canonical_kmers, encode_kmer, reverse_complement_string, KmerStrand, KMER_K,
17 },
18 DomainError,
19};
20use sqlx::{PgConnection, Row};
21use uuid::Uuid;
22
23use crate::repo::db_err;
24use crate::PgPool;
25
26use sbol_db_storage::{BatchSequenceMatch, SequenceMatch, SequenceSearchOptions};
27
28#[derive(Clone)]
29pub struct SequenceSearchRepository {
30 pool: PgPool,
31}
32
33impl SequenceSearchRepository {
34 pub fn new(pool: PgPool) -> Self {
35 Self { pool }
36 }
37
38 pub async fn reindex(
43 &self,
44 conn: &mut PgConnection,
45 object_id: Uuid,
46 elements: Option<&str>,
47 alphabet: Option<&str>,
48 ) -> Result<usize, DomainError> {
49 reindex_kmers(conn, object_id, elements, alphabet).await
50 }
51
52 pub async fn search(
56 &self,
57 pattern: &str,
58 options: SequenceSearchOptions,
59 ) -> Result<Vec<SequenceMatch>, DomainError> {
60 let max = options.max_hits.unwrap_or(1024) as usize;
61 if max == 0 {
62 return Ok(Vec::new());
63 }
64 let q_upper: String = pattern
65 .chars()
66 .filter(|c| !c.is_whitespace())
67 .map(|c| c.to_ascii_uppercase())
68 .collect();
69 if q_upper.is_empty() {
70 return Ok(Vec::new());
71 }
72 let q_rc: String = reverse_complement_string(&q_upper).to_ascii_uppercase();
73 let include_rc = !matches!(options.forward_only, Some(true));
74 let candidates = if q_upper.len() < KMER_K {
75 self.candidates_short(&q_upper, &q_rc, include_rc).await?
76 } else {
77 self.candidates_seeded(&q_upper, &q_rc, include_rc).await?
78 };
79
80 let mut matches = Vec::new();
81 'outer: for (iri, elements) in candidates {
82 let up = elements.to_ascii_uppercase();
83 for (start, _) in up.match_indices(q_upper.as_str()) {
84 matches.push(SequenceMatch {
85 sequence_iri: iri.clone(),
86 start: start as i32,
87 length: q_upper.len() as i32,
88 strand: KmerStrand::Forward.as_char(),
89 });
90 if matches.len() >= max {
91 break 'outer;
92 }
93 }
94 if include_rc && q_upper != q_rc {
95 for (start, _) in up.match_indices(q_rc.as_str()) {
96 matches.push(SequenceMatch {
97 sequence_iri: iri.clone(),
98 start: start as i32,
99 length: q_rc.len() as i32,
100 strand: KmerStrand::Reverse.as_char(),
101 });
102 if matches.len() >= max {
103 break 'outer;
104 }
105 }
106 }
107 }
108 Ok(matches)
109 }
110
111 pub async fn search_many(
115 &self,
116 patterns: &[String],
117 options: SequenceSearchOptions,
118 ) -> Result<Vec<BatchSequenceMatch>, DomainError> {
119 let mut out = Vec::with_capacity(patterns.len());
120 for pattern in patterns {
121 let matches = self.search(pattern, options.clone()).await?;
122 out.push(BatchSequenceMatch {
123 pattern: pattern.clone(),
124 matches,
125 });
126 }
127 Ok(out)
128 }
129
130 async fn candidates_seeded(
131 &self,
132 q_upper: &str,
133 q_rc: &str,
134 include_rc: bool,
135 ) -> Result<Vec<(String, String)>, DomainError> {
136 let mut seeds: Vec<i32> = Vec::new();
137 if let Some(packed) = encode_kmer(&q_upper[..KMER_K]) {
138 seeds.push(canonical(packed).0 as i32);
139 }
140 if include_rc && q_upper != q_rc {
141 if let Some(packed) = encode_kmer(&q_rc[..KMER_K]) {
142 let c = canonical(packed).0 as i32;
143 if !seeds.contains(&c) {
144 seeds.push(c);
145 }
146 }
147 }
148 if seeds.is_empty() {
149 return Ok(Vec::new());
150 }
151 let rows = sqlx::query(
152 r#"
153 SELECT s.iri::text AS iri, s.elements AS elements
154 FROM sbol_sequences s
155 WHERE s.elements IS NOT NULL
156 AND s.alphabet IN ('DNA', 'RNA')
157 AND s.object_id IN (
158 SELECT DISTINCT sequence_object_id FROM sbol_sequence_kmers
159 WHERE kmer = ANY($1::int[])
160 )
161 "#,
162 )
163 .bind(&seeds)
164 .fetch_all(&self.pool)
165 .await
166 .map_err(db_err)?;
167
168 let mut out = Vec::with_capacity(rows.len());
169 for row in rows {
170 let iri: String = row.try_get("iri").map_err(db_err)?;
171 let elements: String = row.try_get("elements").map_err(db_err)?;
172 out.push((iri, elements));
173 }
174 Ok(out)
175 }
176
177 async fn candidates_short(
178 &self,
179 q_upper: &str,
180 q_rc: &str,
181 include_rc: bool,
182 ) -> Result<Vec<(String, String)>, DomainError> {
183 let sql = if include_rc && q_upper != q_rc {
184 r#"
185 SELECT iri::text AS iri, elements
186 FROM sbol_sequences
187 WHERE elements IS NOT NULL
188 AND alphabet IN ('DNA', 'RNA')
189 AND (UPPER(elements) LIKE '%' || $1 || '%'
190 OR UPPER(elements) LIKE '%' || $2 || '%')
191 "#
192 } else {
193 r#"
194 SELECT iri::text AS iri, elements
195 FROM sbol_sequences
196 WHERE elements IS NOT NULL
197 AND alphabet IN ('DNA', 'RNA')
198 AND UPPER(elements) LIKE '%' || $1 || '%'
199 "#
200 };
201 let q = sqlx::query(sql).bind(q_upper);
202 let q = if include_rc && q_upper != q_rc {
203 q.bind(q_rc)
204 } else {
205 q
206 };
207 let rows = q.fetch_all(&self.pool).await.map_err(db_err)?;
208 let mut out = Vec::with_capacity(rows.len());
209 for row in rows {
210 let iri: String = row.try_get("iri").map_err(db_err)?;
211 let elements: String = row.try_get("elements").map_err(db_err)?;
212 out.push((iri, elements));
213 }
214 Ok(out)
215 }
216}
217
218pub(crate) async fn reindex_kmers(
223 conn: &mut PgConnection,
224 object_id: Uuid,
225 elements: Option<&str>,
226 alphabet: Option<&str>,
227) -> Result<usize, DomainError> {
228 sqlx::query("DELETE FROM sbol_sequence_kmers WHERE sequence_object_id = $1")
229 .bind(object_id)
230 .execute(&mut *conn)
231 .await
232 .map_err(db_err)?;
233 let Some(elements) = elements else {
234 return Ok(0);
235 };
236 if !matches!(alphabet, Some("DNA") | Some("RNA")) {
237 return Ok(0);
238 }
239 let normalised: String = elements.chars().filter(|c| !c.is_whitespace()).collect();
240 if normalised.len() < KMER_K {
241 return Ok(0);
242 }
243
244 let hits: Vec<_> = canonical_kmers(&normalised).collect();
245 if hits.is_empty() {
246 return Ok(0);
247 }
248
249 let mut kmers: Vec<i32> = Vec::with_capacity(hits.len());
251 let mut positions: Vec<i32> = Vec::with_capacity(hits.len());
252 let mut strands: Vec<String> = Vec::with_capacity(hits.len());
253 for h in &hits {
254 kmers.push(h.canonical as i32);
255 positions.push(h.position as i32);
256 strands.push(h.strand.as_char().to_string());
257 }
258
259 sqlx::query(
260 r#"
261 INSERT INTO sbol_sequence_kmers (sequence_object_id, kmer, position, strand)
262 SELECT $1, k, p, s
263 FROM UNNEST($2::int[], $3::int[], $4::text[]) AS u(k, p, s)
264 "#,
265 )
266 .bind(object_id)
267 .bind(&kmers)
268 .bind(&positions)
269 .bind(&strands)
270 .execute(&mut *conn)
271 .await
272 .map_err(db_err)?;
273
274 Ok(hits.len())
275}