1use std::io;
26use std::pin::Pin;
27use std::sync::Arc;
28use std::sync::atomic::AtomicBool;
29use std::{fmt::Debug, fs::DirEntry};
30
31use super::manifest::write_manifest;
32use futures::Stream;
33use futures::future::Either;
34use futures::{
35 StreamExt, TryStreamExt,
36 future::{self, BoxFuture},
37 stream::BoxStream,
38};
39use lance_file::format::{MAGIC, MAJOR_VERSION, MINOR_VERSION};
40use lance_io::object_writer::{ObjectWriter, WriteResult, get_etag};
41use log::warn;
42use object_store::ObjectStoreExt as OSObjectStoreExt;
43use object_store::PutOptions;
44use object_store::{Error as ObjectStoreError, ObjectStore as OSObjectStore, path::Path};
45use tracing::info;
46use url::Url;
47
48#[cfg(feature = "dynamodb")]
49pub mod dynamodb;
50pub mod external_manifest;
51
52use lance_core::{Error, Result};
53use lance_io::object_store::{ObjectStore, ObjectStoreExt, ObjectStoreParams};
54use lance_io::traits::{WriteExt, Writer};
55
56use crate::format::{IndexMetadata, Manifest, Transaction, is_detached_version};
57use lance_core::utils::tracing::{AUDIT_MODE_CREATE, AUDIT_TYPE_MANIFEST, TRACE_FILE_AUDIT};
58#[cfg(feature = "dynamodb")]
59use {
60 self::external_manifest::{ExternalManifestCommitHandler, ExternalManifestStore},
61 aws_credential_types::provider::ProvideCredentials,
62 aws_credential_types::provider::error::CredentialsError,
63 lance_io::object_store::{StorageOptions, providers::aws::build_aws_credential},
64 object_store::aws::AmazonS3ConfigKey,
65 object_store::aws::AwsCredentialProvider,
66 std::borrow::Cow,
67 std::time::{Duration, SystemTime},
68};
69
70pub const VERSIONS_DIR: &str = "_versions";
71const MANIFEST_EXTENSION: &str = "manifest";
72const DETACHED_VERSION_PREFIX: &str = "d";
73const VERSION_HINT_FILE: &str = "latest_version_hint.json";
80
81#[derive(Clone, Copy, Debug, PartialEq, Eq)]
83pub enum ManifestNamingScheme {
84 V1,
86 V2,
90}
91
92impl ManifestNamingScheme {
93 pub fn manifest_path(&self, base: &Path, version: u64) -> Path {
94 if is_detached_version(version) {
95 base.clone().join(VERSIONS_DIR).join(format!(
100 "{DETACHED_VERSION_PREFIX}{version}.{MANIFEST_EXTENSION}"
101 ))
102 } else {
103 let directory = base.clone().join(VERSIONS_DIR);
104 match self {
105 Self::V1 => directory.join(format!("{version}.{MANIFEST_EXTENSION}")),
106 Self::V2 => {
107 let inverted_version = u64::MAX - version;
108 directory.join(format!("{inverted_version:020}.{MANIFEST_EXTENSION}"))
109 }
110 }
111 }
112 }
113
114 pub fn parse_version(&self, filename: &str) -> Option<u64> {
115 let file_number = filename
116 .split_once('.')
117 .and_then(|(version_str, _)| version_str.parse::<u64>().ok());
119 match self {
120 Self::V1 => file_number,
121 Self::V2 => file_number.map(|v| u64::MAX - v),
122 }
123 }
124
125 pub fn parse_detached_version(filename: &str) -> Option<u64> {
129 if !filename.starts_with(DETACHED_VERSION_PREFIX) {
130 return None;
131 }
132 let without_prefix = &filename[DETACHED_VERSION_PREFIX.len()..];
133 without_prefix
134 .split_once('.')
135 .and_then(|(version_str, _)| version_str.parse::<u64>().ok())
136 }
137
138 pub fn detect_scheme(filename: &str) -> Option<Self> {
139 if filename.starts_with(DETACHED_VERSION_PREFIX) {
140 return Some(Self::V2);
142 }
143 if filename.ends_with(MANIFEST_EXTENSION) {
144 const V2_LEN: usize = 20 + 1 + MANIFEST_EXTENSION.len();
145 if filename.len() == V2_LEN {
146 Some(Self::V2)
147 } else {
148 Some(Self::V1)
149 }
150 } else {
151 None
152 }
153 }
154
155 pub fn detect_scheme_staging(filename: &str) -> Self {
156 if filename.chars().nth(20) == Some('.') {
159 Self::V2
160 } else {
161 Self::V1
162 }
163 }
164}
165
166pub async fn migrate_scheme_to_v2(object_store: &ObjectStore, dataset_base: &Path) -> Result<()> {
176 object_store
177 .inner
178 .list(Some(&dataset_base.clone().join(VERSIONS_DIR)))
179 .try_filter(|res| {
180 let res = if let Some(filename) = res.location.filename() {
181 ManifestNamingScheme::detect_scheme(filename) == Some(ManifestNamingScheme::V1)
182 } else {
183 false
184 };
185 future::ready(res)
186 })
187 .try_for_each_concurrent(object_store.io_parallelism(), |meta| async move {
188 let filename = meta.location.filename().unwrap();
189 let version = ManifestNamingScheme::V1.parse_version(filename).unwrap();
190 let path = ManifestNamingScheme::V2.manifest_path(dataset_base, version);
191 object_store.inner.rename(&meta.location, &path).await?;
192 Ok(())
193 })
194 .await?;
195
196 Ok(())
197}
198
199pub type ManifestWriter = for<'a> fn(
203 object_store: &'a ObjectStore,
204 manifest: &'a mut Manifest,
205 indices: Option<Vec<IndexMetadata>>,
206 path: &'a Path,
207 transaction: Option<Transaction>,
208) -> BoxFuture<'a, Result<WriteResult>>;
209
210pub fn write_manifest_file_to_path<'a>(
214 object_store: &'a ObjectStore,
215 manifest: &'a mut Manifest,
216 indices: Option<Vec<IndexMetadata>>,
217 path: &'a Path,
218 transaction: Option<Transaction>,
219) -> BoxFuture<'a, Result<WriteResult>> {
220 Box::pin(async move {
221 let mut object_writer = ObjectWriter::new(object_store, path).await?;
222 let pos = write_manifest(&mut object_writer, manifest, indices, transaction).await?;
223 object_writer
224 .write_magics(pos, MAJOR_VERSION, MINOR_VERSION, MAGIC)
225 .await?;
226 let res = Writer::shutdown(&mut object_writer).await?;
227 info!(target: TRACE_FILE_AUDIT, mode=AUDIT_MODE_CREATE, r#type=AUDIT_TYPE_MANIFEST, path = path.to_string());
228 Ok(res)
229 })
230}
231
232#[derive(Debug, Clone)]
233pub struct ManifestLocation {
234 pub version: u64,
236 pub path: Path,
238 pub size: Option<u64>,
240 pub naming_scheme: ManifestNamingScheme,
242 pub e_tag: Option<String>,
260 pub identity: Option<String>,
264}
265
266impl TryFrom<object_store::ObjectMeta> for ManifestLocation {
267 type Error = Error;
268
269 fn try_from(meta: object_store::ObjectMeta) -> Result<Self> {
270 let filename = meta.location.filename().ok_or_else(|| {
271 Error::internal("ObjectMeta location does not have a filename".to_string())
272 })?;
273 let scheme = ManifestNamingScheme::detect_scheme(filename)
274 .ok_or_else(|| Error::internal(format!("Invalid manifest filename: '{}'", filename)))?;
275 let version = scheme
276 .parse_version(filename)
277 .ok_or_else(|| Error::internal(format!("Invalid manifest filename: '{}'", filename)))?;
278 Ok(Self {
279 version,
280 path: meta.location,
281 size: Some(meta.size),
282 naming_scheme: scheme,
283 e_tag: meta.e_tag,
284 identity: None,
285 })
286 }
287}
288
289async fn current_manifest_path(
299 object_store: &ObjectStore,
300 base: &Path,
301) -> Result<ManifestLocation> {
302 if object_store.has_direct_local_paths() {
303 if let Ok(Some(location)) = current_manifest_local(base) {
304 return Ok(location);
305 }
306 } else if uses_version_hint(object_store)
307 && let Some(location) = read_version_hint_and_probe(object_store, base).await
308 {
309 return Ok(location);
310 }
311
312 resolve_version_from_listing(object_store, base).await
313}
314
315#[derive(serde::Serialize, serde::Deserialize)]
317struct VersionHint {
318 version: u64,
319}
320
321const VERSION_HINT_ENV: &str = "LANCE_USE_VERSION_HINT";
326
327fn version_hint_globally_enabled() -> bool {
328 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
329 *ENABLED.get_or_init(|| match std::env::var(VERSION_HINT_ENV) {
330 Ok(v) => !matches!(
331 v.trim().to_ascii_lowercase().as_str(),
332 "0" | "false" | "off"
333 ),
334 Err(_) => true,
335 })
336}
337
338pub fn uses_version_hint(object_store: &ObjectStore) -> bool {
347 version_hint_globally_enabled() && !object_store.list_is_lexically_ordered
348}
349
350fn version_hint_path(base: &Path) -> Path {
352 base.clone().join(VERSIONS_DIR).join(VERSION_HINT_FILE)
353}
354
355pub async fn write_version_hint(object_store: &ObjectStore, base: &Path, version: u64) {
363 if is_detached_version(version) || !uses_version_hint(object_store) {
364 return;
365 }
366 let hint_path = version_hint_path(base);
367 let content = serde_json::to_vec(&VersionHint { version }).expect("serialize version hint");
368 if let Err(e) = object_store.put(&hint_path, content.as_slice()).await {
369 warn!("Failed to write version hint file for version {version}: {e}");
370 }
371}
372
373async fn read_version_from_hint(object_store: &ObjectStore, base: &Path) -> Option<u64> {
376 let bytes = object_store
377 .inner
378 .get(&version_hint_path(base))
379 .await
380 .ok()?
381 .bytes()
382 .await
383 .ok()?;
384 Some(serde_json::from_slice::<VersionHint>(&bytes).ok()?.version)
385}
386
387async fn read_version_hint_and_probe(
392 object_store: &ObjectStore,
393 base: &Path,
394) -> Option<ManifestLocation> {
395 let hint_version = read_version_from_hint(object_store, base).await?;
396 let (version, scheme, mut probed) = probe_versions_upward(object_store, base, hint_version)
397 .await
398 .ok()
399 .flatten()?;
400 let (_, meta) = probed.pop()?;
402 Some(ManifestLocation {
403 version,
404 path: scheme.manifest_path(base, version),
405 size: Some(meta.size),
406 naming_scheme: scheme,
407 e_tag: meta.e_tag,
408 identity: None,
409 })
410}
411
412const MAX_HINT_PROBE_GAP: u64 = 1000;
416
417async fn probe_versions_upward(
434 object_store: &ObjectStore,
435 base: &Path,
436 from_version: u64,
437) -> Result<
438 Option<(
439 u64,
440 ManifestNamingScheme,
441 Vec<(u64, object_store::ObjectMeta)>,
442 )>,
443> {
444 let mut scheme = ManifestNamingScheme::V2;
446 let meta = match object_store
447 .inner
448 .head(&scheme.manifest_path(base, from_version))
449 .await
450 {
451 Ok(meta) => meta,
452 Err(ObjectStoreError::NotFound { .. }) => {
453 scheme = ManifestNamingScheme::V1;
454 match object_store
455 .inner
456 .head(&scheme.manifest_path(base, from_version))
457 .await
458 {
459 Ok(meta) => meta,
460 Err(ObjectStoreError::NotFound { .. }) => return Ok(None),
461 Err(e) => return Err(e.into()),
462 }
463 }
464 Err(e) => return Err(e.into()),
465 };
466
467 let mut probed = vec![(from_version, meta)];
468 let mut version = from_version;
469 loop {
470 let next = version + 1;
471 match object_store
472 .inner
473 .head(&scheme.manifest_path(base, next))
474 .await
475 {
476 Ok(meta) => {
477 probed.push((next, meta));
478 version = next;
479 }
480 Err(ObjectStoreError::NotFound { .. }) => break,
482 Err(e) => return Err(e.into()),
485 }
486 }
487 Ok(Some((version, scheme, probed)))
488}
489
490async fn list_manifests_since_version_with_hint(
497 object_store: &ObjectStore,
498 base: &Path,
499 since_version: u64,
500) -> Option<Vec<ManifestLocation>> {
501 let hint_version = read_version_from_hint(object_store, base).await?;
502
503 if hint_version.saturating_sub(since_version) > MAX_HINT_PROBE_GAP {
506 return None;
507 }
508
509 let probe_from = if hint_version > since_version {
512 hint_version
513 } else {
514 since_version + 1
515 };
516
517 let (scheme, probed) = match probe_versions_upward(object_store, base, probe_from).await {
518 Ok(Some((_true_latest, scheme, probed))) => (scheme, probed),
519 Ok(None) if hint_version > since_version => return None,
523 Ok(None) => return Some(Vec::new()),
524 Err(_) => return None,
526 };
527
528 let mut locations: Vec<ManifestLocation> = probed
529 .into_iter()
530 .filter(|(v, _)| *v > since_version)
531 .map(|(version, meta)| ManifestLocation {
532 version,
533 path: scheme.manifest_path(base, version),
534 size: Some(meta.size),
535 naming_scheme: scheme,
536 e_tag: meta.e_tag,
537 identity: None,
538 })
539 .collect();
540
541 if hint_version > since_version + 1 {
546 let gap_locations: Vec<ManifestLocation> =
547 futures::stream::iter((since_version + 1)..hint_version)
548 .map(|version| async move {
549 object_store
550 .inner
551 .head(&scheme.manifest_path(base, version))
552 .await
553 .map(|meta| ManifestLocation {
554 version,
555 path: scheme.manifest_path(base, version),
556 size: Some(meta.size),
557 naming_scheme: scheme,
558 e_tag: meta.e_tag,
559 identity: None,
560 })
561 })
562 .buffer_unordered(object_store.io_parallelism())
563 .try_collect()
564 .await
565 .ok()?;
566 locations.extend(gap_locations);
567 }
568
569 locations.sort_by_key(|loc| std::cmp::Reverse(loc.version));
570 Some(locations)
571}
572
573async fn resolve_version_from_listing(
575 object_store: &ObjectStore,
576 base: &Path,
577) -> Result<ManifestLocation> {
578 let manifest_files = object_store.list(Some(base.clone().join(VERSIONS_DIR)));
579
580 let mut valid_manifests = manifest_files.try_filter_map(|res| {
581 let filename = res.location.filename().unwrap();
582 if let Some(scheme) = ManifestNamingScheme::detect_scheme(filename) {
583 if scheme.parse_version(filename).is_some() {
585 future::ready(Ok(Some((scheme, res))))
586 } else {
587 future::ready(Ok(None))
588 }
589 } else {
590 future::ready(Ok(None))
591 }
592 });
593
594 let first = valid_manifests.next().await.transpose()?;
595 match (first, object_store.list_is_lexically_ordered) {
596 (Some((scheme @ ManifestNamingScheme::V2, meta)), true) => {
599 let version = scheme
600 .parse_version(meta.location.filename().unwrap())
601 .unwrap();
602
603 for (scheme, meta) in valid_manifests.take(999).try_collect::<Vec<_>>().await? {
607 if scheme != ManifestNamingScheme::V2 {
608 warn!(
609 "Found V1 Manifest in a V2 directory. Use `migrate_manifest_paths_v2` \
610 to migrate the directory."
611 );
612 break;
613 }
614 let next_version = scheme
615 .parse_version(meta.location.filename().unwrap())
616 .unwrap();
617 if next_version >= version {
618 warn!(
619 "List operation was expected to be lexically ordered, but was not. This \
620 could mean a corrupt read. Please make a bug report on the lance-format/lance \
621 GitHub repository."
622 );
623 break;
624 }
625 }
626
627 Ok(ManifestLocation {
628 version,
629 path: meta.location,
630 size: Some(meta.size),
631 naming_scheme: scheme,
632 e_tag: meta.e_tag,
633 identity: None,
634 })
635 }
636 (Some((first_scheme, meta)), _) => {
639 let mut current_version = first_scheme
640 .parse_version(meta.location.filename().unwrap())
641 .unwrap();
642 let mut current_meta = meta;
643 let scheme = first_scheme;
644
645 while let Some((entry_scheme, meta)) = valid_manifests.next().await.transpose()? {
646 if entry_scheme != scheme {
647 return Err(Error::internal(format!(
648 "Found multiple manifest naming schemes in the same directory: {:?} and {:?}. \
649 Use `migrate_manifest_paths_v2` to migrate the directory.",
650 scheme, entry_scheme
651 )));
652 }
653 let version = entry_scheme
654 .parse_version(meta.location.filename().unwrap())
655 .unwrap();
656 if version > current_version {
657 current_version = version;
658 current_meta = meta;
659 }
660 }
661 Ok(ManifestLocation {
662 version: current_version,
663 path: current_meta.location,
664 size: Some(current_meta.size),
665 naming_scheme: scheme,
666 e_tag: current_meta.e_tag,
667 identity: None,
668 })
669 }
670 (None, _) => Err(Error::not_found(
671 base.clone().join(VERSIONS_DIR).to_string(),
672 )),
673 }
674}
675
676fn current_manifest_local(base: &Path) -> std::io::Result<Option<ManifestLocation>> {
680 let path = lance_io::local::to_local_path(&base.clone().join(VERSIONS_DIR));
681 let entries = std::fs::read_dir(path)?;
682
683 let mut latest_entry: Option<(u64, DirEntry, ManifestNamingScheme)> = None;
684
685 let mut scheme: Option<ManifestNamingScheme> = None;
686
687 for entry in entries {
688 let entry = entry?;
689 let filename_raw = entry.file_name();
690 let filename = filename_raw.to_string_lossy();
691
692 let Some(entry_scheme) = ManifestNamingScheme::detect_scheme(&filename) else {
693 continue;
696 };
697
698 if let Some(scheme) = scheme {
699 if scheme != entry_scheme {
700 return Err(io::Error::new(
701 io::ErrorKind::InvalidData,
702 format!(
703 "Found multiple manifest naming schemes in the same directory: {:?} and {:?}",
704 scheme, entry_scheme
705 ),
706 ));
707 }
708 } else {
709 scheme = Some(entry_scheme);
710 }
711
712 let Some(version) = entry_scheme.parse_version(&filename) else {
713 continue;
714 };
715
716 if let Some((latest_version, _, _)) = &latest_entry {
717 if version > *latest_version {
718 latest_entry = Some((version, entry, entry_scheme));
719 }
720 } else {
721 latest_entry = Some((version, entry, entry_scheme));
722 }
723 }
724
725 if let Some((version, entry, naming_scheme)) = latest_entry {
726 let metadata = entry.metadata()?;
727 Ok(Some(ManifestLocation {
728 version,
729 path: naming_scheme.manifest_path(base, version),
730 size: Some(metadata.len()),
731 naming_scheme,
732 e_tag: Some(get_etag(&metadata)),
733 identity: None,
734 }))
735 } else {
736 Ok(None)
737 }
738}
739
740fn list_manifests<'a>(
741 base_path: &Path,
742 object_store: &'a dyn OSObjectStore,
743) -> impl Stream<Item = Result<ManifestLocation>> + 'a {
744 object_store
745 .read_dir_all(&base_path.clone().join(VERSIONS_DIR), None)
746 .filter_map(|obj_meta| {
747 futures::future::ready(
748 obj_meta
749 .map(|m| ManifestLocation::try_from(m).ok())
750 .transpose(),
751 )
752 })
753 .boxed()
754}
755
756fn detached_manifest_location_from_meta(
758 meta: object_store::ObjectMeta,
759) -> Option<ManifestLocation> {
760 let filename = meta.location.filename()?;
761 let version = ManifestNamingScheme::parse_detached_version(filename)?;
762 Some(ManifestLocation {
763 version,
764 path: meta.location,
765 size: Some(meta.size),
766 naming_scheme: ManifestNamingScheme::V2,
767 e_tag: meta.e_tag,
768 identity: None,
769 })
770}
771
772pub fn list_detached_manifests<'a>(
774 base_path: &Path,
775 object_store: &'a dyn OSObjectStore,
776) -> impl Stream<Item = Result<ManifestLocation>> + 'a {
777 object_store
778 .read_dir_all(&base_path.clone().join(VERSIONS_DIR), None)
779 .filter_map(|obj_meta| {
780 futures::future::ready(
781 obj_meta
782 .map(detached_manifest_location_from_meta)
783 .transpose(),
784 )
785 })
786 .boxed()
787}
788
789pub(crate) fn make_staging_manifest_path(base: &Path) -> Result<Path> {
790 let id = uuid::Uuid::new_v4().to_string();
791 Path::parse(format!("{base}-{id}")).map_err(|e| Error::io_source(Box::new(e)))
792}
793
794#[cfg(feature = "dynamodb")]
795const DDB_URL_QUERY_KEY: &str = "ddbTableName";
796
797pub(crate) fn default_list_manifest_locations<'a>(
799 base_path: &Path,
800 object_store: &'a ObjectStore,
801 sorted_descending: bool,
802) -> BoxStream<'a, Result<ManifestLocation>> {
803 let underlying_stream = list_manifests(base_path, &object_store.inner);
804
805 if !sorted_descending {
806 return underlying_stream.boxed();
807 }
808
809 async fn sort_stream(
810 input_stream: impl futures::Stream<Item = Result<ManifestLocation>> + Unpin,
811 ) -> Result<impl Stream<Item = Result<ManifestLocation>> + Unpin> {
812 let mut locations = input_stream.try_collect::<Vec<_>>().await?;
813 locations.sort_by_key(|m| std::cmp::Reverse(m.version));
814 Ok(futures::stream::iter(locations.into_iter().map(Ok)))
815 }
816
817 if object_store.list_is_lexically_ordered {
820 let mut peekable = underlying_stream.peekable();
822
823 futures::stream::once(async move {
824 let naming_scheme = match Pin::new(&mut peekable).peek().await {
825 Some(Ok(m)) => m.naming_scheme,
826 Some(Err(_)) => ManifestNamingScheme::V2,
829 None => ManifestNamingScheme::V2,
830 };
831
832 if naming_scheme == ManifestNamingScheme::V2 {
833 Ok(Either::Left(peekable))
835 } else {
836 sort_stream(peekable).await.map(Either::Right)
837 }
838 })
839 .try_flatten()
840 .boxed()
841 } else {
842 futures::stream::once(sort_stream(underlying_stream))
847 .try_flatten()
848 .boxed()
849 }
850}
851
852pub(crate) fn default_list_manifest_locations_since<'a>(
853 base_path: &Path,
854 object_store: &'a ObjectStore,
855 since_version: u64,
856) -> BoxStream<'a, Result<ManifestLocation>> {
857 if !uses_version_hint(object_store) {
858 return default_list_manifest_locations(base_path, object_store, true)
859 .try_take_while(move |loc| future::ready(Ok(loc.version > since_version)))
860 .boxed();
861 }
862
863 let base_path = base_path.clone();
864 futures::stream::once(async move {
865 let locations =
866 match list_manifests_since_version_with_hint(object_store, &base_path, since_version)
867 .await
868 {
869 Some(locations) => locations,
870 None => {
871 let mut locations = list_manifests(&base_path, &object_store.inner)
872 .try_collect::<Vec<_>>()
873 .await?;
874 locations.retain(|loc| loc.version > since_version);
875 locations.sort_by_key(|loc| std::cmp::Reverse(loc.version));
876 locations
877 }
878 };
879 Ok::<_, Error>(futures::stream::iter(locations.into_iter().map(Ok)))
880 })
881 .try_flatten()
882 .boxed()
883}
884
885#[async_trait::async_trait]
894#[allow(clippy::too_many_arguments)]
895pub trait CommitHandler: Debug + Send + Sync {
896 fn is_version_not_found_definitive(&self) -> bool {
904 false
905 }
906
907 fn propagate_commit_error_after_success(&self) -> bool {
914 true
915 }
916
917 async fn resolve_latest_location(
918 &self,
919 base_path: &Path,
920 object_store: &ObjectStore,
921 ) -> Result<ManifestLocation> {
922 Ok(current_manifest_path(object_store, base_path).await?)
923 }
924
925 async fn resolve_version_location(
926 &self,
927 base_path: &Path,
928 version: u64,
929 object_store: &dyn OSObjectStore,
930 ) -> Result<ManifestLocation> {
931 default_resolve_version(base_path, version, object_store).await
932 }
933
934 async fn version_exists(
940 &self,
941 base_path: &Path,
942 version: u64,
943 object_store: &dyn OSObjectStore,
944 naming_scheme: ManifestNamingScheme,
945 ) -> Result<bool> {
946 let path = naming_scheme.manifest_path(base_path, version);
947 match object_store.head(&path).await {
948 Ok(_) => Ok(true),
949 Err(ObjectStoreError::NotFound { .. }) => Ok(false),
950 Err(e) => Err(e.into()),
951 }
952 }
953
954 fn list_detached_manifest_locations<'a>(
958 &self,
959 base_path: &Path,
960 object_store: &'a ObjectStore,
961 ) -> BoxStream<'a, Result<ManifestLocation>> {
962 list_detached_manifests(base_path, &object_store.inner).boxed()
963 }
964
965 fn list_manifest_locations<'a>(
972 &self,
973 base_path: &Path,
974 object_store: &'a ObjectStore,
975 sorted_descending: bool,
976 ) -> BoxStream<'a, Result<ManifestLocation>> {
977 default_list_manifest_locations(base_path, object_store, sorted_descending)
978 }
979
980 fn list_manifest_locations_since<'a>(
988 &self,
989 base_path: &Path,
990 object_store: &'a ObjectStore,
991 since_version: u64,
992 ) -> BoxStream<'a, Result<ManifestLocation>> {
993 default_list_manifest_locations_since(base_path, object_store, since_version)
994 }
995
996 async fn commit(
1001 &self,
1002 manifest: &mut Manifest,
1003 indices: Option<Vec<IndexMetadata>>,
1004 base_path: &Path,
1005 object_store: &ObjectStore,
1006 manifest_writer: ManifestWriter,
1007 naming_scheme: ManifestNamingScheme,
1008 transaction: Option<Transaction>,
1009 ) -> std::result::Result<ManifestLocation, CommitError>;
1010
1011 fn supports_predecessor_condition(&self) -> bool {
1013 false
1014 }
1015
1016 async fn resolve_latest_identity(
1019 &self,
1020 _base_path: &Path,
1021 _object_store: &ObjectStore,
1022 ) -> Result<Option<PredecessorIdentity>> {
1023 Ok(None)
1024 }
1025
1026 async fn resolve_identity(
1029 &self,
1030 _base_path: &Path,
1031 _object_store: &ObjectStore,
1032 _version: u64,
1033 ) -> Result<Option<PredecessorIdentity>> {
1034 Ok(None)
1035 }
1036
1037 #[allow(clippy::too_many_arguments)]
1041 async fn commit_after(
1042 &self,
1043 _manifest: &mut Manifest,
1044 _indices: Option<Vec<IndexMetadata>>,
1045 _base_path: &Path,
1046 _object_store: &ObjectStore,
1047 _manifest_writer: ManifestWriter,
1048 _naming_scheme: ManifestNamingScheme,
1049 _transaction: Option<Transaction>,
1050 _predecessor: &PredecessorIdentity,
1051 ) -> std::result::Result<ManifestLocation, CommitError> {
1052 Err(CommitError::OtherError(Error::not_supported(
1053 "this commit handler cannot condition publication on the predecessor manifest",
1054 )))
1055 }
1056
1057 async fn forget_version(
1060 &self,
1061 _base_path: &Path,
1062 _version: u64,
1063 _identity: &str,
1064 ) -> Result<()> {
1065 Ok(())
1066 }
1067
1068 async fn delete(&self, _base_path: &Path) -> Result<()> {
1070 Ok(())
1071 }
1072}
1073
1074async fn default_resolve_version(
1075 base_path: &Path,
1076 version: u64,
1077 object_store: &dyn OSObjectStore,
1078) -> Result<ManifestLocation> {
1079 if is_detached_version(version) {
1080 return Ok(ManifestLocation {
1081 version,
1082 naming_scheme: ManifestNamingScheme::V2,
1085 path: ManifestNamingScheme::V2.manifest_path(base_path, version),
1087 size: None,
1088 e_tag: None,
1089 identity: None,
1090 });
1091 }
1092
1093 let scheme = ManifestNamingScheme::V2;
1095 let path = scheme.manifest_path(base_path, version);
1096 match object_store.head(&path).await {
1097 Ok(meta) => Ok(ManifestLocation {
1098 version,
1099 path,
1100 size: Some(meta.size),
1101 naming_scheme: scheme,
1102 e_tag: meta.e_tag,
1103 identity: None,
1104 }),
1105 Err(ObjectStoreError::NotFound { .. }) => {
1106 let scheme = ManifestNamingScheme::V1;
1108 Ok(ManifestLocation {
1109 version,
1110 path: scheme.manifest_path(base_path, version),
1111 size: None,
1112 naming_scheme: scheme,
1113 e_tag: None,
1114 identity: None,
1115 })
1116 }
1117 Err(e) => Err(e.into()),
1118 }
1119}
1120#[cfg(feature = "dynamodb")]
1122#[derive(Debug)]
1123struct OSObjectStoreToAwsCredAdaptor(AwsCredentialProvider);
1124
1125#[cfg(feature = "dynamodb")]
1126impl ProvideCredentials for OSObjectStoreToAwsCredAdaptor {
1127 fn provide_credentials<'a>(
1128 &'a self,
1129 ) -> aws_credential_types::provider::future::ProvideCredentials<'a>
1130 where
1131 Self: 'a,
1132 {
1133 aws_credential_types::provider::future::ProvideCredentials::new(async {
1134 let creds = self
1135 .0
1136 .get_credential()
1137 .await
1138 .map_err(|e| CredentialsError::provider_error(Box::new(e)))?;
1139 Ok(aws_credential_types::Credentials::new(
1140 &creds.key_id,
1141 &creds.secret_key,
1142 creds.token.clone(),
1143 Some(
1144 SystemTime::now()
1145 .checked_add(Duration::from_secs(
1146 60 * 10, ))
1148 .expect("overflow"),
1149 ),
1150 "",
1151 ))
1152 })
1153 }
1154}
1155
1156#[cfg(feature = "dynamodb")]
1157async fn build_dynamodb_external_store(
1158 table_name: &str,
1159 creds: AwsCredentialProvider,
1160 region: &str,
1161 endpoint: Option<String>,
1162 app_name: &str,
1163) -> Result<Arc<dyn ExternalManifestStore>> {
1164 use super::commit::dynamodb::DynamoDBExternalManifestStore;
1165 use aws_sdk_dynamodb::{
1166 Client,
1167 config::{IdentityCache, Region, retry::RetryConfig},
1168 };
1169
1170 let mut dynamodb_config = aws_sdk_dynamodb::config::Builder::new()
1171 .behavior_version_latest()
1172 .region(Some(Region::new(region.to_string())))
1173 .credentials_provider(OSObjectStoreToAwsCredAdaptor(creds))
1174 .identity_cache(IdentityCache::no_cache())
1176 .retry_config(RetryConfig::standard().with_max_attempts(5));
1179
1180 if let Some(endpoint) = endpoint {
1181 dynamodb_config = dynamodb_config.endpoint_url(endpoint);
1182 }
1183 let client = Client::from_conf(dynamodb_config.build());
1184
1185 DynamoDBExternalManifestStore::new_external_store(client.into(), table_name, app_name).await
1186}
1187
1188pub async fn commit_handler_from_url(
1189 url_or_path: &str,
1190 #[allow(unused_variables)] options: &Option<ObjectStoreParams>,
1192) -> Result<Arc<dyn CommitHandler>> {
1193 let local_handler: Arc<dyn CommitHandler> = if cfg!(windows) {
1194 Arc::new(RenameCommitHandler)
1195 } else {
1196 Arc::new(ConditionalPutCommitHandler)
1197 };
1198
1199 let url = match Url::parse(url_or_path) {
1200 Ok(url) if url.scheme().len() == 1 && cfg!(windows) => {
1201 return Ok(local_handler);
1203 }
1204 Ok(url) => url,
1205 Err(_) => {
1206 return Ok(local_handler);
1207 }
1208 };
1209
1210 match url.scheme() {
1211 "file" | "file-object-store" => Ok(local_handler),
1212 "s3" | "gs" | "az" | "abfss" | "memory" | "oss" | "tos" | "shared-memory" | "goosefs" => {
1213 Ok(Arc::new(ConditionalPutCommitHandler))
1214 }
1215 "cos" => Ok(Arc::new(TencentCosCommitHandler)),
1216 #[cfg(not(feature = "dynamodb"))]
1217 "s3+ddb" => Err(Error::invalid_input_source(
1218 "`s3+ddb://` scheme requires `dynamodb` feature to be enabled".into(),
1219 )),
1220 #[cfg(feature = "dynamodb")]
1221 "s3+ddb" => {
1222 if url.query_pairs().count() != 1 {
1223 return Err(Error::invalid_input_source(
1224 "`s3+ddb://` scheme and expects exactly one query `ddbTableName`".into(),
1225 ));
1226 }
1227 let table_name = match url.query_pairs().next() {
1228 Some((Cow::Borrowed(key), Cow::Borrowed(table_name)))
1229 if key == DDB_URL_QUERY_KEY =>
1230 {
1231 if table_name.is_empty() {
1232 return Err(Error::invalid_input_source(
1233 "`s3+ddb://` scheme requires non empty dynamodb table name".into(),
1234 ));
1235 }
1236 table_name
1237 }
1238 _ => {
1239 return Err(Error::invalid_input_source(
1240 "`s3+ddb://` scheme and expects exactly one query `ddbTableName`".into(),
1241 ));
1242 }
1243 };
1244 let options = options.clone().unwrap_or_default();
1245 let storage_options_raw =
1246 StorageOptions(options.storage_options().cloned().unwrap_or_default());
1247 let dynamo_endpoint = get_dynamodb_endpoint(&storage_options_raw);
1248 let storage_options = storage_options_raw.as_s3_options();
1249
1250 let region = storage_options.get(&AmazonS3ConfigKey::Region).cloned();
1251
1252 let accessor = options.get_accessor();
1254
1255 let provider_scheme = storage_options_raw.aws_provider_scheme()?;
1256
1257 let (aws_creds, region) = build_aws_credential(
1258 options.s3_credentials_refresh_offset,
1259 options.aws_credentials.clone(),
1260 Some(&storage_options),
1261 region,
1262 accessor,
1263 provider_scheme,
1264 )
1265 .await?;
1266
1267 Ok(Arc::new(ExternalManifestCommitHandler {
1268 external_manifest_store: build_dynamodb_external_store(
1269 table_name,
1270 aws_creds.clone(),
1271 ®ion,
1272 dynamo_endpoint,
1273 "lancedb",
1274 )
1275 .await?,
1276 }))
1277 }
1278 _ => Ok(Arc::new(UnsafeCommitHandler)),
1279 }
1280}
1281
1282#[cfg(feature = "dynamodb")]
1283fn get_dynamodb_endpoint(storage_options: &StorageOptions) -> Option<String> {
1284 if let Some(endpoint) = storage_options.0.get("dynamodb_endpoint") {
1285 Some(endpoint.clone())
1286 } else {
1287 std::env::var("DYNAMODB_ENDPOINT").ok()
1288 }
1289}
1290
1291#[derive(Debug)]
1293pub enum CommitError {
1294 CommitConflict,
1296 OtherError(Error),
1298}
1299
1300impl From<Error> for CommitError {
1301 fn from(e: Error) -> Self {
1302 Self::OtherError(e)
1303 }
1304}
1305
1306impl From<CommitError> for Error {
1307 fn from(e: CommitError) -> Self {
1308 match e {
1309 CommitError::CommitConflict => Self::internal("Commit conflict".to_string()),
1310 CommitError::OtherError(e) => e,
1311 }
1312 }
1313}
1314
1315static WARNED_ON_UNSAFE_COMMIT: AtomicBool = AtomicBool::new(false);
1317
1318pub struct UnsafeCommitHandler;
1322
1323#[async_trait::async_trait]
1324#[allow(clippy::too_many_arguments)]
1325impl CommitHandler for UnsafeCommitHandler {
1326 fn is_version_not_found_definitive(&self) -> bool {
1327 true
1328 }
1329
1330 fn propagate_commit_error_after_success(&self) -> bool {
1331 false
1332 }
1333
1334 async fn commit(
1335 &self,
1336 manifest: &mut Manifest,
1337 indices: Option<Vec<IndexMetadata>>,
1338 base_path: &Path,
1339 object_store: &ObjectStore,
1340 manifest_writer: ManifestWriter,
1341 naming_scheme: ManifestNamingScheme,
1342 transaction: Option<Transaction>,
1343 ) -> std::result::Result<ManifestLocation, CommitError> {
1344 if !WARNED_ON_UNSAFE_COMMIT.load(std::sync::atomic::Ordering::Relaxed) {
1346 WARNED_ON_UNSAFE_COMMIT.store(true, std::sync::atomic::Ordering::Relaxed);
1347 log::warn!(
1348 "Using unsafe commit handler. Concurrent writes may result in data loss. \
1349 Consider providing a commit handler that prevents conflicting writes."
1350 );
1351 }
1352
1353 let version_path = naming_scheme.manifest_path(base_path, manifest.version);
1354 let res =
1355 manifest_writer(object_store, manifest, indices, &version_path, transaction).await?;
1356
1357 write_version_hint(object_store, base_path, manifest.version).await;
1358
1359 Ok(ManifestLocation {
1360 version: manifest.version,
1361 size: Some(res.size as u64),
1362 naming_scheme,
1363 path: version_path,
1364 e_tag: res.e_tag,
1365 identity: None,
1366 })
1367 }
1368}
1369
1370impl Debug for UnsafeCommitHandler {
1371 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1372 f.debug_struct("UnsafeCommitHandler").finish()
1373 }
1374}
1375
1376#[async_trait::async_trait]
1378pub trait CommitLock: Debug {
1379 type Lease: CommitLease;
1380
1381 async fn lock(&self, version: u64) -> std::result::Result<Self::Lease, CommitError>;
1394}
1395
1396#[async_trait::async_trait]
1397pub trait CommitLease: Send + Sync {
1398 async fn release(&self, success: bool) -> std::result::Result<(), CommitError>;
1404}
1405
1406struct LeaseGuard<L: CommitLease + 'static> {
1415 lease: Option<L>,
1416}
1417
1418impl<L: CommitLease + 'static> LeaseGuard<L> {
1419 fn new(lease: L) -> Self {
1420 Self { lease: Some(lease) }
1421 }
1422
1423 async fn release(mut self, success: bool) -> std::result::Result<(), CommitError> {
1425 let result = {
1430 let lease = self
1431 .lease
1432 .as_ref()
1433 .expect("LeaseGuard released more than once");
1434 lease.release(success).await
1435 };
1436 self.lease = None;
1437 result
1438 }
1439}
1440
1441impl<L: CommitLease + 'static> Drop for LeaseGuard<L> {
1442 fn drop(&mut self) {
1443 if let Some(lease) = self.lease.take() {
1444 if let Ok(handle) = tokio::runtime::Handle::try_current() {
1449 handle.spawn(async move {
1450 let _ = lease.release(false).await;
1451 });
1452 }
1453 }
1454 }
1455}
1456
1457#[async_trait::async_trait]
1458impl<T: CommitLock + Send + Sync> CommitHandler for T
1459where
1460 T::Lease: 'static,
1461{
1462 fn is_version_not_found_definitive(&self) -> bool {
1463 true
1464 }
1465
1466 async fn commit(
1467 &self,
1468 manifest: &mut Manifest,
1469 indices: Option<Vec<IndexMetadata>>,
1470 base_path: &Path,
1471 object_store: &ObjectStore,
1472 manifest_writer: ManifestWriter,
1473 naming_scheme: ManifestNamingScheme,
1474 transaction: Option<Transaction>,
1475 ) -> std::result::Result<ManifestLocation, CommitError> {
1476 let path = naming_scheme.manifest_path(base_path, manifest.version);
1477 let lease = LeaseGuard::new(self.lock(manifest.version).await?);
1482
1483 match object_store.inner.head(&path).await {
1485 Ok(_) => {
1486 lease.release(false).await?;
1489
1490 return Err(CommitError::CommitConflict);
1491 }
1492 Err(ObjectStoreError::NotFound { .. }) => {}
1493 Err(e) => {
1494 lease.release(false).await?;
1497
1498 return Err(CommitError::OtherError(e.into()));
1499 }
1500 }
1501 let res = manifest_writer(object_store, manifest, indices, &path, transaction).await;
1502
1503 lease.release(res.is_ok()).await?;
1505
1506 let res = res?;
1507
1508 write_version_hint(object_store, base_path, manifest.version).await;
1509
1510 Ok(ManifestLocation {
1511 version: manifest.version,
1512 size: Some(res.size as u64),
1513 naming_scheme,
1514 path,
1515 e_tag: res.e_tag,
1516 identity: None,
1517 })
1518 }
1519}
1520
1521#[async_trait::async_trait]
1522impl<T: CommitLock + Send + Sync> CommitHandler for Arc<T>
1523where
1524 T::Lease: 'static,
1525{
1526 fn is_version_not_found_definitive(&self) -> bool {
1527 self.as_ref().is_version_not_found_definitive()
1528 }
1529
1530 fn propagate_commit_error_after_success(&self) -> bool {
1531 self.as_ref().propagate_commit_error_after_success()
1532 }
1533
1534 async fn commit(
1535 &self,
1536 manifest: &mut Manifest,
1537 indices: Option<Vec<IndexMetadata>>,
1538 base_path: &Path,
1539 object_store: &ObjectStore,
1540 manifest_writer: ManifestWriter,
1541 naming_scheme: ManifestNamingScheme,
1542 transaction: Option<Transaction>,
1543 ) -> std::result::Result<ManifestLocation, CommitError> {
1544 self.as_ref()
1545 .commit(
1546 manifest,
1547 indices,
1548 base_path,
1549 object_store,
1550 manifest_writer,
1551 naming_scheme,
1552 transaction,
1553 )
1554 .await
1555 }
1556}
1557
1558pub struct RenameCommitHandler;
1562
1563#[async_trait::async_trait]
1564impl CommitHandler for RenameCommitHandler {
1565 fn is_version_not_found_definitive(&self) -> bool {
1566 true
1567 }
1568
1569 fn propagate_commit_error_after_success(&self) -> bool {
1570 false
1571 }
1572
1573 async fn commit(
1574 &self,
1575 manifest: &mut Manifest,
1576 indices: Option<Vec<IndexMetadata>>,
1577 base_path: &Path,
1578 object_store: &ObjectStore,
1579 manifest_writer: ManifestWriter,
1580 naming_scheme: ManifestNamingScheme,
1581 transaction: Option<Transaction>,
1582 ) -> std::result::Result<ManifestLocation, CommitError> {
1583 let path = naming_scheme.manifest_path(base_path, manifest.version);
1587 let tmp_path = make_staging_manifest_path(&path)?;
1588
1589 let res = manifest_writer(object_store, manifest, indices, &tmp_path, transaction).await?;
1590
1591 match object_store
1592 .inner
1593 .rename_if_not_exists(&tmp_path, &path)
1594 .await
1595 {
1596 Ok(_) => {
1597 write_version_hint(object_store, base_path, manifest.version).await;
1599 Ok(ManifestLocation {
1600 version: manifest.version,
1601 path,
1602 size: Some(res.size as u64),
1603 naming_scheme,
1604 e_tag: None, identity: None,
1606 })
1607 }
1608 Err(ObjectStoreError::AlreadyExists { .. }) => {
1609 let _ = object_store.delete(&tmp_path).await;
1612
1613 return Err(CommitError::CommitConflict);
1614 }
1615 Err(e) => {
1616 return Err(CommitError::OtherError(e.into()));
1618 }
1619 }
1620 }
1621}
1622
1623impl Debug for RenameCommitHandler {
1624 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1625 f.debug_struct("RenameCommitHandler").finish()
1626 }
1627}
1628
1629pub struct ConditionalPutCommitHandler;
1630
1631#[async_trait::async_trait]
1632impl CommitHandler for ConditionalPutCommitHandler {
1633 fn is_version_not_found_definitive(&self) -> bool {
1634 true
1635 }
1636
1637 fn propagate_commit_error_after_success(&self) -> bool {
1638 false
1639 }
1640
1641 async fn commit(
1642 &self,
1643 manifest: &mut Manifest,
1644 indices: Option<Vec<IndexMetadata>>,
1645 base_path: &Path,
1646 object_store: &ObjectStore,
1647 manifest_writer: ManifestWriter,
1648 naming_scheme: ManifestNamingScheme,
1649 transaction: Option<Transaction>,
1650 ) -> std::result::Result<ManifestLocation, CommitError> {
1651 let path = naming_scheme.manifest_path(base_path, manifest.version);
1652
1653 let memory_store = ObjectStore::memory();
1654 let dummy_path = "dummy";
1655 manifest_writer(
1656 &memory_store,
1657 manifest,
1658 indices,
1659 &dummy_path.into(),
1660 transaction,
1661 )
1662 .await?;
1663 let dummy_data = memory_store.read_one_all(&dummy_path.into()).await?;
1664 let size = dummy_data.len() as u64;
1665 let res = object_store
1666 .inner
1667 .put_opts(
1668 &path,
1669 dummy_data.into(),
1670 PutOptions {
1671 mode: object_store::PutMode::Create,
1672 ..Default::default()
1673 },
1674 )
1675 .await
1676 .map_err(|err| match err {
1677 ObjectStoreError::AlreadyExists { .. } | ObjectStoreError::Precondition { .. } => {
1678 CommitError::CommitConflict
1679 }
1680 _ => CommitError::OtherError(err.into()),
1681 })?;
1682
1683 write_version_hint(object_store, base_path, manifest.version).await;
1684
1685 Ok(ManifestLocation {
1686 version: manifest.version,
1687 path,
1688 size: Some(size),
1689 naming_scheme,
1690 e_tag: res.e_tag,
1691 identity: None,
1692 })
1693 }
1694}
1695
1696impl Debug for ConditionalPutCommitHandler {
1697 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1698 f.debug_struct("ConditionalPutCommitHandler").finish()
1699 }
1700}
1701
1702struct TencentCosCommitHandler;
1710
1711#[async_trait::async_trait]
1712impl CommitHandler for TencentCosCommitHandler {
1713 fn is_version_not_found_definitive(&self) -> bool {
1714 true
1715 }
1716
1717 async fn commit(
1718 &self,
1719 _manifest: &mut Manifest,
1720 _indices: Option<Vec<IndexMetadata>>,
1721 _base_path: &Path,
1722 _object_store: &ObjectStore,
1723 _manifest_writer: ManifestWriter,
1724 _naming_scheme: ManifestNamingScheme,
1725 _transaction: Option<Transaction>,
1726 ) -> std::result::Result<ManifestLocation, CommitError> {
1727 Err(CommitError::OtherError(Error::not_supported(
1728 "Default writes to Tencent COS are disabled because COS does not reliably enforce \
1729 put-if-not-exists after bucket versioning has ever been enabled. Provide a \
1730 distributed commit_lock in Python or a custom CommitHandler in Rust.",
1731 )))
1732 }
1733}
1734
1735impl Debug for TencentCosCommitHandler {
1736 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1737 f.debug_struct("TencentCosCommitHandler").finish()
1738 }
1739}
1740
1741#[derive(Debug, Clone, PartialEq, Eq)]
1745pub struct PredecessorIdentity {
1746 pub version: u64,
1747 pub identity: String,
1748}
1749
1750#[derive(Debug, Clone)]
1751pub struct CommitConfig {
1752 pub num_retries: u32,
1753 pub skip_auto_cleanup: bool,
1754 }
1756
1757impl Default for CommitConfig {
1758 fn default() -> Self {
1759 Self {
1760 num_retries: 20,
1761 skip_auto_cleanup: false,
1762 }
1763 }
1764}
1765
1766#[cfg(test)]
1767mod tests {
1768 use std::sync::atomic::AtomicUsize;
1769
1770 use lance_core::utils::tempfile::TempObjDir;
1771
1772 use super::*;
1773
1774 #[test]
1775 fn test_manifest_naming_scheme() {
1776 let v1 = ManifestNamingScheme::V1;
1777 let v2 = ManifestNamingScheme::V2;
1778
1779 assert_eq!(
1780 v1.manifest_path(&Path::from("base"), 0),
1781 Path::from("base/_versions/0.manifest")
1782 );
1783 assert_eq!(
1784 v1.manifest_path(&Path::from("base"), 42),
1785 Path::from("base/_versions/42.manifest")
1786 );
1787
1788 assert_eq!(
1789 v2.manifest_path(&Path::from("base"), 0),
1790 Path::from("base/_versions/18446744073709551615.manifest")
1791 );
1792 assert_eq!(
1793 v2.manifest_path(&Path::from("base"), 42),
1794 Path::from("base/_versions/18446744073709551573.manifest")
1795 );
1796
1797 assert_eq!(v1.parse_version("0.manifest"), Some(0));
1798 assert_eq!(v1.parse_version("42.manifest"), Some(42));
1799 assert_eq!(
1800 v1.parse_version("42.manifest-cee4fbbb-eb19-4ea3-8ca7-54f5ec33dedc"),
1801 Some(42)
1802 );
1803
1804 assert_eq!(v2.parse_version("18446744073709551615.manifest"), Some(0));
1805 assert_eq!(v2.parse_version("18446744073709551573.manifest"), Some(42));
1806 assert_eq!(
1807 v2.parse_version("18446744073709551573.manifest-cee4fbbb-eb19-4ea3-8ca7-54f5ec33dedc"),
1808 Some(42)
1809 );
1810
1811 assert_eq!(ManifestNamingScheme::detect_scheme("0.manifest"), Some(v1));
1812 assert_eq!(
1813 ManifestNamingScheme::detect_scheme("18446744073709551615.manifest"),
1814 Some(v2)
1815 );
1816 assert_eq!(ManifestNamingScheme::detect_scheme("something else"), None);
1817 }
1818
1819 #[tokio::test]
1820 async fn test_manifest_naming_migration() {
1821 let object_store = ObjectStore::memory();
1822 let base = Path::from("base");
1823 let versions_dir = base.clone().join(VERSIONS_DIR);
1824
1825 let original_files = vec![
1827 versions_dir.clone().join("irrelevant"),
1828 ManifestNamingScheme::V1.manifest_path(&base, 0),
1829 ManifestNamingScheme::V2.manifest_path(&base, 1),
1830 ];
1831 for path in original_files {
1832 object_store.put(&path, b"".as_slice()).await.unwrap();
1833 }
1834
1835 migrate_scheme_to_v2(&object_store, &base).await.unwrap();
1836
1837 let expected_files = vec![
1838 ManifestNamingScheme::V2.manifest_path(&base, 1),
1839 ManifestNamingScheme::V2.manifest_path(&base, 0),
1840 versions_dir.clone().join("irrelevant"),
1841 ];
1842 let actual_files = object_store
1843 .inner
1844 .list(Some(&versions_dir))
1845 .map_ok(|res| res.location)
1846 .try_collect::<Vec<_>>()
1847 .await
1848 .unwrap();
1849 assert_eq!(actual_files, expected_files);
1850 }
1851
1852 #[tokio::test]
1853 #[rstest::rstest]
1854 async fn test_list_manifests_sorted(
1855 #[values(true, false)] lexical_list_store: bool,
1856 #[values(ManifestNamingScheme::V1, ManifestNamingScheme::V2)]
1857 naming_scheme: ManifestNamingScheme,
1858 ) {
1859 let tempdir;
1860 let (object_store, base) = if lexical_list_store {
1861 (Box::new(ObjectStore::memory()), Path::from("base"))
1862 } else {
1863 tempdir = TempObjDir::default();
1864 let path = tempdir.clone().join("base");
1865 let store = Box::new(ObjectStore::local());
1866 assert!(!store.list_is_lexically_ordered);
1867 (store, path)
1868 };
1869
1870 let mut expected_paths = Vec::new();
1872 for i in (0..12).rev() {
1873 let path = naming_scheme.manifest_path(&base, i);
1874 object_store.put(&path, b"".as_slice()).await.unwrap();
1875 expected_paths.push(path);
1876 }
1877
1878 let actual_versions = ConditionalPutCommitHandler
1879 .list_manifest_locations(&base, &object_store, true)
1880 .map_ok(|location| location.path)
1881 .try_collect::<Vec<_>>()
1882 .await
1883 .unwrap();
1884
1885 assert_eq!(actual_versions, expected_paths);
1886 }
1887
1888 #[tokio::test]
1889 #[rstest::rstest]
1890 async fn test_current_manifest_path(
1891 #[values(true, false)] lexical_list_store: bool,
1892 #[values(ManifestNamingScheme::V1, ManifestNamingScheme::V2)]
1893 naming_scheme: ManifestNamingScheme,
1894 ) {
1895 let mut object_store = ObjectStore::memory();
1898 object_store.list_is_lexically_ordered = lexical_list_store;
1899 let object_store = Box::new(object_store);
1900 let base = Path::from("base");
1901
1902 for version in [5, 2, 11, 0, 8, 3, 10, 1, 7, 4, 9, 6] {
1904 let path = naming_scheme.manifest_path(&base, version);
1905 object_store.put(&path, b"".as_slice()).await.unwrap();
1906 }
1907
1908 let location = current_manifest_path(&object_store, &base).await.unwrap();
1909
1910 assert_eq!(location.version, 11);
1911 assert_eq!(location.naming_scheme, naming_scheme);
1912 assert_eq!(location.path, naming_scheme.manifest_path(&base, 11));
1913 }
1914
1915 fn non_lexical_memory_store() -> Box<ObjectStore> {
1918 let mut object_store = ObjectStore::memory();
1919 object_store.list_is_lexically_ordered = false;
1920 Box::new(object_store)
1921 }
1922
1923 #[tokio::test]
1924 async fn test_write_version_hint() {
1925 let base = Path::from("base");
1926
1927 let lexical = ObjectStore::memory();
1929 write_version_hint(&lexical, &base, 42).await;
1930 assert_eq!(read_version_from_hint(&lexical, &base).await, None);
1931
1932 let object_store = non_lexical_memory_store();
1933 write_version_hint(&object_store, &base, 42).await;
1934 assert_eq!(read_version_from_hint(&object_store, &base).await, Some(42));
1935
1936 write_version_hint(&object_store, &base, 100).await;
1938 assert_eq!(
1939 read_version_from_hint(&object_store, &base).await,
1940 Some(100)
1941 );
1942
1943 write_version_hint(
1945 &object_store,
1946 &base,
1947 crate::format::DETACHED_VERSION_MASK | 7,
1948 )
1949 .await;
1950 assert_eq!(
1951 read_version_from_hint(&object_store, &base).await,
1952 Some(100)
1953 );
1954
1955 let hint_path = version_hint_path(&base);
1957 object_store
1958 .put(&hint_path, b"not json".as_slice())
1959 .await
1960 .unwrap();
1961 assert_eq!(read_version_from_hint(&object_store, &base).await, None);
1962 }
1963
1964 #[tokio::test]
1965 #[rstest::rstest]
1966 async fn test_read_version_hint_and_probe(
1967 #[values(ManifestNamingScheme::V1, ManifestNamingScheme::V2)]
1968 naming_scheme: ManifestNamingScheme,
1969 ) {
1970 let object_store = non_lexical_memory_store();
1971 let base = Path::from("base");
1972
1973 assert!(
1975 read_version_hint_and_probe(&object_store, &base)
1976 .await
1977 .is_none()
1978 );
1979
1980 for version in 1..=5 {
1981 object_store
1982 .put(&naming_scheme.manifest_path(&base, version), b"".as_slice())
1983 .await
1984 .unwrap();
1985 }
1986
1987 write_version_hint(&object_store, &base, 3).await;
1989 let location = read_version_hint_and_probe(&object_store, &base)
1990 .await
1991 .unwrap();
1992 assert_eq!(location.version, 5);
1993 assert_eq!(location.naming_scheme, naming_scheme);
1994
1995 write_version_hint(&object_store, &base, 5).await;
1997 let location = read_version_hint_and_probe(&object_store, &base)
1998 .await
1999 .unwrap();
2000 assert_eq!(location.version, 5);
2001
2002 write_version_hint(&object_store, &base, 10).await;
2004 assert!(
2005 read_version_hint_and_probe(&object_store, &base)
2006 .await
2007 .is_none()
2008 );
2009 }
2010
2011 #[tokio::test]
2012 async fn test_list_manifests_since_version_with_hint() {
2013 let object_store = non_lexical_memory_store();
2014 let base = Path::from("base");
2015 let scheme = ManifestNamingScheme::V2;
2016
2017 for version in 1..=10 {
2018 object_store
2019 .put(&scheme.manifest_path(&base, version), b"".as_slice())
2020 .await
2021 .unwrap();
2022 }
2023
2024 assert!(
2026 list_manifests_since_version_with_hint(&object_store, &base, 7)
2027 .await
2028 .is_none()
2029 );
2030
2031 write_version_hint(&object_store, &base, 10).await;
2033 assert!(matches!(
2034 list_manifests_since_version_with_hint(&object_store, &base, 10).await,
2035 Some(v) if v.is_empty()
2036 ));
2037
2038 let locations = list_manifests_since_version_with_hint(&object_store, &base, 7)
2041 .await
2042 .unwrap();
2043 assert_eq!(
2044 locations.iter().map(|l| l.version).collect::<Vec<_>>(),
2045 vec![10, 9, 8]
2046 );
2047
2048 write_version_hint(&object_store, &base, 8).await;
2050 let locations = list_manifests_since_version_with_hint(&object_store, &base, 7)
2051 .await
2052 .unwrap();
2053 assert_eq!(
2054 locations.iter().map(|l| l.version).collect::<Vec<_>>(),
2055 vec![10, 9, 8]
2056 );
2057
2058 write_version_hint(&object_store, &base, 20).await;
2060 assert!(
2061 list_manifests_since_version_with_hint(&object_store, &base, 7)
2062 .await
2063 .is_none()
2064 );
2065 }
2066
2067 #[tokio::test]
2068 async fn test_current_manifest_path_with_hint_non_lexical() {
2069 let object_store = non_lexical_memory_store();
2071 let base = Path::from("base");
2072 let naming_scheme = ManifestNamingScheme::V2;
2073
2074 for version in 1..=100 {
2075 object_store
2076 .put(&naming_scheme.manifest_path(&base, version), b"".as_slice())
2077 .await
2078 .unwrap();
2079 }
2080
2081 write_version_hint(&object_store, &base, 98).await;
2083 let location = current_manifest_path(&object_store, &base).await.unwrap();
2084 assert_eq!(location.version, 100);
2085 }
2086
2087 #[tokio::test]
2088 async fn test_current_manifest_path_with_stale_hint_falls_back_to_listing() {
2089 let object_store = non_lexical_memory_store();
2090 let base = Path::from("base");
2091 let naming_scheme = ManifestNamingScheme::V2;
2092
2093 object_store
2095 .put(&naming_scheme.manifest_path(&base, 5), b"".as_slice())
2096 .await
2097 .unwrap();
2098 write_version_hint(&object_store, &base, 10).await;
2099
2100 let location = current_manifest_path(&object_store, &base).await.unwrap();
2102 assert_eq!(location.version, 5);
2103 }
2104
2105 #[test]
2106 fn test_parse_detached_version() {
2107 assert_eq!(
2109 ManifestNamingScheme::parse_detached_version("d12345.manifest"),
2110 Some(12345)
2111 );
2112 assert_eq!(
2113 ManifestNamingScheme::parse_detached_version("d9223372036854775808.manifest"),
2114 Some(9223372036854775808)
2115 );
2116
2117 assert_eq!(
2119 ManifestNamingScheme::parse_detached_version("12345.manifest"),
2120 None
2121 );
2122
2123 assert_eq!(
2125 ManifestNamingScheme::parse_detached_version("18446744073709551615.manifest"),
2126 None
2127 );
2128
2129 assert_eq!(ManifestNamingScheme::parse_detached_version("d12345"), None);
2131 }
2132
2133 #[tokio::test]
2134 async fn test_list_detached_manifests() {
2135 use crate::format::DETACHED_VERSION_MASK;
2136 use futures::TryStreamExt;
2137
2138 let object_store = ObjectStore::memory();
2139 let base = Path::from("base");
2140 let versions_dir = base.clone().join(VERSIONS_DIR);
2141
2142 for version in [1, 2, 3] {
2144 let path = ManifestNamingScheme::V2.manifest_path(&base, version);
2145 object_store.put(&path, b"".as_slice()).await.unwrap();
2146 }
2147
2148 let detached_versions: Vec<u64> = vec![
2150 100 | DETACHED_VERSION_MASK,
2151 200 | DETACHED_VERSION_MASK,
2152 300 | DETACHED_VERSION_MASK,
2153 ];
2154 for version in &detached_versions {
2155 let path = versions_dir.clone().join(format!("d{}.manifest", version));
2156 object_store.put(&path, b"".as_slice()).await.unwrap();
2157 }
2158
2159 let detached_locations: Vec<ManifestLocation> =
2161 list_detached_manifests(&base, &object_store.inner)
2162 .try_collect()
2163 .await
2164 .unwrap();
2165
2166 assert_eq!(detached_locations.len(), 3);
2167 for loc in &detached_locations {
2168 assert_eq!(loc.naming_scheme, ManifestNamingScheme::V2);
2169 }
2170
2171 let mut found_versions: Vec<u64> = detached_locations.iter().map(|l| l.version).collect();
2172 found_versions.sort();
2173 let mut expected_versions = detached_versions.clone();
2174 expected_versions.sort();
2175 assert_eq!(found_versions, expected_versions);
2176 }
2177
2178 #[tokio::test]
2179 #[rstest::rstest]
2180 #[case::memory("memory://bucket-a/ds")]
2181 #[case::shared_memory("shared-memory://bucket-a/ds")]
2182 #[case::s3("s3://bucket-a/ds")]
2183 #[case::gs("gs://bucket-a/ds")]
2184 #[case::az("az://bucket-a/ds")]
2185 #[case::abfss("abfss://bucket-a/ds")]
2186 #[case::oss("oss://bucket-a/ds")]
2187 #[case::tos("tos://bucket-a/ds")]
2188 #[case::goosefs("goosefs://bucket-a/ds")]
2189 async fn test_commit_handler_from_url_conditional_put_schemes(#[case] url: &str) {
2190 let handler = commit_handler_from_url(url, &None).await.unwrap();
2195 assert_eq!(
2196 format!("{:?}", handler),
2197 "ConditionalPutCommitHandler",
2198 "{url} should route to ConditionalPutCommitHandler",
2199 );
2200 }
2201
2202 #[derive(Debug)]
2205 struct TrackingLock {
2206 released: Arc<AtomicBool>,
2207 }
2208
2209 struct TrackingLease {
2210 released: Arc<AtomicBool>,
2211 }
2212
2213 #[async_trait::async_trait]
2214 impl CommitLock for TrackingLock {
2215 type Lease = TrackingLease;
2216 async fn lock(&self, _version: u64) -> std::result::Result<Self::Lease, CommitError> {
2217 Ok(TrackingLease {
2218 released: self.released.clone(),
2219 })
2220 }
2221 }
2222
2223 #[async_trait::async_trait]
2224 impl CommitLease for TrackingLease {
2225 async fn release(&self, _success: bool) -> std::result::Result<(), CommitError> {
2226 self.released
2227 .store(true, std::sync::atomic::Ordering::SeqCst);
2228 Ok(())
2229 }
2230 }
2231
2232 #[derive(Debug)]
2236 struct HangingReleaseLock {
2237 release_calls: Arc<AtomicUsize>,
2238 released: Arc<AtomicBool>,
2239 }
2240
2241 struct HangingReleaseLease {
2242 release_calls: Arc<AtomicUsize>,
2243 released: Arc<AtomicBool>,
2244 }
2245
2246 #[async_trait::async_trait]
2247 impl CommitLock for HangingReleaseLock {
2248 type Lease = HangingReleaseLease;
2249 async fn lock(&self, _version: u64) -> std::result::Result<Self::Lease, CommitError> {
2250 Ok(HangingReleaseLease {
2251 release_calls: self.release_calls.clone(),
2252 released: self.released.clone(),
2253 })
2254 }
2255 }
2256
2257 #[async_trait::async_trait]
2258 impl CommitLease for HangingReleaseLease {
2259 async fn release(&self, _success: bool) -> std::result::Result<(), CommitError> {
2260 if self
2265 .release_calls
2266 .fetch_add(1, std::sync::atomic::Ordering::SeqCst)
2267 == 0
2268 {
2269 future::pending::<()>().await;
2270 unreachable!()
2271 }
2272 self.released
2273 .store(true, std::sync::atomic::Ordering::SeqCst);
2274 Ok(())
2275 }
2276 }
2277
2278 fn succeeding_manifest_writer<'a>(
2281 _object_store: &'a ObjectStore,
2282 _manifest: &'a mut Manifest,
2283 _indices: Option<Vec<IndexMetadata>>,
2284 _path: &'a Path,
2285 _transaction: Option<Transaction>,
2286 ) -> BoxFuture<'a, Result<WriteResult>> {
2287 Box::pin(async move { Ok(WriteResult::default()) })
2288 }
2289
2290 fn test_manifest() -> Manifest {
2291 use std::collections::HashMap;
2292
2293 use arrow_schema::{DataType, Field as ArrowField, Schema as ArrowSchema};
2294 use lance_core::datatypes::Schema;
2295 use lance_file::version::LanceFileVersion;
2296
2297 use crate::format::DataStorageFormat;
2298
2299 let arrow_schema = ArrowSchema::new(vec![ArrowField::new("i", DataType::Int32, false)]);
2300 Manifest::new(
2301 Schema::try_from(&arrow_schema).unwrap(),
2302 Arc::new(vec![]),
2303 DataStorageFormat::new(LanceFileVersion::Stable.resolve()),
2304 HashMap::new(),
2305 )
2306 }
2307
2308 #[tokio::test]
2309 async fn test_cos_commit_requires_custom_handler() {
2310 let handler = commit_handler_from_url("cos://bucket-a/ds", &None)
2311 .await
2312 .unwrap();
2313 assert_eq!(format!("{:?}", handler), "TencentCosCommitHandler");
2314
2315 let mut manifest = test_manifest();
2316 let error = handler
2317 .commit(
2318 &mut manifest,
2319 None,
2320 &Path::from("test"),
2321 &ObjectStore::memory(),
2322 succeeding_manifest_writer,
2323 ManifestNamingScheme::V2,
2324 None,
2325 )
2326 .await
2327 .unwrap_err();
2328 let CommitError::OtherError(error) = error else {
2329 panic!("expected a not-supported commit error");
2330 };
2331 assert!(matches!(error, Error::NotSupported { .. }));
2332 assert!(error.to_string().contains("distributed commit_lock"));
2333 }
2334
2335 fn hanging_manifest_writer<'a>(
2337 _object_store: &'a ObjectStore,
2338 _manifest: &'a mut Manifest,
2339 _indices: Option<Vec<IndexMetadata>>,
2340 _path: &'a Path,
2341 _transaction: Option<Transaction>,
2342 ) -> BoxFuture<'a, Result<WriteResult>> {
2343 Box::pin(async move {
2344 future::pending::<()>().await;
2345 unreachable!()
2346 })
2347 }
2348
2349 #[tokio::test]
2352 async fn test_commit_lock_released_on_cancellation() {
2353 use std::sync::atomic::Ordering;
2354 use std::time::Duration;
2355
2356 let released = Arc::new(AtomicBool::new(false));
2357 let lock = TrackingLock {
2358 released: released.clone(),
2359 };
2360
2361 let object_store = ObjectStore::memory();
2362 let base_path = Path::from("test");
2363 let mut manifest = test_manifest();
2364
2365 let commit_fut = lock.commit(
2368 &mut manifest,
2369 None,
2370 &base_path,
2371 &object_store,
2372 hanging_manifest_writer,
2373 ManifestNamingScheme::V2,
2374 None,
2375 );
2376 let timed_out = tokio::time::timeout(Duration::from_millis(50), commit_fut).await;
2377 assert!(timed_out.is_err(), "commit should not have completed");
2378
2379 for _ in 0..100 {
2381 if released.load(Ordering::SeqCst) {
2382 break;
2383 }
2384 tokio::time::sleep(Duration::from_millis(10)).await;
2385 }
2386 assert!(
2387 released.load(Ordering::SeqCst),
2388 "lock must be released after the commit future is cancelled"
2389 );
2390 }
2391
2392 #[tokio::test]
2396 async fn test_commit_lock_released_on_cancellation_during_release() {
2397 use std::sync::atomic::Ordering;
2398 use std::time::Duration;
2399
2400 let release_calls = Arc::new(AtomicUsize::new(0));
2401 let released = Arc::new(AtomicBool::new(false));
2402 let lock = HangingReleaseLock {
2403 release_calls: release_calls.clone(),
2404 released: released.clone(),
2405 };
2406
2407 let object_store = ObjectStore::memory();
2408 let base_path = Path::from("test");
2409 let mut manifest = test_manifest();
2410
2411 let commit_fut = lock.commit(
2414 &mut manifest,
2415 None,
2416 &base_path,
2417 &object_store,
2418 succeeding_manifest_writer,
2419 ManifestNamingScheme::V2,
2420 None,
2421 );
2422 let timed_out = tokio::time::timeout(Duration::from_millis(50), commit_fut).await;
2423 assert!(timed_out.is_err(), "commit should not have completed");
2424
2425 for _ in 0..100 {
2428 if released.load(Ordering::SeqCst) {
2429 break;
2430 }
2431 tokio::time::sleep(Duration::from_millis(10)).await;
2432 }
2433 assert!(
2434 released.load(Ordering::SeqCst),
2435 "lock must be released even when cancelled during the explicit release"
2436 );
2437 assert_eq!(
2438 release_calls.load(Ordering::SeqCst),
2439 2,
2440 "expected the hung explicit release plus one best-effort drop release"
2441 );
2442 }
2443}