lance_namespace_impls/dir/builder.rs
1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use std::collections::HashMap;
5use std::sync::Arc;
6
7use lance::session::Session;
8use lance_core::Result;
9use lance_core::utils::parse::str_to_bool;
10use lance_io::object_store::{ObjectStore, ObjectStoreParams, ObjectStoreRegistry};
11use lance_namespace::error::NamespaceError;
12use object_store::path::Path;
13use tokio::sync::OnceCell;
14
15use super::{DirectoryNamespace, OpsMetrics, manifest, manifest_feature_flags};
16use crate::context::DynamicContextProvider;
17use crate::credentials::{create_credential_vendor_for_location, has_credential_vendor_config};
18
19/// Builder for creating a DirectoryNamespace.
20///
21/// This builder provides a fluent API for configuring and establishing
22/// connections to directory-based Lance namespaces.
23///
24/// # Examples
25///
26/// ```no_run
27/// # use lance_namespace_impls::DirectoryNamespaceBuilder;
28/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
29/// // Create a local directory namespace
30/// let namespace = DirectoryNamespaceBuilder::new("/path/to/data")
31/// .build()
32/// .await?;
33/// # Ok(())
34/// # }
35/// ```
36///
37/// ```no_run
38/// # use lance_namespace_impls::DirectoryNamespaceBuilder;
39/// # use lance::session::Session;
40/// # use std::sync::Arc;
41/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
42/// // Create with custom storage options and session
43/// let session = Arc::new(Session::default());
44/// let namespace = DirectoryNamespaceBuilder::new("s3://bucket/path")
45/// .storage_option("region", "us-west-2")
46/// .storage_option("access_key_id", "key")
47/// .session(session)
48/// .build()
49/// .await?;
50/// # Ok(())
51/// # }
52/// ```
53#[derive(Clone)]
54pub struct DirectoryNamespaceBuilder {
55 root: String,
56 pub(super) storage_options: Option<HashMap<String, String>>,
57 session: Option<Arc<Session>>,
58 pub(super) manifest_enabled: bool,
59 pub(super) dir_listing_enabled: bool,
60 pub(super) inline_optimization_enabled: bool,
61 table_version_tracking_enabled: bool,
62 /// When true, enables migration mode where the namespace checks the manifest first
63 /// before falling back to directory listing for root-level tables. When false (default),
64 /// root-level tables use directory listing directly without checking the manifest,
65 /// avoiding extra object store calls.
66 dir_listing_to_manifest_migration_enabled: bool,
67 credential_vendor_properties: HashMap<String, String>,
68 context_provider: Option<Arc<dyn DynamicContextProvider>>,
69 commit_retries: Option<u32>,
70 /// When true, returns input storage options in describe_table/declare_table responses
71 /// when no credential vendor is configured. Useful for testing. Default: false.
72 vend_input_storage_options: bool,
73 /// When set, adds expires_at_millis to vended storage options. The value is calculated
74 /// as current_time_millis + this interval. This allows clients to know when to refresh
75 /// credentials by calling describe_table again. Only effective when vend_input_storage_options
76 /// is true.
77 vend_input_storage_options_refresh_interval_millis: Option<u64>,
78 /// When true, tracks operation metrics. Default: false.
79 ops_metrics_enabled: bool,
80}
81
82impl std::fmt::Debug for DirectoryNamespaceBuilder {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 f.debug_struct("DirectoryNamespaceBuilder")
85 .field("root", &self.root)
86 .field("storage_options", &self.storage_options)
87 .field("manifest_enabled", &self.manifest_enabled)
88 .field("dir_listing_enabled", &self.dir_listing_enabled)
89 .field(
90 "inline_optimization_enabled",
91 &self.inline_optimization_enabled,
92 )
93 .field(
94 "table_version_tracking_enabled",
95 &self.table_version_tracking_enabled,
96 )
97 .field(
98 "dir_listing_to_manifest_migration_enabled",
99 &self.dir_listing_to_manifest_migration_enabled,
100 )
101 .field(
102 "context_provider",
103 &self.context_provider.as_ref().map(|_| "Some(...)"),
104 )
105 .field(
106 "vend_input_storage_options",
107 &self.vend_input_storage_options,
108 )
109 .field(
110 "vend_input_storage_options_refresh_interval_millis",
111 &self.vend_input_storage_options_refresh_interval_millis,
112 )
113 .field("ops_metrics_enabled", &self.ops_metrics_enabled)
114 .finish()
115 }
116}
117
118impl DirectoryNamespaceBuilder {
119 /// Create a new DirectoryNamespaceBuilder with the specified root path.
120 ///
121 /// # Arguments
122 ///
123 /// * `root` - Root directory path (local path or cloud URI like s3://bucket/path)
124 pub fn new(root: impl Into<String>) -> Self {
125 Self {
126 root: root.into().trim_end_matches('/').to_string(),
127 storage_options: None,
128 session: None,
129 manifest_enabled: true,
130 dir_listing_enabled: true, // Default to enabled for backwards compatibility
131 inline_optimization_enabled: false,
132 table_version_tracking_enabled: false, // Default to disabled
133 dir_listing_to_manifest_migration_enabled: false, // Default to disabled
134 credential_vendor_properties: HashMap::new(),
135 context_provider: None,
136 commit_retries: None,
137 vend_input_storage_options: false,
138 vend_input_storage_options_refresh_interval_millis: None,
139 ops_metrics_enabled: false,
140 }
141 }
142
143 /// Enable or disable manifest-based listing.
144 ///
145 /// When enabled (default), the namespace uses a `__manifest` table to track tables.
146 /// When disabled, relies solely on directory scanning.
147 pub fn manifest_enabled(mut self, enabled: bool) -> Self {
148 self.manifest_enabled = enabled;
149 self
150 }
151
152 /// Enable or disable directory-based listing fallback.
153 ///
154 /// When enabled (default), falls back to directory scanning for tables not in the manifest.
155 /// When disabled, only consults the manifest table.
156 pub fn dir_listing_enabled(mut self, enabled: bool) -> Self {
157 self.dir_listing_enabled = enabled;
158 self
159 }
160
161 /// Enable or disable migration mode from directory listing to manifest.
162 ///
163 /// When enabled, root-level table operations check the manifest first before
164 /// falling back to directory listing. When disabled (default), root-level tables
165 /// use directory listing directly, avoiding extra object store calls.
166 /// Only relevant when both `manifest_enabled` and `dir_listing_enabled` are true.
167 pub fn dir_listing_to_manifest_migration_enabled(mut self, enabled: bool) -> Self {
168 self.dir_listing_to_manifest_migration_enabled = enabled;
169 self
170 }
171
172 /// Enable or disable replacement index maintenance for the __manifest table.
173 ///
174 /// When enabled, copy-on-write manifest rewrites build replacement indices for fast
175 /// reads. This is disabled by default so rewrites only replace data files.
176 pub fn inline_optimization_enabled(mut self, enabled: bool) -> Self {
177 self.inline_optimization_enabled = enabled;
178 self
179 }
180
181 /// Enable or disable table version tracking through the namespace.
182 ///
183 /// When enabled, `describe_table` returns `managed_versioning: true` to indicate
184 /// that commits should go through the namespace's table version APIs rather than
185 /// direct object store operations.
186 ///
187 /// When disabled (default), `managed_versioning` is not set.
188 pub fn table_version_tracking_enabled(mut self, enabled: bool) -> Self {
189 self.table_version_tracking_enabled = enabled;
190 self
191 }
192
193 /// Create a DirectoryNamespaceBuilder from properties HashMap.
194 ///
195 /// This method parses a properties map into builder configuration.
196 /// It expects:
197 /// - `root`: The root directory path (required)
198 /// - `manifest_enabled`: Enable manifest-based table tracking (optional, default: true)
199 /// - `dir_listing_enabled`: Enable directory listing for table discovery (optional, default: true)
200 /// - `inline_optimization_enabled`: Enable replacement indices on __manifest rewrites (optional, default: false)
201 /// - `storage.*`: Storage options (optional, prefix will be stripped)
202 ///
203 /// Credential vendor properties (prefixed with `credential_vendor.`, prefix is stripped):
204 /// - `credential_vendor.enabled`: Set to "true" to enable credential vending (required)
205 /// - `credential_vendor.permission`: Permission level: read, write, or admin (default: read)
206 ///
207 /// AWS-specific properties (for s3:// locations):
208 /// - `credential_vendor.aws_role_arn`: AWS IAM role ARN (required for AWS)
209 /// - `credential_vendor.aws_external_id`: AWS external ID (optional)
210 /// - `credential_vendor.aws_region`: AWS region (optional)
211 /// - `credential_vendor.aws_role_session_name`: AWS role session name (optional)
212 /// - `credential_vendor.aws_duration_millis`: Credential duration in ms (default: 3600000, range: 15min-12hrs)
213 ///
214 /// GCP-specific properties (for gs:// locations):
215 /// - `credential_vendor.gcp_service_account`: Service account to impersonate (optional)
216 /// - `credential_vendor.gcp_workload_identity_provider`: Workload Identity Provider for OIDC token exchange (optional)
217 /// - `credential_vendor.gcp_impersonation_service_account`: Service account to impersonate after workload identity exchange (optional)
218 ///
219 /// Note: GCP uses Application Default Credentials (ADC). To use a service account key file,
220 /// set the `GOOGLE_APPLICATION_CREDENTIALS` environment variable before starting.
221 /// GCP token duration cannot be configured; it's determined by the STS endpoint (typically 1 hour).
222 ///
223 /// Azure-specific properties (for az:// locations):
224 /// - `credential_vendor.azure_account_name`: Azure storage account name (required for Azure)
225 /// - `credential_vendor.azure_tenant_id`: Azure tenant ID (optional)
226 /// - `credential_vendor.azure_federated_client_id`: Client ID used for workload identity federation (optional)
227 /// - `credential_vendor.azure_duration_millis`: Credential duration in ms (default: 3600000, up to 7 days)
228 ///
229 /// # Arguments
230 ///
231 /// * `properties` - Configuration properties
232 /// * `session` - Optional Lance session to reuse object store registry
233 ///
234 /// # Returns
235 ///
236 /// Returns a `DirectoryNamespaceBuilder` instance.
237 ///
238 /// # Errors
239 ///
240 /// Returns an error if the `root` property is missing.
241 ///
242 /// # Examples
243 ///
244 /// ```no_run
245 /// # use lance_namespace_impls::DirectoryNamespaceBuilder;
246 /// # use std::collections::HashMap;
247 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
248 /// let mut properties = HashMap::new();
249 /// properties.insert("root".to_string(), "/path/to/data".to_string());
250 /// properties.insert("manifest_enabled".to_string(), "true".to_string());
251 /// properties.insert("dir_listing_enabled".to_string(), "false".to_string());
252 /// properties.insert("storage.region".to_string(), "us-west-2".to_string());
253 ///
254 /// let namespace = DirectoryNamespaceBuilder::from_properties(properties, None)?
255 /// .build()
256 /// .await?;
257 /// # Ok(())
258 /// # }
259 /// ```
260 pub fn from_properties(
261 properties: HashMap<String, String>,
262 session: Option<Arc<Session>>,
263 ) -> Result<Self> {
264 // Extract root from properties (required)
265 let root = properties.get("root").cloned().ok_or_else(|| {
266 lance_core::Error::from(NamespaceError::InvalidInput {
267 message: "Missing required property 'root' for directory namespace".to_string(),
268 })
269 })?;
270
271 // Extract storage options (properties prefixed with "storage.")
272 let storage_options: HashMap<String, String> = properties
273 .iter()
274 .filter_map(|(k, v)| {
275 k.strip_prefix("storage.")
276 .map(|key| (key.to_string(), v.clone()))
277 })
278 .collect();
279
280 let storage_options = if storage_options.is_empty() {
281 None
282 } else {
283 Some(storage_options)
284 };
285
286 // Extract manifest_enabled (default: true)
287 let manifest_enabled = properties
288 .get("manifest_enabled")
289 .and_then(|v| str_to_bool(v))
290 .unwrap_or(true);
291
292 // Extract dir_listing_enabled (default: true)
293 let dir_listing_enabled = properties
294 .get("dir_listing_enabled")
295 .and_then(|v| str_to_bool(v))
296 .unwrap_or(true);
297
298 // Extract inline_optimization_enabled (default: false)
299 let inline_optimization_enabled = properties
300 .get("inline_optimization_enabled")
301 .and_then(|v| str_to_bool(v))
302 .unwrap_or(false);
303
304 // Extract table_version_tracking_enabled (default: false)
305 let table_version_tracking_enabled = properties
306 .get("table_version_tracking_enabled")
307 .and_then(|v| str_to_bool(v))
308 .unwrap_or(false);
309
310 // Extract dir_listing_to_manifest_migration_enabled (default: false)
311 let dir_listing_to_manifest_migration_enabled = properties
312 .get("dir_listing_to_manifest_migration_enabled")
313 .and_then(|v| str_to_bool(v))
314 .unwrap_or(false);
315
316 // Extract credential vendor properties (properties prefixed with "credential_vendor.")
317 // The prefix is stripped to get short property names
318 // The build() method will check if enabled=true before creating the vendor
319 let credential_vendor_properties: HashMap<String, String> = properties
320 .iter()
321 .filter_map(|(k, v)| {
322 k.strip_prefix("credential_vendor.")
323 .map(|key| (key.to_string(), v.clone()))
324 })
325 .collect();
326
327 let commit_retries = properties
328 .get("commit_retries")
329 .and_then(|v| v.parse::<u32>().ok());
330
331 // Extract vend_input_storage_options (default: false)
332 let vend_input_storage_options = properties
333 .get("vend_input_storage_options")
334 .and_then(|v| str_to_bool(v))
335 .unwrap_or(false);
336
337 // Extract vend_input_storage_options_refresh_interval_millis (optional)
338 let vend_input_storage_options_refresh_interval_millis = properties
339 .get("vend_input_storage_options_refresh_interval_millis")
340 .and_then(|v| v.parse::<u64>().ok());
341
342 // Extract ops_metrics_enabled (default: false)
343 let ops_metrics_enabled = properties
344 .get("ops_metrics_enabled")
345 .and_then(|v| str_to_bool(v))
346 .unwrap_or(false);
347
348 Ok(Self {
349 root: root.trim_end_matches('/').to_string(),
350 storage_options,
351 session,
352 manifest_enabled,
353 dir_listing_enabled,
354 inline_optimization_enabled,
355 table_version_tracking_enabled,
356 dir_listing_to_manifest_migration_enabled,
357 credential_vendor_properties,
358 context_provider: None,
359 commit_retries,
360 vend_input_storage_options,
361 vend_input_storage_options_refresh_interval_millis,
362 ops_metrics_enabled,
363 })
364 }
365
366 /// Add a storage option.
367 ///
368 /// # Arguments
369 ///
370 /// * `key` - Storage option key (e.g., "region", "access_key_id")
371 /// * `value` - Storage option value
372 pub fn storage_option(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
373 self.storage_options
374 .get_or_insert_with(HashMap::new)
375 .insert(key.into(), value.into());
376 self
377 }
378
379 /// Add multiple storage options.
380 ///
381 /// # Arguments
382 ///
383 /// * `options` - HashMap of storage options to add
384 pub fn storage_options(mut self, options: HashMap<String, String>) -> Self {
385 self.storage_options
386 .get_or_insert_with(HashMap::new)
387 .extend(options);
388 self
389 }
390
391 /// Set the Lance session to use for this namespace.
392 ///
393 /// When a session is provided, the namespace will reuse the session's
394 /// object store registry, allowing multiple namespaces and datasets
395 /// to share the same underlying storage connections.
396 ///
397 /// # Arguments
398 ///
399 /// * `session` - Arc-wrapped Lance session
400 pub fn session(mut self, session: Arc<Session>) -> Self {
401 self.session = Some(session);
402 self
403 }
404
405 /// Set the number of retries for commit operations on the manifest table.
406 /// If not set, defaults to [`lance_table::io::commit::CommitConfig`] default (20).
407 pub fn commit_retries(mut self, retries: u32) -> Self {
408 self.commit_retries = Some(retries);
409 self
410 }
411
412 /// Add a credential vendor property.
413 ///
414 /// Use short property names without the `credential_vendor.` prefix.
415 /// Common properties: `enabled`, `permission`.
416 /// AWS properties: `aws_role_arn`, `aws_external_id`, `aws_region`, `aws_role_session_name`, `aws_duration_millis`.
417 /// GCP properties: `gcp_service_account`, `gcp_workload_identity_provider`, `gcp_impersonation_service_account`.
418 /// Azure properties: `azure_account_name`, `azure_tenant_id`, `azure_federated_client_id`, `azure_duration_millis`.
419 ///
420 /// # Arguments
421 ///
422 /// * `key` - Property key (e.g., "enabled", "aws_role_arn")
423 /// * `value` - Property value
424 ///
425 /// # Example
426 ///
427 /// ```no_run
428 /// # use lance_namespace_impls::DirectoryNamespaceBuilder;
429 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
430 /// let namespace = DirectoryNamespaceBuilder::new("s3://my-bucket/data")
431 /// .credential_vendor_property("enabled", "true")
432 /// .credential_vendor_property("aws_role_arn", "arn:aws:iam::123456789012:role/MyRole")
433 /// .credential_vendor_property("permission", "read")
434 /// .build()
435 /// .await?;
436 /// # Ok(())
437 /// # }
438 /// ```
439 pub fn credential_vendor_property(
440 mut self,
441 key: impl Into<String>,
442 value: impl Into<String>,
443 ) -> Self {
444 self.credential_vendor_properties
445 .insert(key.into(), value.into());
446 self
447 }
448
449 /// Add multiple credential vendor properties.
450 ///
451 /// Use short property names without the `credential_vendor.` prefix.
452 ///
453 /// # Arguments
454 ///
455 /// * `properties` - HashMap of credential vendor properties to add
456 pub fn credential_vendor_properties(mut self, properties: HashMap<String, String>) -> Self {
457 self.credential_vendor_properties.extend(properties);
458 self
459 }
460
461 /// Set a dynamic context provider for per-request context.
462 ///
463 /// The provider can be used to generate additional context for operations.
464 /// For DirectoryNamespace, the context is stored but not directly used
465 /// in operations (unlike RestNamespace where it's converted to HTTP headers).
466 ///
467 /// # Arguments
468 ///
469 /// * `provider` - The context provider implementation
470 pub fn context_provider(mut self, provider: Arc<dyn DynamicContextProvider>) -> Self {
471 self.context_provider = Some(provider);
472 self
473 }
474
475 /// Enable or disable returning input storage options in responses.
476 ///
477 /// When enabled, `describe_table` and `declare_table` will return the storage
478 /// options passed to the builder when no credential vendor is configured.
479 /// This is useful for testing scenarios where you want to pass storage options
480 /// through to clients.
481 ///
482 /// Default is false (storage options are not returned unless credential vending is configured).
483 pub fn vend_input_storage_options(mut self, enabled: bool) -> Self {
484 self.vend_input_storage_options = enabled;
485 self
486 }
487
488 /// Set the refresh interval for vended input storage options.
489 ///
490 /// When set, vended storage options will include an `expires_at_millis` field
491 /// calculated as `current_time_millis + interval_millis`. This allows clients
492 /// to know when to refresh credentials by calling `describe_table` again.
493 ///
494 /// This only has effect when `vend_input_storage_options` is enabled.
495 ///
496 /// # Arguments
497 ///
498 /// * `interval_millis` - The refresh interval in milliseconds
499 pub fn vend_input_storage_options_refresh_interval_millis(
500 mut self,
501 interval_millis: u64,
502 ) -> Self {
503 self.vend_input_storage_options_refresh_interval_millis = Some(interval_millis);
504 self
505 }
506
507 /// Enable or disable operation metrics tracking.
508 ///
509 /// When enabled, the namespace will track how many times each API operation
510 /// is called. Use `retrieve_ops_metrics()` on the built namespace to get
511 /// the current counts.
512 ///
513 /// Default is false.
514 pub fn ops_metrics_enabled(mut self, enabled: bool) -> Self {
515 self.ops_metrics_enabled = enabled;
516 self
517 }
518
519 /// Build the DirectoryNamespace.
520 ///
521 /// # Returns
522 ///
523 /// Returns a `DirectoryNamespace` instance.
524 ///
525 /// # Errors
526 ///
527 /// Returns an error if:
528 /// - The root path is invalid
529 /// - Connection to the storage backend fails
530 /// - Storage options are invalid
531 pub async fn build(self) -> Result<DirectoryNamespace> {
532 let (object_store, base_path) =
533 Self::initialize_object_store(&self.root, &self.storage_options, &self.session).await?;
534
535 let manifest_ns = if self.manifest_enabled {
536 match manifest::ManifestNamespace::open_from_directory(
537 self.root.clone(),
538 self.storage_options.clone(),
539 self.session.clone(),
540 object_store.clone(),
541 base_path.clone(),
542 self.dir_listing_enabled,
543 self.inline_optimization_enabled,
544 self.commit_retries,
545 )
546 .await
547 {
548 Ok(ns) => Some(Arc::new(ns)),
549 Err(e) if manifest_feature_flags::is_incompatible_manifest_error(&e) => {
550 // The manifest exists but was written with a feature flag this
551 // build does not understand. Refuse rather than silently
552 // degrading to a directory-listing view that ignores it.
553 return Err(e);
554 }
555 Err(e) if manifest::ManifestNamespace::is_not_found_load_error(&e) => {
556 log::debug!("Manifest namespace does not exist yet: {}", e);
557 None
558 }
559 Err(e) => return Err(e),
560 }
561 } else {
562 None
563 };
564 let manifest_cell = OnceCell::new();
565 if let Some(manifest_ns) = manifest_ns {
566 let _ = manifest_cell.set(manifest_ns);
567 }
568
569 // Create credential vendor once during initialization if enabled
570 let credential_vendor = if has_credential_vendor_config(&self.credential_vendor_properties)
571 {
572 create_credential_vendor_for_location(&self.root, &self.credential_vendor_properties)
573 .await?
574 .map(Arc::from)
575 } else {
576 None
577 };
578
579 let ops_metrics = if self.ops_metrics_enabled {
580 Some(Arc::new(OpsMetrics::default()))
581 } else {
582 None
583 };
584
585 Ok(DirectoryNamespace {
586 root: self.root,
587 storage_options: self.storage_options,
588 session: self.session,
589 object_store,
590 base_path,
591 manifest_ns: manifest_cell,
592 write_manifest_ns: OnceCell::new(),
593 manifest_enabled: self.manifest_enabled,
594 dir_listing_enabled: self.dir_listing_enabled,
595 inline_optimization_enabled: self.inline_optimization_enabled,
596 commit_retries: self.commit_retries,
597 dir_listing_to_manifest_migration_enabled: self
598 .dir_listing_to_manifest_migration_enabled,
599 table_version_tracking_enabled: self.table_version_tracking_enabled,
600 credential_vendor,
601 context_provider: self.context_provider,
602 vend_input_storage_options: self.vend_input_storage_options,
603 vend_input_storage_options_refresh_interval_millis: self
604 .vend_input_storage_options_refresh_interval_millis,
605 ops_metrics,
606 })
607 }
608
609 /// Initialize the Lance ObjectStore based on the configuration
610 async fn initialize_object_store(
611 root: &str,
612 storage_options: &Option<HashMap<String, String>>,
613 session: &Option<Arc<Session>>,
614 ) -> Result<(Arc<ObjectStore>, Path)> {
615 // Build ObjectStoreParams from storage options
616 let accessor = storage_options.clone().map(|opts| {
617 Arc::new(lance_io::object_store::StorageOptionsAccessor::with_static_options(opts))
618 });
619 let params = ObjectStoreParams {
620 storage_options_accessor: accessor,
621 ..Default::default()
622 };
623
624 // Use object store registry from session if provided, otherwise create a new one
625 let registry = if let Some(session) = session {
626 session.store_registry()
627 } else {
628 Arc::new(ObjectStoreRegistry::default())
629 };
630
631 // Use Lance's object store factory to create from URI
632 let (object_store, base_path) = ObjectStore::from_uri_and_params(registry, root, ¶ms)
633 .await
634 .map_err(|e| {
635 lance_core::Error::from(NamespaceError::Internal {
636 message: format!("Failed to create object store: {:?}", e),
637 })
638 })?;
639
640 Ok((object_store, base_path))
641 }
642}