Skip to main content

lance_namespace_impls/dir/
load.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use lance::dataset::Dataset;
5use lance::dataset::builder::DatasetBuilder;
6use lance_core::Result;
7use lance_namespace::LanceNamespace;
8use lance_namespace::error::NamespaceError;
9use lance_namespace::models::{DescribeTableRequest, DescribeTableResponse};
10use lance_namespace::schema::arrow_schema_to_json;
11
12use super::{DirectoryNamespace, manifest, manifest_feature_flags};
13
14impl DirectoryNamespace {
15    /// Internal describe_table implementation that doesn't record metrics.
16    /// Used by both the public describe_table (which records metrics) and
17    /// internal callers like resolve_table_location (which shouldn't).
18    pub(super) async fn describe_table_impl(
19        &self,
20        request: DescribeTableRequest,
21    ) -> Result<DescribeTableResponse> {
22        let is_root_level = request.id.as_ref().is_some_and(|id| id.len() == 1);
23        let is_child_table = request.id.as_ref().is_some_and(|id| id.len() > 1);
24        let skip_manifest_for_root = self.dir_listing_enabled
25            && is_root_level
26            && !self.dir_listing_to_manifest_migration_enabled;
27        // Self-heal the manifest wherever it can be authoritative: a child table
28        // (no dir-listing fallback), a manifest-only namespace, or migration mode
29        // (manifest-first -- it can hold registered_table -> external .lance
30        // aliases that dir-listing cannot resolve, so a reader built before
31        // __manifest must re-probe to see them). The bypass -- skipping the probe
32        // -- applies ONLY to migration-disabled directory-backed root reads,
33        // which are served entirely from the directory listing.
34        if is_child_table
35            || !self.dir_listing_enabled
36            || self.dir_listing_to_manifest_migration_enabled
37        {
38            self.ensure_read_manifest().await?;
39        }
40        if let Some(manifest_ns) = self.manifest_ns_for_read()
41            && !skip_manifest_for_root
42        {
43            match manifest_ns.describe_table(request.clone()).await {
44                Ok(mut response) => {
45                    if let Some(ref table_uri) = response.table_uri {
46                        // For backwards compatibility, only skip vending credentials when explicitly set to false
47                        let vend = request.vend_credentials.unwrap_or(true);
48                        let identity = request.identity.as_deref();
49                        response.storage_options = self
50                            .get_storage_options_for_table(table_uri, vend, identity)
51                            .await?;
52                    }
53                    // Set managed_versioning flag when table_version_tracking_enabled
54                    if self.table_version_tracking_enabled {
55                        response.managed_versioning = Some(true);
56                    }
57                    return Ok(response);
58                }
59                Err(e) if manifest_feature_flags::is_incompatible_manifest_error(&e) => {
60                    // An incompatible manifest must surface "please upgrade"
61                    // rather than degrading to a directory-listing view.
62                    return Err(e);
63                }
64                Err(e) if self.dir_listing_enabled && is_root_level => {
65                    // Only a genuinely-absent table (e.g. an unmigrated on-disk
66                    // table) may fall through to the directory check; any other
67                    // manifest error must propagate rather than be read as missing.
68                    if !Self::is_manifest_table_absent_error(&e) {
69                        return Err(Self::classify_storage_error(e));
70                    }
71                }
72                Err(e) => return Err(e),
73            }
74        }
75        if is_child_table {
76            return Err(self.child_namespace_requires_manifest_error());
77        }
78
79        let table_name = Self::table_name_from_id(&request.id)?;
80        let table_id = Self::format_table_id_from_request(&request.id);
81        if !self.dir_listing_enabled {
82            return Err(NamespaceError::TableNotFound { message: table_id }.into());
83        }
84
85        let table_uri = self.table_full_uri(&table_name);
86
87        // Atomically check table existence and deregistration status
88        let status = self.check_table_status(&table_name).await?;
89
90        if !status.exists {
91            return Err(NamespaceError::TableNotFound {
92                message: table_id.clone(),
93            }
94            .into());
95        }
96
97        if status.is_deregistered {
98            return Err(NamespaceError::TableNotFound {
99                message: format!("Table is deregistered: {}", table_id),
100            }
101            .into());
102        }
103
104        let load_detailed_metadata = request.load_detailed_metadata.unwrap_or(false);
105        let should_check_declared =
106            load_detailed_metadata || request.check_declared.unwrap_or(false);
107        // For backwards compatibility, only skip vending credentials when explicitly set to false
108        let vend_credentials = request.vend_credentials.unwrap_or(true);
109        let identity = request.identity.as_deref();
110        let is_only_declared = if should_check_declared {
111            if status.has_reserved_file {
112                Some(!self.table_has_actual_manifests(&table_name).await?)
113            } else {
114                Some(false)
115            }
116        } else {
117            None
118        };
119
120        if !load_detailed_metadata {
121            let storage_options = self
122                .get_storage_options_for_table(&table_uri, vend_credentials, identity)
123                .await?;
124            return Ok(DescribeTableResponse {
125                table: Some(table_name),
126                namespace: request.id.as_ref().map(|id| {
127                    if id.len() > 1 {
128                        id[..id.len() - 1].to_vec()
129                    } else {
130                        vec![]
131                    }
132                }),
133                location: Some(table_uri.clone()),
134                table_uri: Some(table_uri),
135                storage_options,
136                is_only_declared,
137                managed_versioning: if self.table_version_tracking_enabled {
138                    Some(true)
139                } else {
140                    None
141                },
142                ..Default::default()
143            });
144        }
145
146        if is_only_declared == Some(true) {
147            let storage_options = self
148                .get_storage_options_for_table(&table_uri, vend_credentials, identity)
149                .await?;
150            return Ok(DescribeTableResponse {
151                table: Some(table_name),
152                namespace: request.id.as_ref().map(|id| {
153                    if id.len() > 1 {
154                        id[..id.len() - 1].to_vec()
155                    } else {
156                        vec![]
157                    }
158                }),
159                location: Some(table_uri.clone()),
160                table_uri: Some(table_uri),
161                storage_options,
162                is_only_declared,
163                managed_versioning: if self.table_version_tracking_enabled {
164                    Some(true)
165                } else {
166                    None
167                },
168                ..Default::default()
169            });
170        }
171
172        // Try to load the dataset to get real information
173        // Use DatasetBuilder with storage options to support S3 with custom endpoints
174        let mut builder = DatasetBuilder::from_uri(&table_uri);
175        if let Some(opts) = &self.storage_options {
176            builder = builder.with_storage_options(opts.clone());
177        }
178        if let Some(sess) = &self.session {
179            builder = builder.with_session(sess.clone());
180        }
181        match builder.load().await {
182            Ok(mut dataset) => {
183                // If a specific version is requested, checkout that version
184                if let Some(requested_version) = request.version {
185                    dataset = dataset
186                        .checkout_version(requested_version as u64)
187                        .await
188                        .map_err(|e| {
189                            let message = format!(
190                                "Version {} not found for table '{}': {}",
191                                requested_version, table_name, e
192                            );
193                            Self::map_open_error(
194                                e,
195                                NamespaceError::TableVersionNotFound { message },
196                            )
197                        })?;
198                }
199
200                let version_info = dataset.version();
201                let lance_schema = dataset.schema();
202                let arrow_schema: arrow_schema::Schema = lance_schema.into();
203                let json_schema = arrow_schema_to_json(&arrow_schema)?;
204                let storage_options = self
205                    .get_storage_options_for_table(&table_uri, vend_credentials, identity)
206                    .await?;
207
208                // Convert BTreeMap to HashMap for the response
209                let metadata: std::collections::HashMap<String, String> =
210                    version_info.metadata.into_iter().collect();
211
212                Ok(DescribeTableResponse {
213                    table: Some(table_name),
214                    namespace: request.id.as_ref().map(|id| {
215                        if id.len() > 1 {
216                            id[..id.len() - 1].to_vec()
217                        } else {
218                            vec![]
219                        }
220                    }),
221                    version: Some(version_info.version as i64),
222                    location: Some(table_uri.clone()),
223                    table_uri: Some(table_uri),
224                    schema: Some(Box::new(json_schema)),
225                    storage_options,
226                    metadata: Some(metadata),
227                    is_only_declared,
228                    managed_versioning: if self.table_version_tracking_enabled {
229                        Some(true)
230                    } else {
231                        None
232                    },
233                    ..Default::default()
234                })
235            }
236            Err(err) => {
237                if manifest::ManifestNamespace::is_not_found_load_error(&err)
238                    && is_only_declared == Some(true)
239                {
240                    let storage_options = self
241                        .get_storage_options_for_table(&table_uri, vend_credentials, identity)
242                        .await?;
243                    Ok(DescribeTableResponse {
244                        table: Some(table_name),
245                        namespace: request.id.as_ref().map(|id| {
246                            if id.len() > 1 {
247                                id[..id.len() - 1].to_vec()
248                            } else {
249                                vec![]
250                            }
251                        }),
252                        location: Some(table_uri.clone()),
253                        table_uri: Some(table_uri),
254                        storage_options,
255                        is_only_declared,
256                        managed_versioning: if self.table_version_tracking_enabled {
257                            Some(true)
258                        } else {
259                            None
260                        },
261                        ..Default::default()
262                    })
263                } else {
264                    Err(NamespaceError::Internal {
265                        message: format!(
266                            "Table directory exists but cannot load dataset {}: {:?}",
267                            table_name, err
268                        ),
269                    }
270                    .into())
271                }
272            }
273        }
274    }
275
276    /// Build a `DatasetBuilder` for `table_uri` with this namespace's storage
277    /// options and session applied. Callers add version/branch scoping.
278    pub(super) fn configured_builder(&self, table_uri: &str) -> DatasetBuilder {
279        let mut builder = DatasetBuilder::from_uri(table_uri);
280        if let Some(opts) = &self.storage_options {
281            builder = builder.with_storage_options(opts.clone());
282        }
283        if let Some(sess) = &self.session {
284            builder = builder.with_session(sess.clone());
285        }
286        builder
287    }
288
289    pub(super) async fn load_dataset(
290        &self,
291        table_uri: &str,
292        version: Option<i64>,
293        operation: &str,
294    ) -> Result<Dataset> {
295        self.load_branch_dataset(table_uri, None, version, operation)
296            .await
297    }
298
299    /// Like [`Self::load_dataset`], on `branch` when it names one.
300    pub(super) async fn load_branch_dataset(
301        &self,
302        table_uri: &str,
303        branch: Option<&str>,
304        version: Option<i64>,
305        operation: &str,
306    ) -> Result<Dataset> {
307        if let Some(version) = version
308            && version < 0
309        {
310            return Err(NamespaceError::InvalidInput {
311                message: format!(
312                    "Table version for {} must be non-negative, got {}",
313                    operation, version
314                ),
315            }
316            .into());
317        }
318
319        let dataset = match Self::normalized_branch(branch)? {
320            Some(branch) => self.open_validated_branch(table_uri, branch).await?,
321            None => self
322                .configured_builder(table_uri)
323                .load()
324                .await
325                .map_err(|e| {
326                    let message = format!(
327                        "Failed to open table at '{}' for {}: {}",
328                        table_uri, operation, e
329                    );
330                    Self::map_open_error(e, NamespaceError::TableNotFound { message })
331                })?,
332        };
333
334        if let Some(version) = version {
335            return dataset.checkout_version(version as u64).await.map_err(|e| {
336                let message = format!(
337                    "Failed to checkout version {} for table at '{}' during {}: {}",
338                    version, table_uri, operation, e
339                );
340                Self::map_open_error(e, NamespaceError::TableVersionNotFound { message })
341            });
342        }
343
344        Ok(dataset)
345    }
346
347    /// Migrate directory-based tables to the manifest.
348    ///
349    /// This is a one-time migration operation that:
350    /// 1. Scans the directory for existing `.lance` tables
351    /// 2. Registers any unmigrated tables in the manifest
352    /// 3. Returns the count of tables that were migrated
353    ///
354    /// This method is safe to run multiple times - it will skip tables that are already
355    /// registered in the manifest.
356    ///
357    /// # Usage
358    ///
359    /// After creating tables in directory-only mode or dual mode, you can migrate them
360    /// to the manifest to enable manifest-only mode:
361    ///
362    /// ```no_run
363    /// #![recursion_limit = "256"]
364    /// # use lance_namespace_impls::DirectoryNamespaceBuilder;
365    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
366    /// // Create namespace with dual mode (manifest + directory listing)
367    /// let namespace = DirectoryNamespaceBuilder::new("/path/to/data")
368    ///     .manifest_enabled(true)
369    ///     .dir_listing_enabled(true)
370    ///     .build()
371    ///     .await?;
372    ///
373    /// // ... tables are created and used ...
374    ///
375    /// // Migrate existing directory tables to manifest
376    /// let migrated_count = namespace.migrate().await?;
377    /// println!("Migrated {} tables", migrated_count);
378    ///
379    /// // Now you can disable directory listing for better performance:
380    /// // (requires rebuilding the namespace)
381    /// let namespace = DirectoryNamespaceBuilder::new("/path/to/data")
382    ///     .manifest_enabled(true)
383    ///     .dir_listing_enabled(false)  // All tables now in manifest
384    ///     .build()
385    ///     .await?;
386    /// # Ok(())
387    /// # }
388    /// ```
389    ///
390    /// # Returns
391    ///
392    /// Returns the number of tables that were migrated to the manifest.
393    ///
394    /// # Errors
395    ///
396    /// Returns an error if:
397    /// - Manifest is not enabled
398    /// - Directory listing fails
399    /// - Manifest registration fails
400    pub async fn migrate(&self) -> Result<usize> {
401        // We only care about tables in the root namespace
402        let Some(manifest_ns) = self.manifest_ns_for_write().await? else {
403            return Ok(0); // No manifest, nothing to migrate
404        };
405
406        // Get all table locations already in the manifest
407        let manifest_locations = manifest_ns.list_manifest_table_locations().await?;
408
409        // Get all tables from directory and skip declared-only tables that have not
410        // written any actual version manifests yet.
411        let dir_tables = self
412            .filter_declared_tables(self.list_directory_tables().await?, false)
413            .await?;
414
415        // Register each directory table that doesn't have an overlapping location
416        // If a directory name already exists in the manifest,
417        // that means the table must have already been migrated or created
418        // in the manifest, so we can skip it.
419        let mut migrated_count = 0;
420        for table_name in dir_tables {
421            // For root namespace tables, the directory name is "table_name.lance"
422            let dir_name = format!("{}.lance", table_name);
423            if !manifest_locations.contains(&dir_name) {
424                manifest_ns.register_table(&table_name, dir_name).await?;
425                migrated_count += 1;
426            }
427        }
428
429        Ok(migrated_count)
430    }
431}