1use std::collections::HashMap;
7use std::pin::Pin;
8use std::sync::Arc;
9
10use anyhow::{Result, anyhow};
11use arrow_array::RecordBatch;
12use arrow_schema::Schema as ArrowSchema;
13use async_trait::async_trait;
14use dashmap::DashMap;
15use futures::{Stream, StreamExt, TryStreamExt};
16
17use uni_common::core::schema::TokenizerConfig;
18
19use super::lance_branch;
20use super::lance_directory::LanceDirectory;
21use super::traits::{RecordBatchStream, StorageBackend};
22use super::types::*;
23
24pub struct LanceDbBackend {
30 directory: LanceDirectory,
35 base_uri: String,
36 table_write_locks: DashMap<String, Arc<tokio::sync::Mutex<()>>>,
46 existence_cache: DashMap<String, bool>,
56 schema_cache: DashMap<String, Arc<ArrowSchema>>,
64}
65
66fn distance_metric_of(metric: DistanceMetric) -> lance_linalg::distance::MetricType {
68 match metric {
69 DistanceMetric::L2 => lance_linalg::distance::MetricType::L2,
70 DistanceMetric::Cosine => lance_linalg::distance::MetricType::Cosine,
71 DistanceMetric::Dot => lance_linalg::distance::MetricType::Dot,
72 }
73}
74
75impl LanceDbBackend {
76 pub async fn connect(
78 uri: &str,
79 storage_options: Option<HashMap<String, String>>,
80 ) -> Result<Self> {
81 let directory = LanceDirectory::connect(uri, storage_options).await?;
82
83 Ok(Self {
84 directory,
85 base_uri: uri.to_string(),
86 table_write_locks: DashMap::new(),
87 existence_cache: DashMap::new(),
88 schema_cache: DashMap::new(),
89 })
90 }
91
92 fn write_lock_for(&self, name: &str) -> Arc<tokio::sync::Mutex<()>> {
96 self.table_write_locks
97 .entry(name.to_string())
98 .or_insert_with(|| Arc::new(tokio::sync::Mutex::new(())))
99 .clone()
100 }
101
102 async fn write_batches(
113 &self,
114 table: &str,
115 mut batches: Vec<RecordBatch>,
116 schema: Arc<ArrowSchema>,
117 mode: lance::dataset::WriteMode,
118 ) -> Result<()> {
119 if batches.is_empty() {
120 batches.push(RecordBatch::new_empty(schema.clone()));
121 }
122 let uri = self.directory.dataset_uri(table);
123 let params = self.directory.write_params(mode);
124 let reader = arrow_array::RecordBatchIterator::new(batches.into_iter().map(Ok), schema);
125 lance::Dataset::write(reader, &uri, Some(params))
126 .await
127 .map_err(|e| anyhow!("Write to '{}' ({:?}) failed: {}", table, mode, e))?;
128 Ok(())
129 }
130
131 async fn execute_primary_scan(&self, request: &ScanRequest) -> Result<RecordBatchStream> {
140 let dataset = self.directory.open(&request.table_name).await?;
141 let mut scanner = dataset.scan();
142
143 if let ColumnProjection::Columns(cols) = &request.columns {
144 scanner.project(cols).map_err(|e| {
145 anyhow!(
146 "Project columns {:?} on '{}': {}",
147 cols,
148 request.table_name,
149 e
150 )
151 })?;
152 }
153
154 if !request.filter.is_trivially_true() {
155 let sql = request.filter.to_sql()?;
156 scanner
157 .filter(&sql)
158 .map_err(|e| anyhow!("Filter '{}' on '{}': {}", sql, request.table_name, e))?;
159 }
160
161 if let Some(limit) = request.limit {
162 scanner
163 .limit(Some(limit as i64), None)
164 .map_err(|e| anyhow!("Limit on scan of '{}': {}", request.table_name, e))?;
165 }
166
167 let stream = scanner
168 .try_into_stream()
169 .await
170 .map_err(|e| anyhow!("Scan stream on '{}': {}", request.table_name, e))?;
171
172 let mapped: Pin<Box<dyn Stream<Item = Result<RecordBatch>> + Send>> =
173 Box::pin(stream.map(|r| r.map_err(|e| anyhow!("{}", e))));
174 Ok(mapped)
175 }
176
177 async fn execute_branch_scan(
179 &self,
180 request: &ScanRequest,
181 branch: &str,
182 ) -> Result<RecordBatchStream> {
183 let uri = self.directory.dataset_uri(&request.table_name);
184 let dataset = lance_branch::open_branch(&uri, branch).await?;
185
186 let mut scanner = dataset.scan();
187 scanner.use_scalar_index(false);
194
195 if let ColumnProjection::Columns(cols) = &request.columns {
196 scanner.project(cols).map_err(|e| {
197 anyhow!(
198 "Project columns {:?} on '{}@{}': {}",
199 cols,
200 request.table_name,
201 branch,
202 e
203 )
204 })?;
205 }
206
207 if !request.filter.is_trivially_true() {
208 let sql = request.filter.to_sql()?;
209 scanner.filter(&sql).map_err(|e| {
210 anyhow!(
211 "Filter '{}' on '{}@{}': {}",
212 sql,
213 request.table_name,
214 branch,
215 e
216 )
217 })?;
218 }
219
220 if let Some(limit) = request.limit {
221 scanner
222 .limit(Some(limit as i64), None)
223 .map_err(|e| anyhow!("Limit on branched scan failed: {}", e))?;
224 }
225
226 let stream = scanner.try_into_stream().await.map_err(|e| {
227 anyhow!(
228 "Branched scan stream on '{}@{}': {}",
229 request.table_name,
230 branch,
231 e
232 )
233 })?;
234
235 let mapped: Pin<Box<dyn Stream<Item = Result<RecordBatch>> + Send>> =
236 Box::pin(stream.map(|r| r.map_err(|e| anyhow!("{}", e))));
237 Ok(mapped)
238 }
239
240 async fn execute_scan_stream(&self, request: &ScanRequest) -> Result<RecordBatchStream> {
242 if let Some(branch) = request.branch.clone() {
243 return self.execute_branch_scan(request, &branch).await;
244 }
245 self.execute_primary_scan(request).await
246 }
247}
248
249#[async_trait]
250impl StorageBackend for LanceDbBackend {
251 async fn table_names(&self) -> Result<Vec<String>> {
256 self.directory
257 .table_names()
258 .await
259 .map_err(|e| anyhow!("Failed to list tables: {}", e))
260 }
261
262 async fn table_exists(&self, name: &str) -> Result<bool> {
263 if let Some(entry) = self.existence_cache.get(name) {
264 return Ok(*entry);
265 }
266 let tables = self.table_names().await?;
267 let exists = tables.iter().any(|t| t == name);
268 let final_value = *self
273 .existence_cache
274 .entry(name.to_string())
275 .or_insert(exists);
276 Ok(final_value)
277 }
278
279 async fn create_table(&self, name: &str, batches: Vec<RecordBatch>) -> Result<()> {
280 crate::backend::table_names::validate_table_name(name)?;
283 if batches.is_empty() {
284 return Err(anyhow!(
285 "Cannot create table '{}' with empty data. Use create_empty_table instead.",
286 name
287 ));
288 }
289 let lock = self.write_lock_for(name);
296 let _guard = lock.lock().await;
297 let schema = batches[0].schema();
302 if self.table_exists(name).await? {
303 self.write_batches(name, batches, schema, lance::dataset::WriteMode::Append)
304 .await
305 .map_err(|e| anyhow!("Failed to append (fallback from create) to '{name}': {e}"))?;
306 return Ok(());
307 }
308 self.write_batches(name, batches, schema, lance::dataset::WriteMode::Create)
309 .await
310 .map_err(|e| anyhow!("Failed to create table '{name}': {e}"))?;
311 self.existence_cache.insert(name.to_string(), true);
312 Ok(())
313 }
314
315 async fn create_empty_table(&self, name: &str, schema: Arc<ArrowSchema>) -> Result<()> {
316 crate::backend::table_names::validate_table_name(name)?;
318 self.write_batches(name, Vec::new(), schema, lance::dataset::WriteMode::Create)
319 .await
320 .map_err(|e| anyhow!("Failed to create empty table '{name}': {e}"))?;
321 self.existence_cache.insert(name.to_string(), true);
322 Ok(())
323 }
324
325 async fn open_or_create_table(&self, name: &str, schema: Arc<ArrowSchema>) -> Result<()> {
326 if self.table_exists(name).await? {
327 self.directory.open(name).await?;
329 } else {
330 self.create_empty_table(name, schema).await?;
331 }
332 Ok(())
333 }
334
335 async fn drop_table(&self, name: &str) -> Result<()> {
336 self.schema_cache.remove(name);
337 self.directory
338 .remove_table(name)
339 .await
340 .map_err(|e| anyhow!("Failed to drop table '{}': {}", name, e))?;
341 self.existence_cache.insert(name.to_string(), false);
342 Ok(())
343 }
344
345 async fn notify_table_created(&self, name: &str) {
346 self.existence_cache.insert(name.to_string(), true);
351 }
352
353 async fn scan(&self, request: ScanRequest) -> Result<Vec<RecordBatch>> {
358 if !self.table_exists(&request.table_name).await? {
371 return Ok(vec![]);
372 }
373
374 let stream = self.execute_scan_stream(&request).await?;
375
376 stream
377 .try_collect()
378 .await
379 .map_err(|e| anyhow!("Failed to collect scan results: {}", e))
380 }
381
382 async fn scan_stream(&self, request: ScanRequest) -> Result<RecordBatchStream> {
383 self.execute_scan_stream(&request).await
384 }
385
386 async fn get_table_schema(&self, name: &str) -> Result<Option<Arc<ArrowSchema>>> {
387 if let Some(entry) = self.schema_cache.get(name) {
388 return Ok(Some(entry.clone()));
389 }
390 match self.directory.open(name).await {
391 Ok(dataset) => {
392 let schema: Arc<ArrowSchema> = Arc::new(dataset.schema().into());
396 self.schema_cache.insert(name.to_string(), schema.clone());
397 Ok(Some(schema))
398 }
399 Err(_) => Ok(None),
402 }
403 }
404
405 async fn count_rows(&self, table_name: &str, filter: Option<&FilterExpr>) -> Result<usize> {
406 let dataset = self.directory.open(table_name).await?;
407 let predicate = filter.map(FilterExpr::to_sql).transpose()?;
408 dataset
409 .count_rows(predicate)
410 .await
411 .map_err(|e| anyhow!("Failed to count rows in '{}': {}", table_name, e))
412 }
413
414 async fn write(
419 &self,
420 table_name: &str,
421 batches: Vec<RecordBatch>,
422 mode: WriteMode,
423 ) -> Result<()> {
424 if batches.is_empty() {
425 return Ok(());
426 }
427
428 let lock = self.write_lock_for(table_name);
436 let _guard = lock.lock().await;
437
438 let schema = batches[0].schema();
439 let lance_mode = match mode {
443 WriteMode::Append => lance::dataset::WriteMode::Append,
444 WriteMode::Overwrite => lance::dataset::WriteMode::Overwrite,
445 };
446 self.write_batches(table_name, batches, schema, lance_mode)
447 .await?;
448
449 Ok(())
450 }
451
452 async fn merge_insert(
453 &self,
454 table_name: &str,
455 on: &[&str],
456 batches: Vec<RecordBatch>,
457 ) -> Result<()> {
458 if batches.is_empty() {
459 return Ok(());
460 }
461
462 let lock = self.write_lock_for(table_name);
464 let _guard = lock.lock().await;
465
466 let schema = batches[0].schema();
470 let reader = arrow_array::RecordBatchIterator::new(batches.into_iter().map(Ok), schema);
471 let on_owned: Vec<String> = on.iter().map(|s| (*s).to_string()).collect();
473
474 let dataset = self.directory.open(table_name).await?;
481 let mut builder = lance::dataset::MergeInsertBuilder::try_new(Arc::new(dataset), on_owned)
482 .map_err(|e| anyhow!("merge_insert builder on '{}': {}", table_name, e))?;
483 builder
484 .when_matched(lance::dataset::WhenMatched::UpdateAll)
485 .when_not_matched(lance::dataset::WhenNotMatched::DoNothing);
486 let job = builder
487 .try_build()
488 .map_err(|e| anyhow!("merge_insert build on '{}': {}", table_name, e))?;
489 job.execute_reader(Box::new(reader))
490 .await
491 .map_err(|e| anyhow!("merge_insert on '{}': {}", table_name, e))?;
492 Ok(())
493 }
494
495 async fn delete_rows(&self, table_name: &str, filter: &FilterExpr) -> Result<()> {
496 let mut dataset = self.directory.open(table_name).await?;
497 dataset
498 .delete(&filter.to_sql()?)
499 .await
500 .map_err(|e| anyhow!("Failed to delete from '{}': {}", table_name, e))?;
501 Ok(())
502 }
503
504 async fn replace_table_atomic(
505 &self,
506 name: &str,
507 batches: Vec<RecordBatch>,
508 schema: Arc<ArrowSchema>,
509 ) -> Result<()> {
510 let staging_name = format!("{}_staging", name);
512 if self.table_exists(&staging_name).await? {
513 self.drop_table(&staging_name).await?;
514 }
515
516 if self.table_exists(name).await? {
517 if batches.is_empty() {
518 let mut dataset = self.directory.open(name).await?;
523 dataset
524 .delete("true")
525 .await
526 .map_err(|e| anyhow!("Failed to clear table '{}': {}", name, e))?;
527 } else {
528 let batch_schema = batches[0].schema();
529 self.write_batches(
530 name,
531 batches,
532 batch_schema,
533 lance::dataset::WriteMode::Overwrite,
534 )
535 .await
536 .map_err(|e| anyhow!("Failed to overwrite table '{}': {}", name, e))?;
537 }
538 } else if batches.is_empty() {
540 self.create_empty_table(name, schema).await?;
541 } else {
542 self.create_table(name, batches).await?;
543 }
544 Ok(())
545 }
546
547 async fn lock_table_for_write(&self, name: &str) -> crate::backend::traits::TableWriteGuard {
548 crate::backend::traits::TableWriteGuard::held(self.write_lock_for(name).lock_owned().await)
554 }
555
556 async fn get_table_version(&self, table_name: &str) -> Result<Option<u64>> {
561 if !self.table_exists(table_name).await? {
562 return Ok(None);
563 }
564 let dataset = self.directory.open(table_name).await?;
565 Ok(Some(dataset.version().version))
566 }
567
568 async fn rollback_table(&self, table_name: &str, target_version: u64) -> Result<()> {
569 let mut dataset = self
574 .directory
575 .open_at_version(table_name, target_version)
576 .await
577 .map_err(|e| {
578 anyhow!(
579 "Failed to checkout version {} for '{}': {}",
580 target_version,
581 table_name,
582 e
583 )
584 })?;
585 dataset.restore().await.map_err(|e| {
586 anyhow!(
587 "Failed to restore '{}' to version {}: {}",
588 table_name,
589 target_version,
590 e
591 )
592 })?;
593 Ok(())
594 }
595
596 async fn optimize_table(&self, table_name: &str) -> Result<()> {
601 let mut dataset = self.directory.open(table_name).await?;
602
603 lance::dataset::optimize::compact_files(
607 &mut dataset,
608 lance::dataset::optimize::CompactionOptions::default(),
609 None,
610 )
611 .await
612 .map_err(|e| anyhow!("Failed to compact '{}': {}", table_name, e))?;
613
614 let policy = lance::dataset::cleanup::CleanupPolicy {
622 before_timestamp: Some(chrono::Utc::now() - chrono::Duration::days(7)),
623 ..Default::default()
624 };
625 lance::dataset::cleanup::cleanup_old_versions(&dataset, policy)
626 .await
627 .map_err(|e| anyhow!("Failed to prune old versions of '{}': {}", table_name, e))?;
628
629 lance::index::DatasetIndexExt::optimize_indices(
630 &mut dataset,
631 &lance_index::optimize::OptimizeOptions::default(),
632 )
633 .await
634 .map_err(|e| anyhow!("Failed to optimize indices on '{}': {}", table_name, e))?;
635
636 Ok(())
637 }
638
639 async fn recover_staging(&self, name: &str) -> Result<()> {
640 let staging_name = format!("{}_staging", name);
641
642 if !self.table_exists(&staging_name).await? {
643 return Ok(());
644 }
645
646 let main_exists = self.table_exists(name).await?;
647
648 if main_exists {
649 log::info!("Cleaning up leftover staging table: {}", staging_name);
650 self.drop_table(&staging_name).await?;
651 } else {
652 log::warn!("Recovering table '{}' from staging after crash", name);
653
654 let staging = self.directory.open(&staging_name).await?;
655 let schema: Arc<ArrowSchema> = Arc::new(staging.schema().into());
656
657 let stream = staging
658 .scan()
659 .try_into_stream()
660 .await
661 .map_err(|e| anyhow!("Failed to query staging: {}", e))?;
662 let batches: Vec<RecordBatch> = stream
663 .try_collect()
664 .await
665 .map_err(|e| anyhow!("Failed to collect staging data: {}", e))?;
666
667 if batches.is_empty() {
668 self.create_empty_table(name, schema).await?;
669 } else {
670 self.create_table(name, batches).await?;
671 }
672
673 self.drop_table(&staging_name).await?;
674 log::info!("Successfully recovered table '{}' from staging", name);
675 }
676
677 Ok(())
678 }
679
680 fn invalidate_cache(&self, _table_name: &str) {}
694
695 fn clear_cache(&self) {}
697
698 fn base_uri(&self) -> &str {
703 &self.base_uri
704 }
705
706 fn branching(&self) -> Option<Arc<dyn crate::backend::branching::ForkBranching>> {
707 Some(Arc::new(super::lance_branch::LanceBranching::new(
708 self.base_uri.clone(),
709 )))
710 }
711
712 fn supports_vector_search(&self) -> bool {
717 true
718 }
719
720 fn supports_full_text_search(&self) -> bool {
721 true
722 }
723
724 fn supports_scalar_index(&self) -> bool {
725 true
726 }
727
728 #[allow(clippy::too_many_arguments)]
735 async fn vector_search(
736 &self,
737 table: &str,
738 column: &str,
739 query: &[f32],
740 k: usize,
741 metric: DistanceMetric,
742 filter: FilterExpr,
743 opts: VectorQueryOpts,
744 ) -> Result<Vec<RecordBatch>> {
745 let dataset = self.directory.open(table).await?;
746 let key = arrow_array::Float32Array::from(query.to_vec());
747 let mut scanner = dataset.scan();
748 scanner
749 .nearest(column, &key, k)
750 .map_err(|e| anyhow!("Failed to create vector search on '{}': {}", table, e))?;
751 scanner.distance_metric(distance_metric_of(metric));
756
757 if let Some(n) = opts.nprobes {
758 scanner.nprobes(n);
759 }
760 if let Some(r) = opts.refine_factor {
761 scanner.refine(r);
762 }
763 if let Some(ef) = opts.ef {
764 scanner.ef(ef);
765 }
766 if !filter.is_trivially_true() {
767 let sql = filter.to_sql()?;
768 scanner.prefilter(true);
773 scanner
774 .filter(&sql)
775 .map_err(|e| anyhow!("Vector search filter '{}' on '{}': {}", sql, table, e))?;
776 }
777
778 scanner
779 .try_into_stream()
780 .await
781 .map_err(|e| anyhow!("Vector search execution failed on '{}': {}", table, e))?
782 .try_collect()
783 .await
784 .map_err(|e| {
785 anyhow!(
786 "Failed to collect vector search results from '{}': {}",
787 table,
788 e
789 )
790 })
791 }
792
793 #[allow(clippy::too_many_arguments)]
794 async fn multivector_search(
795 &self,
796 table: &str,
797 column: &str,
798 query: &[Vec<f32>],
799 k: usize,
800 metric: DistanceMetric,
801 filter: FilterExpr,
802 opts: VectorQueryOpts,
803 ) -> Result<Vec<RecordBatch>> {
804 if query.is_empty() {
805 return Err(anyhow!("multivector_search on '{}': empty query", table));
806 }
807 let dataset = self.directory.open(table).await?;
808
809 let mut builder =
817 arrow_array::builder::ListBuilder::new(arrow_array::builder::Float32Builder::new());
818 for token in query {
819 builder.values().append_slice(token);
820 builder.append(true);
821 }
822 let key = builder.finish();
823
824 let mut scanner = dataset.scan();
825 scanner
826 .nearest(column, &key, k)
827 .map_err(|e| anyhow!("Failed to create multivector search on '{}': {}", table, e))?;
828 scanner.distance_metric(distance_metric_of(metric));
829
830 if let Some(n) = opts.nprobes {
831 scanner.nprobes(n);
832 }
833 if let Some(r) = opts.refine_factor {
834 scanner.refine(r);
835 }
836 if let Some(ef) = opts.ef {
837 scanner.ef(ef);
838 }
839 if !filter.is_trivially_true() {
840 let sql = filter.to_sql()?;
841 scanner.prefilter(true);
843 scanner.filter(&sql).map_err(|e| {
844 anyhow!("Multivector search filter '{}' on '{}': {}", sql, table, e)
845 })?;
846 }
847
848 scanner
849 .try_into_stream()
850 .await
851 .map_err(|e| anyhow!("Multivector search execution failed on '{}': {}", table, e))?
852 .try_collect()
853 .await
854 .map_err(|e| {
855 anyhow!(
856 "Failed to collect multivector search results from '{}': {}",
857 table,
858 e
859 )
860 })
861 }
862
863 async fn full_text_search(
864 &self,
865 table: &str,
866 column: &str,
867 query: &str,
868 k: usize,
869 filter: FilterExpr,
870 ) -> Result<Vec<RecordBatch>> {
871 use lance_index::scalar::FullTextSearchQuery;
872 use lance_index::scalar::inverted::query::MatchQuery;
873
874 let dataset = self.directory.open(table).await?;
875
876 let match_query = MatchQuery::new(query.to_string()).with_column(Some(column.to_string()));
879 let fts_query = FullTextSearchQuery {
880 query: match_query.into(),
881 limit: Some(k as i64),
882 wand_factor: None,
883 };
884
885 let mut scanner = dataset.scan();
886 scanner
887 .full_text_search(fts_query)
888 .map_err(|e| anyhow!("FTS query on '{}': {}", table, e))?;
889 scanner
893 .limit(Some(k as i64), None)
894 .map_err(|e| anyhow!("FTS limit on '{}': {}", table, e))?;
895
896 if !filter.is_trivially_true() {
897 let sql = filter.to_sql()?;
898 scanner
899 .filter(&sql)
900 .map_err(|e| anyhow!("FTS filter '{}' on '{}': {}", sql, table, e))?;
901 }
902
903 scanner
904 .try_into_stream()
905 .await
906 .map_err(|e| anyhow!("FTS search execution failed on '{}': {}", table, e))?
907 .try_collect()
908 .await
909 .map_err(|e| anyhow!("Failed to collect FTS results from '{}': {}", table, e))
910 }
911
912 async fn create_vector_index(
913 &self,
914 table: &str,
915 column: &str,
916 name: &str,
917 params: VectorIndexParams,
918 ) -> Result<()> {
919 use lance::index::vector::VectorIndexParams as LanceVectorParams;
920 use lance_index::vector::hnsw::builder::HnswBuildParams;
921 use lance_index::vector::ivf::IvfBuildParams;
922 use lance_index::vector::pq::PQBuildParams;
923 use lance_index::vector::sq::builder::SQBuildParams;
924
925 let dt = match params.metric {
926 DistanceMetric::L2 => lance_linalg::distance::MetricType::L2,
927 DistanceMetric::Cosine => lance_linalg::distance::MetricType::Cosine,
928 DistanceMetric::Dot => lance_linalg::distance::MetricType::Dot,
929 };
930
931 let hnsw = |m: u32, ef_construction: u32| HnswBuildParams {
937 m: m as usize,
938 ef_construction: ef_construction as usize,
939 ..Default::default()
940 };
941
942 let lance_params = match params.kind {
943 VectorIndexKind::Flat => {
945 LanceVectorParams::with_ivf_flat_params(dt, IvfBuildParams::new(1))
946 }
947 VectorIndexKind::IvfFlat { num_partitions } => LanceVectorParams::with_ivf_flat_params(
948 dt,
949 IvfBuildParams::new(num_partitions as usize),
950 ),
951 VectorIndexKind::IvfPq {
952 num_partitions,
953 num_sub_vectors,
954 num_bits,
955 } => LanceVectorParams::with_ivf_pq_params(
956 dt,
957 IvfBuildParams::new(num_partitions as usize),
958 PQBuildParams {
959 num_sub_vectors: num_sub_vectors as usize,
960 num_bits: usize::from(num_bits),
961 ..Default::default()
962 },
963 ),
964 VectorIndexKind::IvfSq { num_partitions } => LanceVectorParams::with_ivf_sq_params(
965 dt,
966 IvfBuildParams::new(num_partitions as usize),
967 SQBuildParams::default(),
968 ),
969 VectorIndexKind::IvfRq {
970 num_partitions,
971 num_bits,
972 } => LanceVectorParams::ivf_rq(
973 num_partitions as usize,
974 num_bits.unwrap_or(8),
978 dt,
979 ),
980 VectorIndexKind::HnswFlat {
981 m,
982 ef_construction,
983 num_partitions,
984 } => LanceVectorParams::ivf_hnsw(
985 dt,
986 IvfBuildParams::new(num_partitions as usize),
987 hnsw(m, ef_construction),
988 ),
989 VectorIndexKind::HnswSq {
990 m,
991 ef_construction,
992 num_partitions,
993 } => LanceVectorParams::with_ivf_hnsw_sq_params(
994 dt,
995 IvfBuildParams::new(num_partitions as usize),
996 hnsw(m, ef_construction),
997 SQBuildParams::default(),
998 ),
999 VectorIndexKind::HnswPq {
1000 m,
1001 ef_construction,
1002 num_sub_vectors,
1003 num_partitions,
1004 } => LanceVectorParams::with_ivf_hnsw_pq_params(
1005 dt,
1006 IvfBuildParams::new(num_partitions as usize),
1007 hnsw(m, ef_construction),
1008 PQBuildParams {
1009 num_sub_vectors: num_sub_vectors as usize,
1010 num_bits: 8,
1012 ..Default::default()
1013 },
1014 ),
1015 };
1016
1017 let mut dataset = self.directory.open(table).await?;
1018 lance::index::DatasetIndexExt::create_index(
1019 &mut dataset,
1020 &[column],
1021 lance_index::IndexType::Vector,
1022 Some(name.to_string()),
1023 &lance_params,
1024 true,
1025 )
1026 .await
1027 .map(|_| ())
1029 .map_err(|e| {
1030 anyhow!(
1031 "Failed to create vector index '{}' on '{}.{}': {}",
1032 name,
1033 table,
1034 column,
1035 e
1036 )
1037 })
1038 }
1039
1040 async fn create_scalar_index(
1041 &self,
1042 table: &str,
1043 columns: &[&str],
1044 index_type: ScalarIndexType,
1045 name: Option<&str>,
1046 ) -> Result<()> {
1047 let builtin = match index_type {
1051 ScalarIndexType::BTree => lance_index::scalar::BuiltinIndexType::BTree,
1052 ScalarIndexType::Bitmap => lance_index::scalar::BuiltinIndexType::Bitmap,
1053 ScalarIndexType::LabelList => lance_index::scalar::BuiltinIndexType::LabelList,
1054 };
1055 let params = lance_index::scalar::ScalarIndexParams::for_builtin(builtin);
1056
1057 let mut dataset = self.directory.open(table).await?;
1058 lance::index::DatasetIndexExt::create_index(
1059 &mut dataset,
1060 columns,
1061 lance_index::IndexType::Scalar,
1062 name.map(str::to_string),
1063 ¶ms,
1064 true,
1065 )
1066 .await
1067 .map(|_| ())
1069 .map_err(|e| {
1070 anyhow!(
1071 "Failed to create {:?} index on '{}.{:?}': {}",
1072 index_type,
1073 table,
1074 columns,
1075 e
1076 )
1077 })
1078 }
1079
1080 async fn create_fts_index(
1081 &self,
1082 table: &str,
1083 columns: &[&str],
1084 name: Option<&str>,
1085 tokenizer: &TokenizerConfig,
1086 with_positions: bool,
1087 ) -> Result<()> {
1088 let params =
1096 super::fts_analyzer::to_inverted_params(tokenizer, with_positions).map_err(|e| {
1097 anyhow!(
1098 "invalid FTS tokenizer config for '{}.{:?}': {}",
1099 table,
1100 columns,
1101 e
1102 )
1103 })?;
1104
1105 let mut dataset = self.directory.open(table).await?;
1106 lance::index::DatasetIndexExt::create_index(
1107 &mut dataset,
1108 columns,
1109 lance_index::IndexType::Inverted,
1110 name.map(str::to_string),
1111 ¶ms,
1112 true,
1113 )
1114 .await
1115 .map(|_| ())
1117 .map_err(|e| {
1118 if matches!(tokenizer, TokenizerConfig::Custom { .. })
1121 || matches!(
1122 tokenizer,
1123 TokenizerConfig::Analyzer(a)
1124 if matches!(&a.base, uni_common::core::schema::BaseTokenizer::Custom(_))
1125 )
1126 {
1127 anyhow!(
1128 "Failed to create FTS index on '{}.{:?}' with custom tokenizer {:?}: {}. \
1129 CJK/custom tokenizers require dictionary files under the directory named by \
1130 the LANCE_LANGUAGE_MODEL_HOME environment variable (uni does not ship them).",
1131 table,
1132 columns,
1133 tokenizer,
1134 e
1135 )
1136 } else {
1137 anyhow!(
1138 "Failed to create FTS index on '{}.{:?}': {}",
1139 table,
1140 columns,
1141 e
1142 )
1143 }
1144 })
1145 }
1146
1147 async fn drop_index(&self, table: &str, index_name: &str) -> Result<()> {
1148 let mut dataset = self.directory.open(table).await?;
1149 lance::index::DatasetIndexExt::drop_index(&mut dataset, index_name)
1150 .await
1151 .map_err(|e| {
1152 anyhow!(
1153 "Failed to drop index '{}' on '{}': {}",
1154 index_name,
1155 table,
1156 e
1157 )
1158 })
1159 }
1160
1161 async fn list_indexes(&self, table: &str) -> Result<Vec<IndexInfo>> {
1162 let dataset = self.directory.open(table).await?;
1163 let indices = lance::index::DatasetIndexExt::load_indices(&dataset)
1164 .await
1165 .map_err(|e| anyhow!("Failed to list indexes on '{}': {}", table, e))?;
1166
1167 let schema = dataset.schema();
1172 Ok(indices
1173 .iter()
1174 .map(|idx| IndexInfo {
1175 name: idx.name.clone(),
1176 columns: idx
1177 .fields
1178 .iter()
1179 .filter_map(|fid| schema.field_by_id(*fid).map(|f| f.name.clone()))
1180 .collect(),
1181 index_type: String::from("unknown"),
1188 })
1189 .collect())
1190 }
1191}
1192
1193#[cfg(test)]
1194mod tests {
1195 use super::*;
1196 use arrow_array::{Int64Array, UInt64Array};
1197 use arrow_schema::{DataType, Field};
1198 use tempfile::TempDir;
1199
1200 async fn create_test_backend() -> (TempDir, LanceDbBackend) {
1201 let temp_dir = TempDir::new().unwrap();
1202 let uri = temp_dir.path().to_str().unwrap();
1203 let backend = LanceDbBackend::connect(uri, None).await.unwrap();
1204 (temp_dir, backend)
1205 }
1206
1207 fn test_schema() -> Arc<ArrowSchema> {
1208 Arc::new(ArrowSchema::new(vec![
1209 Field::new("id", DataType::UInt64, false),
1210 Field::new("value", DataType::Int64, false),
1211 ]))
1212 }
1213
1214 fn test_batch(ids: Vec<u64>, values: Vec<i64>) -> RecordBatch {
1215 RecordBatch::try_new(
1216 test_schema(),
1217 vec![
1218 Arc::new(UInt64Array::from(ids)),
1219 Arc::new(Int64Array::from(values)),
1220 ],
1221 )
1222 .unwrap()
1223 }
1224
1225 #[tokio::test]
1234 async fn lance_directory_listing_matches_lancedb() {
1235 use crate::backend::lance_directory::LanceDirectory;
1236
1237 let (dir, backend) = create_test_backend().await;
1238 let uri = dir.path().to_str().unwrap();
1239 let directory = LanceDirectory::connect(uri, None).await.unwrap();
1240
1241 assert!(directory.table_names().await.unwrap().is_empty());
1244
1245 for name in [
1249 "vertices_Person",
1250 "adjacency_KNOWS_fwd",
1251 "deltas_KNOWS_bwd",
1252 "vertices_Zebra",
1253 ] {
1254 backend
1255 .create_table(name, vec![test_batch(vec![1], vec![10])])
1256 .await
1257 .unwrap();
1258 }
1259
1260 let via_lancedb = backend.table_names().await.unwrap();
1261 let via_directory = directory.table_names().await.unwrap();
1262
1263 let mut expected = via_lancedb.clone();
1264 expected.sort();
1265 assert_eq!(
1266 via_directory, expected,
1267 "LanceDirectory listing diverged from lancedb's: {via_directory:?} vs {expected:?}"
1268 );
1269
1270 for name in &via_directory {
1273 directory.open(name).await.unwrap();
1274 }
1275 }
1276
1277 #[tokio::test]
1278 async fn lock_table_for_write_provides_mutual_exclusion() {
1279 use std::sync::Arc;
1285 use std::sync::atomic::{AtomicBool, Ordering};
1286
1287 let (_dir, backend) = create_test_backend().await;
1288 let backend = Arc::new(backend);
1289
1290 let held = backend.lock_table_for_write("vertices_Doc").await;
1291
1292 let other = tokio::time::timeout(
1294 std::time::Duration::from_secs(1),
1295 backend.lock_table_for_write("vertices_Other"),
1296 )
1297 .await;
1298 assert!(
1299 other.is_ok(),
1300 "a different table's lock must be independent"
1301 );
1302 drop(other);
1303
1304 let entered = Arc::new(AtomicBool::new(false));
1306 let b2 = Arc::clone(&backend);
1307 let e2 = Arc::clone(&entered);
1308 let waiter = tokio::spawn(async move {
1309 let _g = b2.lock_table_for_write("vertices_Doc").await;
1310 e2.store(true, Ordering::SeqCst);
1311 });
1312
1313 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1315 assert!(
1316 !entered.load(Ordering::SeqCst),
1317 "second holder acquired the same-name lock while it was still held"
1318 );
1319
1320 drop(held);
1321 tokio::time::timeout(std::time::Duration::from_secs(1), waiter)
1323 .await
1324 .expect("waiter did not acquire the lock after release")
1325 .unwrap();
1326 assert!(entered.load(Ordering::SeqCst));
1327 }
1328
1329 #[tokio::test]
1330 async fn test_table_lifecycle() {
1331 let (_dir, backend) = create_test_backend().await;
1332
1333 backend
1335 .create_empty_table("test", test_schema())
1336 .await
1337 .unwrap();
1338 assert!(backend.table_exists("test").await.unwrap());
1339
1340 let names = backend.table_names().await.unwrap();
1341 assert!(names.contains(&"test".to_string()));
1342
1343 backend.drop_table("test").await.unwrap();
1345 assert!(!backend.table_exists("test").await.unwrap());
1346 }
1347
1348 #[tokio::test]
1349 async fn test_scan_with_filter() {
1350 let (_dir, backend) = create_test_backend().await;
1351
1352 backend
1353 .create_table("test", vec![test_batch(vec![1, 2, 3], vec![100, 200, 300])])
1354 .await
1355 .unwrap();
1356
1357 let batches = backend.scan(ScanRequest::all("test")).await.unwrap();
1359 let total: usize = batches.iter().map(|b| b.num_rows()).sum();
1360 assert_eq!(total, 3);
1361
1362 let batches = backend
1364 .scan(ScanRequest::all("test").with_filter(FilterExpr::compare(
1365 "id",
1366 CmpOp::Gt,
1367 Scalar::Int(1),
1368 )))
1369 .await
1370 .unwrap();
1371 let total: usize = batches.iter().map(|b| b.num_rows()).sum();
1372 assert_eq!(total, 2);
1373 }
1374
1375 #[tokio::test]
1381 async fn test_scan_propagates_errors_but_tolerates_missing_table() {
1382 let (_dir, backend) = create_test_backend().await;
1383
1384 let batches = backend.scan(ScanRequest::all("never_created")).await;
1386 assert!(
1387 matches!(batches, Ok(ref b) if b.is_empty()),
1388 "scan of a non-existent table must be Ok(empty), got {batches:?}"
1389 );
1390
1391 backend
1392 .create_table("test", vec![test_batch(vec![1, 2, 3], vec![100, 200, 300])])
1393 .await
1394 .unwrap();
1395
1396 let result = backend
1399 .scan(
1400 ScanRequest::all("test")
1401 .with_filter(FilterExpr::equals("no_such_column", Scalar::Int(1))),
1402 )
1403 .await;
1404 assert!(
1405 result.is_err(),
1406 "a scan failure on an existing table must propagate as Err, got Ok"
1407 );
1408 }
1409
1410 #[tokio::test]
1411 async fn test_write_append_and_overwrite() {
1412 let (_dir, backend) = create_test_backend().await;
1413
1414 backend
1415 .create_table("test", vec![test_batch(vec![1, 2], vec![100, 200])])
1416 .await
1417 .unwrap();
1418 assert_eq!(backend.count_rows("test", None).await.unwrap(), 2);
1419
1420 backend
1422 .write(
1423 "test",
1424 vec![test_batch(vec![3], vec![300])],
1425 WriteMode::Append,
1426 )
1427 .await
1428 .unwrap();
1429 assert_eq!(backend.count_rows("test", None).await.unwrap(), 3);
1430
1431 backend
1433 .write(
1434 "test",
1435 vec![test_batch(vec![10], vec![1000])],
1436 WriteMode::Overwrite,
1437 )
1438 .await
1439 .unwrap();
1440 assert_eq!(backend.count_rows("test", None).await.unwrap(), 1);
1441 }
1442
1443 #[tokio::test]
1444 async fn test_replace_table_atomic() {
1445 let (_dir, backend) = create_test_backend().await;
1446
1447 backend
1448 .create_table("test", vec![test_batch(vec![1, 2, 3], vec![100, 200, 300])])
1449 .await
1450 .unwrap();
1451
1452 backend
1454 .replace_table_atomic(
1455 "test",
1456 vec![test_batch(vec![4, 5], vec![400, 500])],
1457 test_schema(),
1458 )
1459 .await
1460 .unwrap();
1461 assert_eq!(backend.count_rows("test", None).await.unwrap(), 2);
1462 }
1463
1464 #[tokio::test]
1465 async fn test_version_and_rollback() {
1466 let (_dir, backend) = create_test_backend().await;
1467
1468 backend
1469 .create_table("test", vec![test_batch(vec![1], vec![100])])
1470 .await
1471 .unwrap();
1472
1473 let v1 = backend.get_table_version("test").await.unwrap().unwrap();
1474 assert!(v1 > 0);
1475
1476 backend
1478 .write(
1479 "test",
1480 vec![test_batch(vec![2], vec![200])],
1481 WriteMode::Append,
1482 )
1483 .await
1484 .unwrap();
1485 assert_eq!(backend.count_rows("test", None).await.unwrap(), 2);
1486
1487 backend.rollback_table("test", v1).await.unwrap();
1489 assert_eq!(backend.count_rows("test", None).await.unwrap(), 1);
1490 }
1491
1492 #[tokio::test]
1493 async fn test_recover_staging() {
1494 let (_dir, backend) = create_test_backend().await;
1495
1496 backend.recover_staging("test").await.unwrap();
1498 assert!(!backend.table_exists("test").await.unwrap());
1499 }
1500
1501 #[tokio::test]
1502 async fn test_get_table_schema() {
1503 let (_dir, backend) = create_test_backend().await;
1504
1505 assert!(backend.get_table_schema("missing").await.unwrap().is_none());
1507
1508 backend
1510 .create_empty_table("test", test_schema())
1511 .await
1512 .unwrap();
1513 let schema = backend.get_table_schema("test").await.unwrap().unwrap();
1514 assert_eq!(schema.fields().len(), 2);
1515 }
1516
1517 #[tokio::test]
1518 async fn test_cache_invalidation() {
1519 let (_dir, backend) = create_test_backend().await;
1526
1527 backend
1528 .create_table("test", vec![test_batch(vec![1], vec![100])])
1529 .await
1530 .unwrap();
1531
1532 let _ = backend.scan(ScanRequest::all("test")).await.unwrap();
1533 backend.invalidate_cache("test"); let _ = backend.scan(ScanRequest::all("test")).await.unwrap();
1535 backend.clear_cache();
1536 let _ = backend.scan(ScanRequest::all("test")).await.unwrap();
1537 }
1538}