1use std::collections::HashMap;
8use std::sync::Arc;
9
10use crate::facts::FileFacts;
11use crate::model::{Edge, Symbol};
12use anyhow::{Context, Result};
13use lancedb::arrow::arrow_array::{
14 ArrayRef, BooleanArray, FixedSizeListArray, Float32Array, RecordBatch, StringArray,
15 UInt32Array, UInt64Array,
16};
17use lancedb::arrow::arrow_schema::{DataType, Field, Schema};
18use lancedb::table::AddDataMode;
19
20type TextRow = (GlobalSymbolId, String, Vec<String>);
22type SearchRow = (GlobalSymbolId, String, Vec<f32>);
24type VectorRow = (GlobalSymbolId, Vec<f32>);
26
27fn symbols_schema() -> Arc<Schema> {
28 Arc::new(Schema::new(vec![
29 Field::new("symbol_id", DataType::Utf8, false),
30 Field::new("scheme", DataType::Utf8, false),
31 Field::new("package_manager", DataType::Utf8, false),
32 Field::new("package", DataType::Utf8, false),
33 Field::new("version", DataType::Utf8, false),
34 Field::new("descriptor", DataType::Utf8, false),
35 Field::new("name", DataType::Utf8, false),
36 Field::new("kind", DataType::Utf8, false),
37 Field::new("language", DataType::Utf8, false),
38 Field::new("repo", DataType::Utf8, false),
39 Field::new("file_path", DataType::Utf8, false),
40 Field::new("line_start", DataType::UInt32, false),
41 Field::new("col_start", DataType::UInt32, false),
42 Field::new("line_end", DataType::UInt32, false),
43 Field::new("col_end", DataType::UInt32, false),
44 Field::new("signature", DataType::Utf8, true),
45 Field::new("docstring", DataType::Utf8, true),
46 Field::new("fp_sig", DataType::UInt64, true),
47 Field::new("fp_body", DataType::UInt64, true),
48 Field::new("fp_shape", DataType::UInt64, true),
49 ]))
50}
51
52fn edges_schema() -> Arc<Schema> {
53 Arc::new(Schema::new(vec![
54 Field::new("src_id", DataType::Utf8, false),
55 Field::new("dst_id", DataType::Utf8, false),
56 Field::new("kind", DataType::Utf8, false),
57 Field::new("confidence", DataType::Float32, false),
58 Field::new("cross_repo", DataType::Boolean, false),
59 ]))
60}
61
62fn symbols_batch(symbols: &[Symbol]) -> Result<RecordBatch> {
63 let arrays: Vec<ArrayRef> = vec![
64 Arc::new(StringArray::from_iter_values(
65 symbols.iter().map(|s| s.id.render()),
66 )),
67 Arc::new(StringArray::from_iter_values(
68 symbols.iter().map(|s| s.id.scheme.clone()),
69 )),
70 Arc::new(StringArray::from_iter_values(
71 symbols.iter().map(|s| s.id.package_manager.clone()),
72 )),
73 Arc::new(StringArray::from_iter_values(
74 symbols.iter().map(|s| s.id.package.clone()),
75 )),
76 Arc::new(StringArray::from_iter_values(
77 symbols.iter().map(|s| s.id.version.clone()),
78 )),
79 Arc::new(StringArray::from_iter_values(
80 symbols.iter().map(|s| s.id.descriptor.clone()),
81 )),
82 Arc::new(StringArray::from_iter_values(
83 symbols.iter().map(|s| s.name.clone()),
84 )),
85 Arc::new(StringArray::from_iter_values(
86 symbols.iter().map(|s| format!("{:?}", s.kind)),
87 )),
88 Arc::new(StringArray::from_iter_values(
89 symbols.iter().map(|s| format!("{:?}", s.language)),
90 )),
91 Arc::new(StringArray::from_iter_values(
92 symbols.iter().map(|s| s.repo.0.clone()),
93 )),
94 Arc::new(StringArray::from_iter_values(
95 symbols.iter().map(|s| s.file_path.clone()),
96 )),
97 Arc::new(UInt32Array::from_iter_values(
98 symbols.iter().map(|s| s.range.start.line),
99 )),
100 Arc::new(UInt32Array::from_iter_values(
101 symbols.iter().map(|s| s.range.start.column),
102 )),
103 Arc::new(UInt32Array::from_iter_values(
104 symbols.iter().map(|s| s.range.end.line),
105 )),
106 Arc::new(UInt32Array::from_iter_values(
107 symbols.iter().map(|s| s.range.end.column),
108 )),
109 Arc::new(StringArray::from_iter(
110 symbols.iter().map(|s| s.signature.clone()),
111 )),
112 Arc::new(StringArray::from_iter(
113 symbols.iter().map(|s| s.docstring.clone()),
114 )),
115 Arc::new(UInt64Array::from_iter(
116 symbols.iter().map(|s| s.fingerprint.map(|f| f.sig)),
117 )),
118 Arc::new(UInt64Array::from_iter(
119 symbols.iter().map(|s| s.fingerprint.map(|f| f.body)),
120 )),
121 Arc::new(UInt64Array::from_iter(
122 symbols.iter().map(|s| s.fingerprint.map(|f| f.shape)),
123 )),
124 ];
125 RecordBatch::try_new(symbols_schema(), arrays).context("build symbols batch")
126}
127
128fn edges_batch(edges: &[Edge]) -> Result<RecordBatch> {
129 let arrays: Vec<ArrayRef> = vec![
130 Arc::new(StringArray::from_iter_values(
131 edges.iter().map(|e| e.src.render()),
132 )),
133 Arc::new(StringArray::from_iter_values(
134 edges.iter().map(|e| e.dst.render()),
135 )),
136 Arc::new(StringArray::from_iter_values(
137 edges.iter().map(|e| format!("{:?}", e.kind)),
138 )),
139 Arc::new(Float32Array::from_iter_values(
140 edges.iter().map(|e| e.confidence),
141 )),
142 Arc::new(BooleanArray::from(
143 edges.iter().map(|e| e.cross_repo).collect::<Vec<_>>(),
144 )),
145 ];
146 RecordBatch::try_new(edges_schema(), arrays).context("build edges batch")
147}
148
149pub(crate) async fn write_versioned(
154 db: &lancedb::Connection,
155 name: &str,
156 batch: RecordBatch,
157) -> Result<u64> {
158 let tbl = match db.open_table(name).execute().await {
160 Ok(t) => {
161 t.add(batch)
162 .mode(AddDataMode::Overwrite)
163 .execute()
164 .await
165 .with_context(|| format!("overwrite table `{name}`"))?;
166 t
167 }
168 Err(_) => db
169 .create_table(name, batch)
170 .execute()
171 .await
172 .with_context(|| format!("create table `{name}`"))?,
173 };
174 tbl.version()
175 .await
176 .with_context(|| format!("read version of `{name}`"))
177}
178
179pub async fn write_graph(uri: &str, symbols: &[Symbol], edges: &[Edge]) -> Result<(u64, u64)> {
182 let db = crate::storage::connect(uri)
183 .execute()
184 .await
185 .with_context(|| format!("connect lancedb at {uri}"))?;
186 let sv = write_versioned(&db, "symbols", symbols_batch(symbols)?).await?;
187 let ev = write_versioned(&db, "edges", edges_batch(edges)?).await?;
188 Ok((sv, ev))
189}
190
191pub async fn latest_versions(uri: &str) -> Result<(u64, u64)> {
193 let db = crate::storage::connect(uri).execute().await?;
194 let sv = db.open_table("symbols").execute().await?.version().await?;
195 let ev = db.open_table("edges").execute().await?.version().await?;
196 Ok((sv, ev))
197}
198
199use crate::model::{
202 EdgeKind, Fingerprint, GlobalSymbolId, Language, Position, Range, RepoId, SymbolKind,
203};
204use futures::TryStreamExt;
205use lancedb::arrow::arrow_array::Array;
206use lancedb::query::ExecutableQuery;
207
208fn kind_from_str(s: &str) -> SymbolKind {
209 match s {
210 "File" => SymbolKind::File,
211 "Module" => SymbolKind::Module,
212 "Namespace" => SymbolKind::Namespace,
213 "Class" => SymbolKind::Class,
214 "Interface" => SymbolKind::Interface,
215 "Trait" => SymbolKind::Trait,
216 "Enum" => SymbolKind::Enum,
217 "Struct" => SymbolKind::Struct,
218 "Function" => SymbolKind::Function,
219 "Method" => SymbolKind::Method,
220 "Field" => SymbolKind::Field,
221 "Variable" => SymbolKind::Variable,
222 "Constant" => SymbolKind::Constant,
223 "Import" => SymbolKind::Import,
224 "TypeAlias" => SymbolKind::TypeAlias,
225 "Process" => SymbolKind::Process,
226 "Contract" => SymbolKind::Contract,
227 _ => SymbolKind::Variable,
228 }
229}
230
231fn lang_from_str(s: &str) -> Language {
232 match s {
233 "Python" => Language::Python,
234 "TypeScript" => Language::TypeScript,
235 "JavaScript" => Language::JavaScript,
236 "Go" => Language::Go,
237 "Rust" => Language::Rust,
238 "Java" => Language::Java,
239 "Kotlin" => Language::Kotlin,
240 "Ruby" => Language::Ruby,
241 "CSharp" => Language::CSharp,
242 "Cpp" => Language::Cpp,
243 "C" => Language::C,
244 "Php" => Language::Php,
245 "Scala" => Language::Scala,
246 "Swift" => Language::Swift,
247 other => Language::Other(
249 other
250 .strip_prefix("Other(\"")
251 .and_then(|s| s.strip_suffix("\")"))
252 .unwrap_or(other)
253 .to_string(),
254 ),
255 }
256}
257
258fn id_from_rendered(s: &str) -> GlobalSymbolId {
260 let mut it = s.splitn(5, ' ');
261 let mut next = || it.next().unwrap_or(".").to_string();
262 GlobalSymbolId {
263 scheme: next(),
264 package_manager: next(),
265 package: next(),
266 version: next(),
267 descriptor: next(),
268 }
269}
270
271pub(crate) fn str_col<'a>(
272 batch: &'a lancedb::arrow::arrow_array::RecordBatch,
273 name: &str,
274) -> Result<&'a StringArray> {
275 batch
276 .column_by_name(name)
277 .and_then(|c| c.as_any().downcast_ref::<StringArray>())
278 .with_context(|| format!("column `{name}` missing or not Utf8"))
279}
280
281fn u32_col<'a>(
282 batch: &'a lancedb::arrow::arrow_array::RecordBatch,
283 name: &str,
284) -> Result<&'a UInt32Array> {
285 batch
286 .column_by_name(name)
287 .and_then(|c| c.as_any().downcast_ref::<UInt32Array>())
288 .with_context(|| format!("column `{name}` missing or not UInt32"))
289}
290
291pub async fn read_graph(uri: &str) -> Result<(Vec<Symbol>, Vec<Edge>)> {
294 read_graph_at(uri, None).await
295}
296
297pub async fn read_graph_at(uri: &str, at: Option<(u64, u64)>) -> Result<(Vec<Symbol>, Vec<Edge>)> {
299 let db = crate::storage::connect(uri).execute().await?;
300 read_graph_in(&db, at).await
301}
302
303async fn read_graph_in(
304 db: &lancedb::Connection,
305 at: Option<(u64, u64)>,
306) -> Result<(Vec<Symbol>, Vec<Edge>)> {
307 let open = |name: &'static str, v: Option<u64>| async move {
308 let t = db.open_table(name).execute().await?;
309 if let Some(v) = v {
310 t.checkout(v).await?;
311 }
312 anyhow::Ok(t)
313 };
314
315 let mut symbols = Vec::new();
317 let mut st = open("symbols", at.map(|a| a.0))
318 .await?
319 .query()
320 .execute()
321 .await?;
322 while let Some(b) = st.try_next().await? {
323 let (scheme, pm, pkg, ver, desc) = (
324 str_col(&b, "scheme")?,
325 str_col(&b, "package_manager")?,
326 str_col(&b, "package")?,
327 str_col(&b, "version")?,
328 str_col(&b, "descriptor")?,
329 );
330 let (name, kind, lang, repo, file) = (
331 str_col(&b, "name")?,
332 str_col(&b, "kind")?,
333 str_col(&b, "language")?,
334 str_col(&b, "repo")?,
335 str_col(&b, "file_path")?,
336 );
337 let (ls, cs, le, ce) = (
338 u32_col(&b, "line_start")?,
339 u32_col(&b, "col_start")?,
340 u32_col(&b, "line_end")?,
341 u32_col(&b, "col_end")?,
342 );
343 let sig = str_col(&b, "signature")?;
344 let doc = str_col(&b, "docstring")?;
345 let fp = |name: &str| {
347 b.column_by_name(name)
348 .and_then(|c| c.as_any().downcast_ref::<UInt64Array>())
349 };
350 let (fs, fb, fsh) = (fp("fp_sig"), fp("fp_body"), fp("fp_shape"));
351 for i in 0..b.num_rows() {
352 symbols.push(Symbol {
353 id: GlobalSymbolId {
354 scheme: scheme.value(i).to_string(),
355 package_manager: pm.value(i).to_string(),
356 package: pkg.value(i).to_string(),
357 version: ver.value(i).to_string(),
358 descriptor: desc.value(i).to_string(),
359 },
360 name: name.value(i).to_string(),
361 kind: kind_from_str(kind.value(i)),
362 language: lang_from_str(lang.value(i)),
363 repo: RepoId(repo.value(i).to_string()),
364 file_path: file.value(i).to_string(),
365 range: Range {
366 start: Position {
367 line: ls.value(i),
368 column: cs.value(i),
369 },
370 end: Position {
371 line: le.value(i),
372 column: ce.value(i),
373 },
374 },
375 signature: (!sig.is_null(i)).then(|| sig.value(i).to_string()),
376 docstring: (!doc.is_null(i)).then(|| doc.value(i).to_string()),
377 fingerprint: match (fs, fb, fsh) {
378 (Some(a), Some(c), Some(d)) if !a.is_null(i) => Some(Fingerprint {
379 sig: a.value(i),
380 body: c.value(i),
381 shape: d.value(i),
382 }),
383 _ => None,
384 },
385 });
386 }
387 }
388
389 let mut edges = Vec::new();
391 let mut st = open("edges", at.map(|a| a.1))
392 .await?
393 .query()
394 .execute()
395 .await?;
396 while let Some(b) = st.try_next().await? {
397 let src = str_col(&b, "src_id")?;
398 let dst = str_col(&b, "dst_id")?;
399 let kind = str_col(&b, "kind")?;
400 let conf = b
401 .column_by_name("confidence")
402 .and_then(|c| c.as_any().downcast_ref::<Float32Array>())
403 .context("confidence column")?;
404 let cross = b
405 .column_by_name("cross_repo")
406 .and_then(|c| c.as_any().downcast_ref::<BooleanArray>())
407 .context("cross_repo column")?;
408 for i in 0..b.num_rows() {
409 edges.push(Edge {
410 src: id_from_rendered(src.value(i)),
411 dst: id_from_rendered(dst.value(i)),
412 kind: edge_kind_from_str(kind.value(i)),
413 confidence: conf.value(i),
414 cross_repo: cross.value(i),
415 });
416 }
417 }
418
419 Ok((symbols, edges))
420}
421
422fn edge_kind_from_str(s: &str) -> EdgeKind {
423 match s {
424 "Contains" => EdgeKind::Contains,
425 "Imports" => EdgeKind::Imports,
426 "Calls" => EdgeKind::Calls,
427 "Instantiates" => EdgeKind::Instantiates,
428 "Inherits" => EdgeKind::Inherits,
429 "Implements" => EdgeKind::Implements,
430 "References" => EdgeKind::References,
431 "Defines" => EdgeKind::Defines,
432 "Uses" => EdgeKind::Uses,
433 "ParticipatesIn" => EdgeKind::ParticipatesIn,
434 "Serves" => EdgeKind::Serves,
435 "Consumes" => EdgeKind::Consumes,
436 "Publishes" => EdgeKind::Publishes,
437 "Subscribes" => EdgeKind::Subscribes,
438 "Reads" => EdgeKind::Reads,
439 "Writes" => EdgeKind::Writes,
440 _ => EdgeKind::References,
441 }
442}
443
444pub async fn graph_versions(uri: &str) -> Result<Vec<(u64, u64, String)>> {
448 let db = crate::storage::connect(uri).execute().await?;
449 let s = db.open_table("symbols").execute().await?;
450 let e = db.open_table("edges").execute().await?;
451 versions_of(&s, &e).await
452}
453
454async fn versions_of(s: &lancedb::Table, e: &lancedb::Table) -> Result<Vec<(u64, u64, String)>> {
455 let (s, e) = (s.list_versions().await?, e.list_versions().await?);
456 Ok(s.iter()
457 .zip(e.iter())
458 .map(|(a, b)| (a.version, b.version, a.timestamp.to_rfc3339()))
459 .collect())
460}
461
462pub fn graph_versions_blocking(uri: &str) -> Result<Vec<(u64, u64, String)>> {
463 crate::rt::block_on(graph_versions(uri))
464}
465
466pub fn read_graph_blocking(uri: &str) -> Result<(Vec<Symbol>, Vec<Edge>)> {
467 crate::rt::block_on(read_graph(uri))
468}
469
470pub async fn count_rows(uri: &str) -> Result<(usize, usize)> {
472 let db = crate::storage::connect(uri).execute().await?;
473 let symbols = db
474 .open_table("symbols")
475 .execute()
476 .await?
477 .count_rows(None)
478 .await?;
479 let edges = db
480 .open_table("edges")
481 .execute()
482 .await?
483 .count_rows(None)
484 .await?;
485 Ok((symbols, edges))
486}
487
488pub fn write_graph_blocking(uri: &str, symbols: &[Symbol], edges: &[Edge]) -> Result<(u64, u64)> {
491 crate::rt::block_on(write_graph(uri, symbols, edges))
492}
493
494pub fn count_rows_blocking(uri: &str) -> Result<(usize, usize)> {
496 crate::rt::block_on(count_rows(uri))
497}
498
499pub fn latest_versions_blocking(uri: &str) -> Result<(u64, u64)> {
501 crate::rt::block_on(latest_versions(uri))
502}
503
504pub async fn write_repo_meta(uri: &str, repo: &str, commit: &str) -> Result<()> {
510 let schema = Arc::new(Schema::new(vec![
511 Field::new("repo", DataType::Utf8, false),
512 Field::new("commit", DataType::Utf8, false),
513 ]));
514 let batch = RecordBatch::try_new(
515 schema,
516 vec![
517 Arc::new(StringArray::from(vec![repo])) as ArrayRef,
518 Arc::new(StringArray::from(vec![commit])) as ArrayRef,
519 ],
520 )
521 .context("build repo_meta batch")?;
522 let db = crate::storage::connect(uri).execute().await?;
523 write_versioned(&db, "repo_meta", batch).await?;
524 Ok(())
525}
526
527pub async fn read_repo_meta(uri: &str) -> Result<Option<String>> {
529 let db = crate::storage::connect(uri).execute().await?;
530 let Ok(tbl) = db.open_table("repo_meta").execute().await else {
531 return Ok(None);
532 };
533 let mut st = tbl.query().execute().await?;
534 while let Some(b) = st.try_next().await? {
535 if b.num_rows() > 0 {
536 return Ok(Some(str_col(&b, "commit")?.value(0).to_string()));
537 }
538 }
539 Ok(None)
540}
541
542pub fn write_repo_meta_blocking(uri: &str, repo: &str, commit: &str) -> Result<()> {
544 crate::rt::block_on(write_repo_meta(uri, repo, commit))
545}
546
547pub async fn write_repo_commits(uri: &str, repos: &[IndexedRepo]) -> Result<()> {
550 if repos.is_empty() {
551 return Ok(());
552 }
553 let schema = Arc::new(Schema::new(vec![
554 Field::new("repo", DataType::Utf8, false),
555 Field::new("commit", DataType::Utf8, false),
556 Field::new("remote", DataType::Utf8, true),
557 Field::new("branch", DataType::Utf8, true),
558 ]));
559 let batch = RecordBatch::try_new(
560 schema,
561 vec![
562 Arc::new(StringArray::from_iter_values(
563 repos.iter().map(|r| r.name.clone()),
564 )) as ArrayRef,
565 Arc::new(StringArray::from_iter_values(
566 repos.iter().map(|r| r.commit.clone()),
567 )) as ArrayRef,
568 Arc::new(StringArray::from_iter(
569 repos.iter().map(|r| r.remote.clone()),
570 )) as ArrayRef,
571 Arc::new(StringArray::from_iter(
572 repos.iter().map(|r| r.branch.clone()),
573 )) as ArrayRef,
574 ],
575 )
576 .context("build repo_commits batch")?;
577 let db = crate::storage::connect(uri).execute().await?;
578 write_versioned(&db, "repo_meta", batch).await?;
579 Ok(())
580}
581
582#[derive(Debug, Clone, PartialEq, serde::Serialize)]
584pub struct IndexedRepo {
585 pub name: String,
586 pub commit: String,
587 pub remote: Option<String>,
588 pub branch: Option<String>,
589}
590
591pub async fn read_indexed_repos(uri: &str) -> Result<Vec<IndexedRepo>> {
593 let db = crate::storage::connect(uri).execute().await?;
594 let Ok(tbl) = db.open_table("repo_meta").execute().await else {
595 return Ok(Vec::new());
596 };
597 let mut out = Vec::new();
598 let mut st = tbl.query().execute().await?;
599 while let Some(b) = st.try_next().await? {
600 let repo = str_col(&b, "repo")?;
601 let commit = str_col(&b, "commit")?;
602 let remote = str_col(&b, "remote").ok();
603 let branch = str_col(&b, "branch").ok();
604 for i in 0..b.num_rows() {
605 out.push(IndexedRepo {
606 name: repo.value(i).to_string(),
607 commit: commit.value(i).to_string(),
608 remote: remote
609 .filter(|r| r.is_valid(i))
610 .map(|r| r.value(i).to_string()),
611 branch: branch
612 .filter(|r| r.is_valid(i))
613 .map(|r| r.value(i).to_string()),
614 });
615 }
616 }
617 out.sort_by(|a, b| a.name.cmp(&b.name));
618 Ok(out)
619}
620
621pub async fn read_repo_commits(uri: &str) -> Result<std::collections::HashMap<String, String>> {
623 Ok(read_indexed_repos(uri)
624 .await?
625 .into_iter()
626 .map(|r| (r.name, r.commit))
627 .collect())
628}
629
630pub fn write_repo_commits_blocking(uri: &str, repos: &[IndexedRepo]) -> Result<()> {
632 crate::rt::block_on(write_repo_commits(uri, repos))
633}
634
635pub fn read_repo_commits_blocking(uri: &str) -> Result<std::collections::HashMap<String, String>> {
637 crate::rt::block_on(read_repo_commits(uri))
638}
639
640pub fn read_repo_meta_blocking(uri: &str) -> Result<Option<String>> {
642 crate::rt::block_on(read_repo_meta(uri))
643}
644
645use lance_index::scalar::FullTextSearchQuery;
648use lancedb::index::scalar::FtsIndexBuilder;
649use lancedb::index::vector::IvfHnswFlatIndexBuilder;
650use lancedb::index::Index;
651use lancedb::query::{QueryBase, QueryExecutionOptions, Select};
652
653fn search_schema(dim: usize) -> Arc<Schema> {
654 let item = Arc::new(Field::new("item", DataType::Float32, true));
655 Arc::new(Schema::new(vec![
656 Field::new("symbol_id", DataType::Utf8, false),
657 Field::new("text", DataType::Utf8, false),
658 Field::new("vector", DataType::FixedSizeList(item, dim as i32), false),
659 ]))
660}
661
662pub async fn write_search_table(uri: &str, rows: &[SearchRow]) -> Result<u64> {
666 let Some((_, _, first)) = rows.first() else {
667 return Ok(0);
668 };
669 let dim = first.len();
670 let ids = StringArray::from_iter_values(rows.iter().map(|(id, _, _)| id.render()));
671 let texts = StringArray::from_iter_values(rows.iter().map(|(_, t, _)| t.clone()));
672 let flat: Vec<f32> = rows
673 .iter()
674 .flat_map(|(_, _, v)| v.iter().copied())
675 .collect();
676 let item = Arc::new(Field::new("item", DataType::Float32, true));
677 let vectors =
678 FixedSizeListArray::try_new(item, dim as i32, Arc::new(Float32Array::from(flat)), None)
679 .context("build vector column")?;
680 let batch = RecordBatch::try_new(
681 search_schema(dim),
682 vec![
683 Arc::new(ids) as ArrayRef,
684 Arc::new(texts) as ArrayRef,
685 Arc::new(vectors) as ArrayRef,
686 ],
687 )
688 .context("build search batch")?;
689
690 let db = crate::storage::connect(uri).execute().await?;
691 let version = write_versioned(&db, "search", batch).await?;
692
693 let tbl = db.open_table("search").execute().await?;
695 tbl.create_index(&["text"], Index::FTS(FtsIndexBuilder::default()))
696 .execute()
697 .await
698 .context("create FTS index")?;
699
700 let parts = (rows.len() / 20_000).max(1) as u32;
703 let ann = IvfHnswFlatIndexBuilder::default().num_partitions(parts);
704 if let Err(e) = tbl
705 .create_index(&["vector"], Index::IvfHnswFlat(ann))
706 .execute()
707 .await
708 {
709 eprintln!("comindly: vector ANN index skipped (non-fatal): {e}");
710 }
711 Ok(version)
712}
713
714pub async fn read_search_vectors(uri: &str) -> Result<Option<Vec<VectorRow>>> {
716 let db = crate::storage::connect(uri).execute().await?;
717 let Ok(tbl) = db.open_table("search").execute().await else {
718 return Ok(None);
719 };
720 let mut out = Vec::new();
721 let mut st = tbl
722 .query()
723 .select(Select::columns(&["symbol_id", "vector"]))
724 .execute()
725 .await?;
726 while let Some(b) = st.try_next().await? {
727 let ids = str_col(&b, "symbol_id")?;
728 let vecs = b
729 .column_by_name("vector")
730 .and_then(|c| c.as_any().downcast_ref::<FixedSizeListArray>())
731 .context("vector column")?;
732 for i in 0..b.num_rows() {
733 let sub = vecs.value(i);
734 let f = sub.as_any().downcast_ref::<Float32Array>().context("f32")?;
735 out.push((id_from_rendered(ids.value(i)), f.values().to_vec()));
736 }
737 }
738 Ok(Some(out))
739}
740
741pub async fn hybrid_search(
744 uri: &str,
745 query_text: &str,
746 query_vector: Vec<f32>,
747 k: usize,
748) -> Result<Vec<(String, f32)>> {
749 let db = crate::storage::connect(uri).execute().await?;
750 let tbl = db.open_table("search").execute().await?;
751 hybrid_in(&tbl, query_text, query_vector, k).await
752}
753
754async fn hybrid_in(
755 tbl: &lancedb::Table,
756 query_text: &str,
757 query_vector: Vec<f32>,
758 k: usize,
759) -> Result<Vec<(String, f32)>> {
760 let mut st = tbl
761 .query()
762 .select(lancedb::query::Select::columns(&["symbol_id"]))
763 .full_text_search(FullTextSearchQuery::new(query_text.to_owned()))
764 .nearest_to(query_vector)?
765 .limit(k)
766 .execute_hybrid(QueryExecutionOptions::default())
768 .await?;
769
770 let mut out = Vec::new();
771 let mut rank = 0usize;
772 while let Some(b) = st.try_next().await? {
773 let ids = str_col(&b, "symbol_id")?;
774 let scores = b
776 .column_by_name("_relevance_score")
777 .and_then(|c| c.as_any().downcast_ref::<Float32Array>());
778 for i in 0..b.num_rows() {
779 let score = scores
780 .map(|s| s.value(i))
781 .unwrap_or(1.0 / (rank as f32 + 1.0));
782 out.push((ids.value(i).to_string(), score));
783 rank += 1;
784 }
785 }
786 Ok(out)
787}
788
789pub struct Tables {
792 db: lancedb::Connection,
793 open: tokio::sync::Mutex<std::collections::HashMap<&'static str, lancedb::Table>>,
794}
795
796impl Tables {
797 pub async fn connect(uri: &str, fresh: std::time::Duration) -> Result<Tables> {
798 let db = crate::storage::connect(uri)
799 .read_consistency_interval(fresh)
800 .execute()
801 .await
802 .with_context(|| format!("connect lancedb at {uri}"))?;
803 Ok(Tables {
804 db,
805 open: Default::default(),
806 })
807 }
808
809 async fn table(&self, name: &'static str) -> Result<lancedb::Table> {
810 let mut open = self.open.lock().await;
811 if let Some(t) = open.get(name) {
812 return Ok(t.clone());
813 }
814 let t = self.db.open_table(name).execute().await?;
815 open.insert(name, t.clone());
816 Ok(t)
817 }
818
819 pub async fn hybrid_search(
820 &self,
821 query_text: &str,
822 query_vector: Vec<f32>,
823 k: usize,
824 ) -> Result<Vec<(String, f32)>> {
825 hybrid_in(&self.table("search").await?, query_text, query_vector, k).await
826 }
827
828 pub async fn versions(&self) -> Result<Vec<(u64, u64, String)>> {
829 versions_of(&self.table("symbols").await?, &self.table("edges").await?).await
830 }
831
832 pub async fn graph_at(&self, at: (u64, u64)) -> Result<(Vec<Symbol>, Vec<Edge>)> {
835 read_graph_in(&self.db, Some(at)).await
836 }
837}
838
839pub fn write_search_table_blocking(uri: &str, rows: &[SearchRow]) -> Result<u64> {
841 crate::rt::block_on(write_search_table(uri, rows))
842}
843pub fn read_search_vectors_blocking(uri: &str) -> Result<Option<Vec<VectorRow>>> {
844 crate::rt::block_on(read_search_vectors(uri))
845}
846pub fn hybrid_search_blocking(
847 uri: &str,
848 text: &str,
849 vector: Vec<f32>,
850 k: usize,
851) -> Result<Vec<(String, f32)>> {
852 crate::rt::block_on(hybrid_search(uri, text, vector, k))
853}
854
855fn enrichment_schema() -> Arc<Schema> {
858 Arc::new(Schema::new(vec![
859 Field::new("symbol_id", DataType::Utf8, false),
860 Field::new("summary", DataType::Utf8, false),
861 Field::new("queries", DataType::Utf8, false), ]))
863}
864
865pub async fn write_enrichment(uri: &str, rows: &[TextRow]) -> Result<u64> {
868 if rows.is_empty() {
869 return Ok(0);
870 }
871 let ids = StringArray::from_iter_values(rows.iter().map(|(id, _, _)| id.render()));
872 let summaries = StringArray::from_iter_values(rows.iter().map(|(_, s, _)| s.clone()));
873 let queries = StringArray::from_iter_values(
874 rows.iter()
875 .map(|(_, _, q)| serde_json::to_string(q).unwrap_or_else(|_| "[]".into())),
876 );
877 let batch = RecordBatch::try_new(
878 enrichment_schema(),
879 vec![
880 Arc::new(ids) as ArrayRef,
881 Arc::new(summaries) as ArrayRef,
882 Arc::new(queries) as ArrayRef,
883 ],
884 )
885 .context("build enrichment batch")?;
886 let db = crate::storage::connect(uri).execute().await?;
887 write_versioned(&db, "enrichment", batch).await
888}
889
890pub async fn read_enrichment(uri: &str) -> Result<Option<Vec<TextRow>>> {
892 let db = crate::storage::connect(uri).execute().await?;
893 let Ok(tbl) = db.open_table("enrichment").execute().await else {
894 return Ok(None);
895 };
896 let mut out = Vec::new();
897 let mut st = tbl.query().execute().await?;
898 while let Some(b) = st.try_next().await? {
899 let ids = str_col(&b, "symbol_id")?;
900 let summaries = str_col(&b, "summary")?;
901 let queries = str_col(&b, "queries")?;
902 for i in 0..b.num_rows() {
903 let q: Vec<String> = serde_json::from_str(queries.value(i)).unwrap_or_default();
904 out.push((
905 id_from_rendered(ids.value(i)),
906 summaries.value(i).to_string(),
907 q,
908 ));
909 }
910 }
911 Ok(Some(out))
912}
913
914pub fn write_enrichment_blocking(uri: &str, rows: &[TextRow]) -> Result<u64> {
916 crate::rt::block_on(write_enrichment(uri, rows))
917}
918
919pub fn read_enrichment_blocking(uri: &str) -> Result<Option<Vec<TextRow>>> {
921 crate::rt::block_on(read_enrichment(uri))
922}
923
924pub async fn write_keys(uri: &str, table: &str, keys: &[(String, String)]) -> Result<u64> {
927 let schema = Arc::new(Schema::new(vec![
928 Field::new("symbol_id", DataType::Utf8, false),
929 Field::new("key", DataType::Utf8, false),
930 ]));
931 let batch = RecordBatch::try_new(
932 schema,
933 vec![
934 Arc::new(StringArray::from_iter_values(
935 keys.iter().map(|(i, _)| i.as_str()),
936 )) as ArrayRef,
937 Arc::new(StringArray::from_iter_values(
938 keys.iter().map(|(_, k)| k.as_str()),
939 )) as ArrayRef,
940 ],
941 )?;
942 let db = crate::storage::connect(uri).execute().await?;
943 write_versioned(&db, table, batch).await
944}
945
946pub async fn read_keys(uri: &str, table: &str) -> Result<HashMap<String, String>> {
948 let db = crate::storage::connect(uri).execute().await?;
949 let tbl = db.open_table(table).execute().await?;
950 let mut out = HashMap::new();
951 let mut st = tbl.query().execute().await?;
952 while let Some(b) = st.try_next().await? {
953 let (ids, keys) = (str_col(&b, "symbol_id")?, str_col(&b, "key")?);
954 for i in 0..b.num_rows() {
955 out.insert(ids.value(i).to_string(), keys.value(i).to_string());
956 }
957 }
958 Ok(out)
959}
960
961pub fn write_keys_blocking(uri: &str, table: &str, keys: &[(String, String)]) -> Result<u64> {
962 crate::rt::block_on(write_keys(uri, table, keys))
963}
964
965pub fn read_keys_blocking(uri: &str, table: &str) -> HashMap<String, String> {
966 crate::rt::block_on(read_keys(uri, table)).unwrap_or_default()
967}
968
969fn flows_schema() -> Arc<Schema> {
970 Arc::new(Schema::new(vec![
971 Field::new("symbol_id", DataType::Utf8, false),
972 Field::new("narration", DataType::Utf8, false),
973 Field::new("queries", DataType::Utf8, false), ]))
975}
976
977pub async fn write_flows(uri: &str, rows: &[TextRow]) -> Result<u64> {
979 if rows.is_empty() {
980 return Ok(0);
981 }
982 let ids = StringArray::from_iter_values(rows.iter().map(|(id, _, _)| id.render()));
983 let narr = StringArray::from_iter_values(rows.iter().map(|(_, n, _)| n.clone()));
984 let queries = StringArray::from_iter_values(
985 rows.iter()
986 .map(|(_, _, q)| serde_json::to_string(q).unwrap_or_else(|_| "[]".into())),
987 );
988 let batch = RecordBatch::try_new(
989 flows_schema(),
990 vec![
991 Arc::new(ids) as ArrayRef,
992 Arc::new(narr) as ArrayRef,
993 Arc::new(queries) as ArrayRef,
994 ],
995 )
996 .context("build flows batch")?;
997 let db = crate::storage::connect(uri).execute().await?;
998 write_versioned(&db, "flows", batch).await
999}
1000
1001pub async fn read_flows(uri: &str) -> Result<Option<Vec<TextRow>>> {
1003 let db = crate::storage::connect(uri).execute().await?;
1004 let Ok(tbl) = db.open_table("flows").execute().await else {
1005 return Ok(None);
1006 };
1007 let mut out = Vec::new();
1008 let mut st = tbl.query().execute().await?;
1009 while let Some(b) = st.try_next().await? {
1010 let ids = str_col(&b, "symbol_id")?;
1011 let narr = str_col(&b, "narration")?;
1012 let queries = str_col(&b, "queries")?;
1013 for i in 0..b.num_rows() {
1014 let q: Vec<String> = serde_json::from_str(queries.value(i)).unwrap_or_default();
1015 out.push((id_from_rendered(ids.value(i)), narr.value(i).to_string(), q));
1016 }
1017 }
1018 Ok(Some(out))
1019}
1020
1021pub fn write_flows_blocking(uri: &str, rows: &[TextRow]) -> Result<u64> {
1023 crate::rt::block_on(write_flows(uri, rows))
1024}
1025pub fn read_flows_blocking(uri: &str) -> Result<Option<Vec<TextRow>>> {
1026 crate::rt::block_on(read_flows(uri))
1027}
1028
1029pub async fn write_style_guide(uri: &str, rows: &[(String, String)]) -> Result<()> {
1032 let schema = Arc::new(Schema::new(vec![
1033 Field::new("repo", DataType::Utf8, false),
1034 Field::new("content", DataType::Utf8, false),
1035 ]));
1036 let repos: Vec<&str> = rows.iter().map(|(r, _)| r.as_str()).collect();
1037 let contents: Vec<&str> = rows.iter().map(|(_, c)| c.as_str()).collect();
1038 let batch = RecordBatch::try_new(
1039 schema,
1040 vec![
1041 Arc::new(StringArray::from(repos)) as ArrayRef,
1042 Arc::new(StringArray::from(contents)) as ArrayRef,
1043 ],
1044 )
1045 .context("build style_guide batch")?;
1046 let db = crate::storage::connect(uri).execute().await?;
1047 write_versioned(&db, "style_guide", batch).await?;
1048 Ok(())
1049}
1050
1051pub async fn read_style_guide(uri: &str) -> Result<Vec<(String, String)>> {
1053 let db = crate::storage::connect(uri).execute().await?;
1054 let Ok(tbl) = db.open_table("style_guide").execute().await else {
1055 return Ok(vec![]);
1056 };
1057 let mut st = tbl.query().execute().await?;
1058 let mut out = Vec::new();
1059 while let Some(b) = st.try_next().await? {
1060 let repo = str_col(&b, "repo")?;
1061 let content = str_col(&b, "content")?;
1062 for i in 0..b.num_rows() {
1063 out.push((repo.value(i).to_string(), content.value(i).to_string()));
1064 }
1065 }
1066 Ok(out)
1067}
1068
1069pub fn write_style_guide_blocking(uri: &str, rows: &[(String, String)]) -> Result<()> {
1071 crate::rt::block_on(write_style_guide(uri, rows))
1072}
1073
1074pub fn read_style_guide_blocking(uri: &str) -> Result<Vec<(String, String)>> {
1076 crate::rt::block_on(read_style_guide(uri))
1077}
1078
1079pub async fn write_facts(uri: &str, facts: &[FileFacts]) -> Result<()> {
1081 let schema = Arc::new(Schema::new(vec![
1082 Field::new("repo", DataType::Utf8, false),
1083 Field::new("path", DataType::Utf8, false),
1084 Field::new("facts", DataType::Utf8, false),
1085 ]));
1086 let json: Vec<String> = facts
1087 .iter()
1088 .map(serde_json::to_string)
1089 .collect::<Result<_, _>>()
1090 .context("serialize facts")?;
1091 let batch = RecordBatch::try_new(
1092 schema,
1093 vec![
1094 Arc::new(StringArray::from_iter_values(
1095 facts.iter().map(|f| f.repo.as_str()),
1096 )) as ArrayRef,
1097 Arc::new(StringArray::from_iter_values(
1098 facts.iter().map(|f| f.path.as_str()),
1099 )),
1100 Arc::new(StringArray::from(json)),
1101 ],
1102 )
1103 .context("build file_facts batch")?;
1104 let db = crate::storage::connect(uri).execute().await?;
1105 write_versioned(&db, "file_facts", batch).await?;
1106 Ok(())
1107}
1108
1109pub async fn read_facts(uri: &str) -> Result<Option<Vec<FileFacts>>> {
1111 let db = crate::storage::connect(uri).execute().await?;
1112 let Ok(tbl) = db.open_table("file_facts").execute().await else {
1113 return Ok(None);
1114 };
1115 let mut st = tbl.query().execute().await?;
1116 let mut out = Vec::new();
1117 while let Some(b) = st.try_next().await? {
1118 let col = str_col(&b, "facts")?;
1119 for i in 0..b.num_rows() {
1120 out.push(serde_json::from_str(col.value(i)).context("parse facts row")?);
1121 }
1122 }
1123 Ok(Some(out))
1124}
1125
1126pub async fn write_recipes(uri: &str, rows: &[crate::conventions::Convention]) -> Result<()> {
1128 let schema = Arc::new(Schema::new(vec![
1129 Field::new("repo", DataType::Utf8, false),
1130 Field::new("row", DataType::Utf8, false),
1131 ]));
1132 let json: Vec<String> = rows
1133 .iter()
1134 .map(serde_json::to_string)
1135 .collect::<Result<_, _>>()
1136 .context("serialize recipes")?;
1137 let batch = RecordBatch::try_new(
1138 schema,
1139 vec![
1140 Arc::new(StringArray::from_iter_values(
1141 rows.iter().map(|r| r.repo.as_str()),
1142 )) as ArrayRef,
1143 Arc::new(StringArray::from(json)),
1144 ],
1145 )
1146 .context("build recipes batch")?;
1147 let db = crate::storage::connect(uri).execute().await?;
1148 write_versioned(&db, "recipes", batch).await?;
1149 Ok(())
1150}
1151
1152pub async fn read_recipes(uri: &str) -> Result<Vec<crate::conventions::Convention>> {
1153 let db = crate::storage::connect(uri).execute().await?;
1154 let Ok(tbl) = db.open_table("recipes").execute().await else {
1155 return Ok(Vec::new());
1156 };
1157 let mut st = tbl.query().execute().await?;
1158 let mut out = Vec::new();
1159 while let Some(b) = st.try_next().await? {
1160 let col = str_col(&b, "row")?;
1161 for i in 0..b.num_rows() {
1162 out.push(serde_json::from_str(col.value(i)).context("parse recipe row")?);
1163 }
1164 }
1165 Ok(out)
1166}
1167
1168pub fn write_recipes_blocking(uri: &str, rows: &[crate::conventions::Convention]) -> Result<()> {
1169 crate::rt::block_on(write_recipes(uri, rows))
1170}
1171
1172pub fn read_recipes_blocking(uri: &str) -> Result<Vec<crate::conventions::Convention>> {
1173 crate::rt::block_on(read_recipes(uri))
1174}
1175
1176pub async fn write_packages(uri: &str, rows: &[crate::manifests::PackageRow]) -> Result<()> {
1178 let schema = Arc::new(Schema::new(vec![
1179 Field::new("repo", DataType::Utf8, false),
1180 Field::new("row", DataType::Utf8, false),
1181 ]));
1182 let json: Vec<String> = rows
1183 .iter()
1184 .map(serde_json::to_string)
1185 .collect::<Result<_, _>>()
1186 .context("serialize packages")?;
1187 let batch = RecordBatch::try_new(
1188 schema,
1189 vec![
1190 Arc::new(StringArray::from_iter_values(
1191 rows.iter().map(|r| r.repo.as_str()),
1192 )) as ArrayRef,
1193 Arc::new(StringArray::from(json)),
1194 ],
1195 )
1196 .context("build packages batch")?;
1197 let db = crate::storage::connect(uri).execute().await?;
1198 write_versioned(&db, "packages", batch).await?;
1199 Ok(())
1200}
1201
1202pub async fn read_packages(uri: &str) -> Result<Vec<crate::manifests::PackageRow>> {
1203 let db = crate::storage::connect(uri).execute().await?;
1204 let Ok(tbl) = db.open_table("packages").execute().await else {
1205 return Ok(Vec::new());
1206 };
1207 let mut st = tbl.query().execute().await?;
1208 let mut out = Vec::new();
1209 while let Some(b) = st.try_next().await? {
1210 let col = str_col(&b, "row")?;
1211 for i in 0..b.num_rows() {
1212 out.push(serde_json::from_str(col.value(i)).context("parse package row")?);
1213 }
1214 }
1215 Ok(out)
1216}
1217
1218pub fn write_packages_blocking(uri: &str, rows: &[crate::manifests::PackageRow]) -> Result<()> {
1219 crate::rt::block_on(write_packages(uri, rows))
1220}
1221
1222pub fn read_packages_blocking(uri: &str) -> Result<Vec<crate::manifests::PackageRow>> {
1223 crate::rt::block_on(read_packages(uri))
1224}
1225
1226pub fn write_facts_blocking(uri: &str, facts: &[FileFacts]) -> Result<()> {
1227 crate::rt::block_on(write_facts(uri, facts))
1228}
1229
1230pub fn read_facts_blocking(uri: &str) -> Result<Option<Vec<FileFacts>>> {
1231 crate::rt::block_on(read_facts(uri))
1232}
1233
1234pub async fn write_sources(
1240 uri: &str,
1241 files: &[crate::sources::SourceFile],
1242 merge: bool,
1243) -> Result<(usize, usize)> {
1244 use std::collections::HashSet;
1245 let db = crate::storage::connect(uri).execute().await?;
1246 let blobs = db.open_table("source_blobs").execute().await.ok();
1247 let mut have: HashSet<String> = HashSet::new();
1248 if let Some(t) = &blobs {
1249 let mut st = t
1250 .query()
1251 .select(Select::columns(&["hash"]))
1252 .execute()
1253 .await?;
1254 while let Some(b) = st.try_next().await? {
1255 let h = str_col(&b, "hash")?;
1256 have.extend((0..b.num_rows()).map(|i| h.value(i).to_string()));
1257 }
1258 }
1259 let mut fresh: Vec<&crate::sources::SourceFile> = Vec::new();
1260 for f in files {
1261 if have.insert(f.hash.clone()) {
1262 fresh.push(f);
1263 }
1264 }
1265 if !fresh.is_empty() {
1266 let batch = RecordBatch::try_new(
1267 Arc::new(Schema::new(vec![
1268 Field::new("hash", DataType::Utf8, false),
1269 Field::new("content", DataType::Utf8, false),
1270 ])),
1271 vec![
1272 Arc::new(StringArray::from_iter_values(fresh.iter().map(|f| &f.hash))) as ArrayRef,
1273 Arc::new(StringArray::from_iter_values(
1274 fresh.iter().map(|f| &f.content),
1275 )) as ArrayRef,
1276 ],
1277 )?;
1278 let tbl = match blobs {
1279 Some(t) => {
1280 t.add(batch).mode(AddDataMode::Append).execute().await?;
1281 t
1282 }
1283 None => db.create_table("source_blobs", batch).execute().await?,
1284 };
1285 tbl.create_index(&["hash"], Index::BTree(Default::default()))
1286 .replace(true)
1287 .execute()
1288 .await
1289 .context("index source_blobs")?;
1290 }
1291
1292 let covered: HashSet<&str> = files.iter().map(|f| f.repo.as_str()).collect();
1293 let mut rows: Vec<(String, String, String)> = if merge {
1294 read_source_files(uri)
1295 .await
1296 .unwrap_or_default()
1297 .into_iter()
1298 .filter(|(r, _, _)| !covered.contains(r.as_str()))
1299 .collect()
1300 } else {
1301 Vec::new()
1302 };
1303 rows.extend(
1304 files
1305 .iter()
1306 .map(|f| (f.repo.clone(), f.path.clone(), f.hash.clone())),
1307 );
1308 let batch = RecordBatch::try_new(
1309 Arc::new(Schema::new(vec![
1310 Field::new("repo", DataType::Utf8, false),
1311 Field::new("path", DataType::Utf8, false),
1312 Field::new("hash", DataType::Utf8, false),
1313 ])),
1314 vec![
1315 Arc::new(StringArray::from_iter_values(rows.iter().map(|r| &r.0))) as ArrayRef,
1316 Arc::new(StringArray::from_iter_values(rows.iter().map(|r| &r.1))) as ArrayRef,
1317 Arc::new(StringArray::from_iter_values(rows.iter().map(|r| &r.2))) as ArrayRef,
1318 ],
1319 )?;
1320 write_versioned(&db, "source_files", batch).await?;
1321 Ok((files.len(), fresh.len()))
1322}
1323
1324pub async fn read_source_files(uri: &str) -> Result<Vec<(String, String, String)>> {
1326 let db = crate::storage::connect(uri).execute().await?;
1327 let tbl = db.open_table("source_files").execute().await?;
1328 let mut out = Vec::new();
1329 let mut st = tbl.query().execute().await?;
1330 while let Some(b) = st.try_next().await? {
1331 let (r, p, h) = (
1332 str_col(&b, "repo")?,
1333 str_col(&b, "path")?,
1334 str_col(&b, "hash")?,
1335 );
1336 for i in 0..b.num_rows() {
1337 out.push((
1338 r.value(i).to_string(),
1339 p.value(i).to_string(),
1340 h.value(i).to_string(),
1341 ));
1342 }
1343 }
1344 Ok(out)
1345}
1346
1347pub async fn read_source(uri: &str, repo: &str, path: &str) -> Result<Option<String>> {
1349 let quote = |s: &str| format!("'{}'", s.replace('\'', "''"));
1350 let db = crate::storage::connect(uri).execute().await?;
1351 let Ok(files) = db.open_table("source_files").execute().await else {
1352 return Ok(None);
1353 };
1354 let mut st = files
1355 .query()
1356 .only_if(format!("repo = {} AND path = {}", quote(repo), quote(path)))
1357 .select(Select::columns(&["hash"]))
1358 .limit(1)
1359 .execute()
1360 .await?;
1361 let Some(hash) = first_str(&mut st, "hash").await? else {
1362 return Ok(None);
1363 };
1364 let blobs = db.open_table("source_blobs").execute().await?;
1365 let mut st = blobs
1366 .query()
1367 .only_if(format!("hash = {}", quote(&hash)))
1368 .select(Select::columns(&["content"]))
1369 .limit(1)
1370 .execute()
1371 .await?;
1372 first_str(&mut st, "content").await
1373}
1374
1375async fn first_str(
1376 st: &mut lancedb::arrow::SendableRecordBatchStream,
1377 col: &str,
1378) -> Result<Option<String>> {
1379 while let Some(b) = st.try_next().await? {
1380 if b.num_rows() > 0 {
1381 return Ok(Some(str_col(&b, col)?.value(0).to_string()));
1382 }
1383 }
1384 Ok(None)
1385}
1386
1387#[cfg(test)]
1388mod tests {
1389 use super::*;
1390
1391 #[tokio::test]
1392 async fn repo_meta_gains_remotes_and_old_tables_still_read() {
1393 let dir = std::env::temp_dir().join(format!("comindly-meta-{}", std::process::id()));
1394 let uri = dir.to_str().unwrap();
1395 let old = RecordBatch::try_new(
1396 Arc::new(Schema::new(vec![
1397 Field::new("repo", DataType::Utf8, false),
1398 Field::new("commit", DataType::Utf8, false),
1399 ])),
1400 vec![
1401 Arc::new(StringArray::from(vec!["orders"])) as ArrayRef,
1402 Arc::new(StringArray::from(vec!["abc"])) as ArrayRef,
1403 ],
1404 )
1405 .unwrap();
1406 let db = crate::storage::connect(uri).execute().await.unwrap();
1407 write_versioned(&db, "repo_meta", old).await.unwrap();
1408 let read = read_indexed_repos(uri).await.unwrap();
1409 assert_eq!(read[0].remote, None);
1410 let next = IndexedRepo {
1411 name: "orders".into(),
1412 commit: "def".into(),
1413 remote: Some("https://example.com/orders.git".into()),
1414 branch: Some("main".into()),
1415 };
1416 write_repo_commits(uri, std::slice::from_ref(&next))
1417 .await
1418 .unwrap();
1419 assert_eq!(read_indexed_repos(uri).await.unwrap(), vec![next]);
1420 std::fs::remove_dir_all(&dir).ok();
1421 }
1422
1423 #[tokio::test]
1424 async fn sources_store_each_content_once_and_merge_by_repo() {
1425 use crate::sources::SourceFile;
1426 let dir = std::env::temp_dir().join(format!("comindly-sources-idx-{}", std::process::id()));
1427 let _ = std::fs::remove_dir_all(&dir);
1428 let uri = dir.to_str().unwrap();
1429 let file = |repo: &str, path: &str, content: &str| SourceFile {
1430 repo: repo.into(),
1431 path: path.into(),
1432 hash: blake3::hash(content.as_bytes()).to_hex()[..32].to_string(),
1433 content: content.into(),
1434 };
1435 let a = file("orders", "src/it's.py", "x = 1\n");
1436 let b = file("billing", "lib/b.py", "x = 1\n");
1437 assert_eq!(
1438 write_sources(uri, &[a.clone(), b], false).await.unwrap(),
1439 (2, 1)
1440 );
1441 assert_eq!(
1442 read_source(uri, "orders", "src/it's.py")
1443 .await
1444 .unwrap()
1445 .as_deref(),
1446 Some("x = 1\n")
1447 );
1448
1449 let changed = file("orders", "src/it's.py", "x = 2\n");
1450 assert_eq!(write_sources(uri, &[changed], true).await.unwrap(), (1, 1));
1451 assert_eq!(
1452 read_source(uri, "orders", "src/it's.py")
1453 .await
1454 .unwrap()
1455 .as_deref(),
1456 Some("x = 2\n")
1457 );
1458 assert_eq!(
1459 read_source(uri, "billing", "lib/b.py")
1460 .await
1461 .unwrap()
1462 .as_deref(),
1463 Some("x = 1\n")
1464 );
1465 assert_eq!(
1466 read_source(uri, "billing", "missing.py").await.unwrap(),
1467 None
1468 );
1469 std::fs::remove_dir_all(&dir).ok();
1470 }
1471}