1pub mod constants;
7mod credentials;
8pub mod errors;
9pub mod logstore;
10pub mod storage;
11
12use aws_config::Region;
13use aws_config::SdkConfig;
14pub use aws_credential_types::provider::SharedCredentialsProvider;
15use aws_sdk_dynamodb::error::SdkError;
16use aws_sdk_dynamodb::{
17 Client,
18 operation::{
19 create_table::CreateTableError, delete_item::DeleteItemError, get_item::GetItemError,
20 put_item::PutItemError, query::QueryError, update_item::UpdateItemError,
21 },
22 types::{
23 AttributeDefinition, AttributeValue, BillingMode, KeySchemaElement, KeyType,
24 ScalarAttributeType,
25 },
26};
27use deltalake_core::kernel::Version;
28use deltalake_core::logstore::{
29 LogStore, LogStoreFactory, ObjectStoreRef, StorageConfig, default_logstore, logstore_factories,
30 object_store_factories,
31};
32use deltalake_core::{DeltaResult, Path};
33use errors::{DynamoDbConfigError, LockClientError};
34use regex::Regex;
35use std::{
36 collections::HashMap,
37 str::FromStr,
38 sync::{Arc, LazyLock},
39 time::{Duration, SystemTime},
40};
41use storage::S3StorageOptionsConversion;
42use storage::{S3ObjectStoreFactory, S3StorageOptions};
43use tracing::log::*;
44use typed_builder::TypedBuilder;
45use url::Url;
46
47#[derive(Clone, Debug, Default)]
48pub struct S3LogStoreFactory {}
49
50impl S3StorageOptionsConversion for S3LogStoreFactory {}
51
52impl LogStoreFactory for S3LogStoreFactory {
53 fn with_options(
54 &self,
55 prefixed_store: ObjectStoreRef,
56 root_store: ObjectStoreRef,
57 location: &Url,
58 options: &StorageConfig,
59 ) -> DeltaResult<Arc<dyn LogStore>> {
60 let s3_options = self.with_env_s3(&options.raw.clone());
61 let s3_options = S3StorageOptions::from_map(&s3_options)?;
62 if s3_options.locking_provider.as_deref() == Some("dynamodb") {
63 debug!(
64 "S3LogStoreFactory has been asked to create a LogStore with the dynamodb locking provider"
65 );
66 return Ok(Arc::new(logstore::S3DynamoDbLogStore::try_new(
67 location.clone(),
68 options,
69 &s3_options,
70 prefixed_store,
71 root_store,
72 )?));
73 }
74 Ok(default_logstore(
75 prefixed_store,
76 root_store,
77 location,
78 options,
79 ))
80 }
81}
82
83pub fn register_handlers(_additional_prefixes: Option<Url>) {
87 let object_stores = Arc::new(S3ObjectStoreFactory::default());
88 let log_stores = Arc::new(S3LogStoreFactory::default());
89 for scheme in ["s3", "s3a"].iter() {
90 let url = Url::parse(&format!("{scheme}://")).unwrap();
91 object_store_factories().insert(url.clone(), object_stores.clone());
92 logstore_factories().insert(url.clone(), log_stores.clone());
93 }
94}
95
96#[derive(Debug, PartialEq, TypedBuilder)]
104#[builder(doc)]
105pub struct CommitEntry {
106 pub version: Version,
108 #[builder(setter(into))]
110 pub temp_path: Path,
111 #[builder(default = false)]
113 pub complete: bool,
114 #[builder(default, setter(strip_option))]
116 pub expire_time: Option<SystemTime>,
117}
118
119#[derive(TypedBuilder)]
121#[builder(doc)]
122pub struct DynamoDbLockClient {
123 dynamodb_client: Client,
125 config: DynamoDbConfig,
127}
128
129#[cfg(test)]
130impl Default for DynamoDbLockClient {
131 fn default() -> Self {
132 let sdk_config = aws_config::SdkConfig::builder().build();
133 Self::try_new(&sdk_config, None, None, None, None, None, None, None, None)
134 .expect("Failed to create a default DynamoDbLockClient for testing purpose")
135 }
136}
137
138impl std::fmt::Debug for DynamoDbLockClient {
139 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
140 write!(f, "DynamoDbLockClient(config: {:?})", self.config)
141 }
142}
143
144impl DynamoDbLockClient {
145 #[allow(clippy::too_many_arguments)]
147 pub fn try_new(
148 sdk_config: &SdkConfig,
149 lock_table_name: Option<String>,
150 billing_mode: Option<String>,
151 max_elapsed_request_time: Option<String>,
152 dynamodb_override_endpoint: Option<String>,
153 dynamodb_override_region: Option<String>,
154 dynamodb_override_access_key_id: Option<String>,
155 dynamodb_override_secret_access_key: Option<String>,
156 dynamodb_override_session_token: Option<String>,
157 ) -> Result<Self, DynamoDbConfigError> {
158 let dynamodb_sdk_config = Self::create_dynamodb_sdk_config(
159 sdk_config,
160 dynamodb_override_endpoint,
161 dynamodb_override_region,
162 dynamodb_override_access_key_id,
163 dynamodb_override_secret_access_key,
164 dynamodb_override_session_token,
165 );
166
167 let dynamodb_client = aws_sdk_dynamodb::Client::new(&dynamodb_sdk_config);
168
169 let lock_table_name = lock_table_name
170 .or_else(|| std::env::var(constants::LOCK_TABLE_KEY_NAME).ok())
171 .unwrap_or(constants::DEFAULT_LOCK_TABLE_NAME.to_owned());
172
173 let billing_mode = if let Some(bm) = billing_mode
174 .or_else(|| std::env::var(constants::BILLING_MODE_KEY_NAME).ok())
175 .as_ref()
176 {
177 BillingMode::try_parse(bm.to_ascii_uppercase().as_str())
178 .map_err(|_| DynamoDbConfigError::InvalidBillingMode(String::default()))?
179 } else {
180 BillingMode::PayPerRequest
181 };
182
183 let max_elapsed_request_time = max_elapsed_request_time
184 .or_else(|| std::env::var(constants::MAX_ELAPSED_REQUEST_TIME_KEY_NAME).ok())
185 .map_or_else(
186 || Ok(Duration::from_secs(60)),
187 |secs| u64::from_str(&secs).map(Duration::from_secs),
188 )
189 .map_err(|err| DynamoDbConfigError::ParseMaxElapsedRequestTime { source: err })?;
190
191 let config = DynamoDbConfig::builder()
192 .billing_mode(billing_mode)
193 .lock_table_name(lock_table_name)
194 .max_elapsed_request_time(max_elapsed_request_time)
195 .sdk_config(sdk_config.clone())
196 .build();
197 Ok(Self::builder()
198 .dynamodb_client(dynamodb_client)
199 .config(config)
200 .build())
201 }
202 fn create_dynamodb_sdk_config(
203 sdk_config: &SdkConfig,
204 dynamodb_override_endpoint: Option<String>,
205 dynamodb_override_region: Option<String>,
206 dynamodb_override_access_key_id: Option<String>,
207 dynamodb_override_secret_access_key: Option<String>,
208 dynamodb_override_session_token: Option<String>,
209 ) -> SdkConfig {
210 let mut config_builder = sdk_config.to_owned().to_builder();
222
223 if let Some(dynamodb_endpoint_url) = dynamodb_override_endpoint {
224 config_builder = config_builder.endpoint_url(dynamodb_endpoint_url);
225 }
226
227 if let Some(dynamodb_region) = dynamodb_override_region {
228 config_builder = config_builder.region(Region::new(dynamodb_region));
229 }
230
231 if let (Some(access_key_id), Some(secret_access_key)) = (
232 dynamodb_override_access_key_id,
233 dynamodb_override_secret_access_key,
234 ) {
235 config_builder = config_builder.credentials_provider(SharedCredentialsProvider::new(
236 aws_credential_types::Credentials::from_keys(
237 access_key_id,
238 secret_access_key,
239 dynamodb_override_session_token,
240 ),
241 ));
242 }
243 config_builder.build()
244 }
245
246 pub async fn try_create_lock_table(&self) -> Result<CreateLockTableResult, LockClientError> {
253 let attribute_definitions = vec![
254 AttributeDefinition::builder()
255 .attribute_name(constants::ATTR_TABLE_PATH)
256 .attribute_type(ScalarAttributeType::S)
257 .build()
258 .unwrap(),
259 AttributeDefinition::builder()
260 .attribute_name(constants::ATTR_FILE_NAME)
261 .attribute_type(ScalarAttributeType::S)
262 .build()
263 .unwrap(),
264 ];
265 let request = self
266 .dynamodb_client
267 .create_table()
268 .set_attribute_definitions(Some(attribute_definitions))
269 .set_key_schema(Some(vec![
270 KeySchemaElement::builder()
271 .attribute_name(constants::ATTR_TABLE_PATH.to_owned())
272 .key_type(KeyType::Hash)
273 .build()
274 .unwrap(),
275 KeySchemaElement::builder()
276 .attribute_name(constants::ATTR_FILE_NAME.to_owned())
277 .key_type(KeyType::Range)
278 .build()
279 .unwrap(),
280 ]))
281 .billing_mode(self.config.billing_mode.clone())
282 .table_name(&self.config.lock_table_name)
283 .send();
284 match request.await {
285 Ok(_) => Ok(CreateLockTableResult::TableCreated),
286 Err(sdk_err) => match sdk_err.as_service_error() {
287 Some(CreateTableError::ResourceInUseException(_)) => {
288 Ok(CreateLockTableResult::TableAlreadyExists)
289 }
290 Some(_) => Err(LockClientError::LockTableCreateFailure {
291 name: self.config.lock_table_name.clone(),
292 source: Box::new(sdk_err.into_service_error()),
293 }),
294 _ => Err(LockClientError::GenericDynamoDb {
295 source: Box::new(sdk_err),
296 }),
297 },
298 }
299 }
300
301 pub fn get_lock_table_name(&self) -> String {
303 self.config.lock_table_name.clone()
304 }
305
306 pub fn get_dynamodb_config(&self) -> &DynamoDbConfig {
307 &self.config
308 }
309
310 pub async fn get_commit_entry(
312 &self,
313 table_path: &str,
314 version: Version,
315 ) -> Result<Option<CommitEntry>, LockClientError> {
316 let item = self
317 .retry(
318 || async {
319 self.dynamodb_client
320 .get_item()
321 .consistent_read(true)
322 .table_name(&self.config.lock_table_name)
323 .set_key(Some(get_primary_key(version, table_path)))
324 .send()
325 .await
326 },
327 |err| {
328 matches!(
329 err.as_service_error(),
330 Some(GetItemError::ProvisionedThroughputExceededException(_))
331 )
332 },
333 )
334 .await
335 .map_err(|err| match err.as_service_error() {
336 Some(GetItemError::ProvisionedThroughputExceededException(_)) => {
337 LockClientError::ProvisionedThroughputExceeded
338 }
339 _ => err.into(),
340 })?;
341 item.item.as_ref().map(CommitEntry::try_from).transpose()
342 }
343
344 pub async fn put_commit_entry(
346 &self,
347 table_path: &str,
348 entry: &CommitEntry,
349 ) -> Result<(), LockClientError> {
350 self.retry(
351 || async {
352 let item = create_value_map(entry, table_path);
353 let _ = self
354 .dynamodb_client
355 .put_item()
356 .condition_expression(constants::CONDITION_EXPR_CREATE.as_str())
357 .table_name(self.get_lock_table_name())
358 .set_item(Some(item))
359 .send()
360 .await?;
361 Ok(())
362 },
363 |err: &SdkError<_, _>| {
364 matches!(
365 err.as_service_error(),
366 Some(PutItemError::ProvisionedThroughputExceededException(_))
367 )
368 },
369 )
370 .await
371 .map_err(|err| match err.as_service_error() {
372 Some(PutItemError::ProvisionedThroughputExceededException(_)) => {
373 LockClientError::ProvisionedThroughputExceeded
374 }
375 Some(PutItemError::ConditionalCheckFailedException(_)) => {
376 LockClientError::VersionAlreadyExists {
377 table_path: table_path.to_owned(),
378 version: entry.version,
379 }
380 }
381 Some(PutItemError::ResourceNotFoundException(_)) => LockClientError::LockTableNotFound,
382 _ => err.into(),
383 })
384 }
385
386 pub async fn get_latest_entry(
388 &self,
389 table_path: &str,
390 ) -> Result<Option<CommitEntry>, LockClientError> {
391 Ok(self
392 .get_latest_entries(table_path, 1)
393 .await?
394 .into_iter()
395 .next())
396 }
397
398 pub async fn get_latest_entries(
400 &self,
401 table_path: &str,
402 limit: u64,
403 ) -> Result<Vec<CommitEntry>, LockClientError> {
404 let query_result = self
405 .retry(
406 || async {
407 self.dynamodb_client
408 .query()
409 .table_name(self.get_lock_table_name())
410 .consistent_read(true)
411 .limit(limit.try_into().unwrap_or(i32::MAX))
412 .scan_index_forward(false)
413 .key_condition_expression(format!("{} = :tn", constants::ATTR_TABLE_PATH))
414 .set_expression_attribute_values(Some(HashMap::from([(
415 ":tn".into(),
416 string_attr(table_path.trim_end_matches('/')),
419 )])))
420 .send()
421 .await
422 },
423 |err: &SdkError<_, _>| {
424 matches!(
425 err.as_service_error(),
426 Some(QueryError::ProvisionedThroughputExceededException(_))
427 )
428 },
429 )
430 .await
431 .map_err(|err| match err.as_service_error() {
432 Some(QueryError::ProvisionedThroughputExceededException(_)) => {
433 LockClientError::ProvisionedThroughputExceeded
434 }
435 _ => err.into(),
436 })?;
437
438 query_result
439 .items
440 .unwrap()
441 .iter()
442 .map(CommitEntry::try_from)
443 .collect()
444 }
445
446 pub async fn update_commit_entry(
448 &self,
449 version: Version,
450 table_path: &str,
451 ) -> Result<UpdateLogEntryResult, LockClientError> {
452 let seconds_since_epoch = (SystemTime::now()
453 + constants::DEFAULT_COMMIT_ENTRY_EXPIRATION_DELAY)
454 .duration_since(SystemTime::UNIX_EPOCH)
455 .unwrap()
456 .as_secs();
457 let res = self
458 .retry(
459 || async {
460 let _ = self
461 .dynamodb_client
462 .update_item()
463 .table_name(self.get_lock_table_name())
464 .set_key(Some(get_primary_key(version, table_path)))
465 .update_expression("SET complete = :c, expireTime = :e".to_owned())
466 .set_expression_attribute_values(Some(HashMap::from([
467 (":c".to_owned(), string_attr("true")),
468 (":e".to_owned(), num_attr(seconds_since_epoch)),
469 (":f".into(), string_attr("false")),
470 ])))
471 .condition_expression(constants::CONDITION_UPDATE_INCOMPLETE)
472 .send()
473 .await?;
474 Ok(())
475 },
476 |err: &SdkError<_, _>| {
477 matches!(
478 err.as_service_error(),
479 Some(UpdateItemError::ProvisionedThroughputExceededException(_))
480 )
481 },
482 )
483 .await;
484
485 match res {
486 Ok(()) => Ok(UpdateLogEntryResult::UpdatePerformed),
487 Err(err) => match err.as_service_error() {
488 Some(UpdateItemError::ProvisionedThroughputExceededException(_)) => {
489 Err(LockClientError::ProvisionedThroughputExceeded)
490 }
491 Some(UpdateItemError::ConditionalCheckFailedException(_)) => {
492 Ok(UpdateLogEntryResult::AlreadyCompleted)
493 }
494 _ => Err(err.into()),
495 },
496 }
497 }
498
499 pub async fn delete_commit_entry(
501 &self,
502 version: Version,
503 table_path: &str,
504 ) -> Result<(), LockClientError> {
505 self.retry(
506 || async {
507 let _ = self
508 .dynamodb_client
509 .delete_item()
510 .table_name(self.get_lock_table_name())
511 .set_key(Some(get_primary_key(version, table_path)))
512 .set_expression_attribute_values(Some(HashMap::from([(
513 ":f".into(),
514 string_attr("false"),
515 )])))
516 .condition_expression(constants::CONDITION_DELETE_INCOMPLETE.as_str())
517 .send()
518 .await?;
519 Ok(())
520 },
521 |err: &SdkError<_, _>| {
522 matches!(
523 err.as_service_error(),
524 Some(DeleteItemError::ProvisionedThroughputExceededException(_))
525 )
526 },
527 )
528 .await
529 .map_err(|err| match err.as_service_error() {
530 Some(DeleteItemError::ProvisionedThroughputExceededException(_)) => {
531 LockClientError::ProvisionedThroughputExceeded
532 }
533 Some(DeleteItemError::ConditionalCheckFailedException(_)) => {
534 LockClientError::VersionAlreadyCompleted {
535 table_path: table_path.to_owned(),
536 version,
537 }
538 }
539 _ => err.into(),
540 })
541 }
542
543 async fn retry<I, E, F, Fut, Wn>(&self, operation: F, when: Wn) -> Result<I, E>
544 where
545 F: FnMut() -> Fut,
546 Fut: std::future::Future<Output = Result<I, E>>,
547 Wn: Fn(&E) -> bool,
548 {
549 use backon::Retryable;
550 let backoff = backon::ExponentialBuilder::default()
551 .with_factor(2.)
552 .with_max_delay(self.config.max_elapsed_request_time);
553 operation.retry(backoff).when(when).await
554 }
555}
556
557#[derive(Debug, PartialEq)]
558pub enum UpdateLogEntryResult {
559 UpdatePerformed,
560 AlreadyCompleted,
561}
562
563impl TryFrom<&HashMap<String, AttributeValue>> for CommitEntry {
564 type Error = LockClientError;
565
566 fn try_from(item: &HashMap<String, AttributeValue>) -> Result<Self, Self::Error> {
567 let version_str = extract_required_string_field(item, constants::ATTR_FILE_NAME)?;
568 let version = extract_version_from_filename(version_str).ok_or_else(|| {
569 LockClientError::InconsistentData {
570 description: format!(
571 "invalid log file name: can't extract version number from '{version_str}'"
572 ),
573 }
574 })?;
575 let temp_path = extract_required_string_field(item, constants::ATTR_TEMP_PATH)?;
576 let temp_path =
577 Path::from_iter(DELTA_LOG_PATH.parts().chain(Path::from(temp_path).parts()));
578 let expire_time: Option<SystemTime> =
579 extract_optional_number_field(item, constants::ATTR_EXPIRE_TIME)?
580 .map(|s| {
581 s.parse::<u64>()
582 .map_err(|err| LockClientError::InconsistentData {
583 description: format!("conversion to number failed, {err}"),
584 })
585 })
586 .transpose()?
587 .map(epoch_to_system_time);
588 let complete = extract_required_string_field(item, constants::ATTR_COMPLETE)? == "true";
589
590 Ok(Self {
591 version,
592 temp_path,
593 complete,
594 expire_time,
595 })
596 }
597}
598
599fn system_time_to_epoch(t: &SystemTime) -> u64 {
600 t.duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs()
601}
602
603fn epoch_to_system_time(s: u64) -> SystemTime {
604 SystemTime::UNIX_EPOCH + Duration::from_secs(s)
605}
606
607fn get_primary_key(version: Version, table_path: &str) -> HashMap<String, AttributeValue> {
612 HashMap::from([
613 (
614 constants::ATTR_TABLE_PATH.to_owned(),
615 string_attr(table_path.trim_end_matches('/')),
616 ),
617 (
618 constants::ATTR_FILE_NAME.to_owned(),
619 string_attr(format!("{version:020}.json")),
620 ),
621 ])
622}
623
624fn create_value_map(
625 commit_entry: &CommitEntry,
626 table_path: &str,
627) -> HashMap<String, AttributeValue> {
628 let temp_path = Path::from_iter(commit_entry.temp_path.parts().skip(1));
630 let mut value_map = get_primary_key(commit_entry.version, table_path);
631
632 value_map.extend(HashMap::from([
633 (constants::ATTR_TEMP_PATH.to_owned(), string_attr(temp_path)),
634 (
635 constants::ATTR_COMPLETE.to_owned(),
636 string_attr(if commit_entry.complete {
637 "true"
638 } else {
639 "false"
640 }),
641 ),
642 ]));
643 commit_entry.expire_time.as_ref().map(|t| {
644 value_map.insert(
645 constants::ATTR_EXPIRE_TIME.to_owned(),
646 num_attr(system_time_to_epoch(t)),
647 )
648 });
649 value_map
650}
651
652#[derive(Debug, TypedBuilder)]
654#[builder(doc)]
655pub struct DynamoDbConfig {
656 pub billing_mode: BillingMode,
658 #[builder(setter(into))]
660 pub lock_table_name: String,
661 pub max_elapsed_request_time: Duration,
663 pub sdk_config: SdkConfig,
665}
666
667impl Eq for DynamoDbConfig {}
668impl PartialEq for DynamoDbConfig {
669 fn eq(&self, other: &Self) -> bool {
670 self.billing_mode == other.billing_mode
671 && self.lock_table_name == other.lock_table_name
672 && self.max_elapsed_request_time == other.max_elapsed_request_time
673 && self.sdk_config.endpoint_url() == other.sdk_config.endpoint_url()
674 && self.sdk_config.region() == other.sdk_config.region()
675 }
676}
677
678#[derive(Debug, PartialEq)]
680pub enum CreateLockTableResult {
681 TableCreated,
683 TableAlreadyExists,
686}
687
688fn extract_required_string_field<'a>(
691 fields: &'a HashMap<String, AttributeValue>,
692 field_name: &str,
693) -> Result<&'a str, LockClientError> {
694 fields
695 .get(field_name)
696 .ok_or_else(|| LockClientError::InconsistentData {
697 description: format!("mandatory string field '{field_name}' missing"),
698 })?
699 .as_s()
700 .map_err(|v| LockClientError::InconsistentData {
701 description: format!(
702 "mandatory string field '{field_name}' exists, but is not a string: {v:#?}",
703 ),
704 })
705 .map(|s| s.as_str())
706}
707
708fn extract_optional_number_field<'a>(
711 fields: &'a HashMap<String, AttributeValue>,
712 field_name: &str,
713) -> Result<Option<&'a String>, LockClientError> {
714 fields
715 .get(field_name)
716 .map(|attr| {
717 attr.as_n().map_err(|_| LockClientError::InconsistentData {
718 description: format!(
719 "field with name '{field_name}' exists, but is not of type number"
720 ),
721 })
722 })
723 .transpose()
724}
725
726fn string_attr<T: ToString>(s: T) -> AttributeValue {
727 AttributeValue::S(s.to_string())
728}
729
730fn num_attr<T: ToString>(n: T) -> AttributeValue {
731 AttributeValue::N(n.to_string())
732}
733
734static DELTA_LOG_PATH: LazyLock<Path> = LazyLock::new(|| Path::from("_delta_log"));
735static DELTA_LOG_REGEX: LazyLock<Regex> =
736 LazyLock::new(|| Regex::new(r"(\d{20})\.(json|checkpoint).*$").unwrap());
737
738fn extract_version_from_filename(name: &str) -> Option<Version> {
740 DELTA_LOG_REGEX
741 .captures(name)
742 .map(|captures| captures.get(1).unwrap().as_str().parse().unwrap())
743}
744
745#[cfg(test)]
746mod tests {
747 use super::*;
748 use aws_sdk_sts::config::ProvideCredentials;
749
750 use pretty_assertions::assert_eq;
751
752 use object_store::memory::InMemory;
753 use serial_test::serial;
754
755 fn commit_entry_roundtrip(c: &CommitEntry) -> Result<(), LockClientError> {
756 let item_data: HashMap<String, AttributeValue> = create_value_map(c, "some_table");
757 let c_parsed = CommitEntry::try_from(&item_data)?;
758 assert_eq!(c, &c_parsed);
759 Ok(())
760 }
761
762 #[test]
763 fn test_get_primary_key() -> DeltaResult<()> {
764 let version = 0;
765 let expected = HashMap::from([
766 (
767 constants::ATTR_TABLE_PATH.to_owned(),
768 string_attr("s3://bucket/table"),
771 ),
772 (
773 constants::ATTR_FILE_NAME.to_owned(),
774 string_attr(format!("{version:020}.json")),
775 ),
776 ]);
777
778 assert_eq!(expected, get_primary_key(version, "s3://bucket/table"));
779 assert_eq!(expected, get_primary_key(version, "s3://bucket/table/"));
780 Ok(())
781 }
782
783 #[test]
784 fn commit_entry_roundtrip_test() -> Result<(), LockClientError> {
785 let system_time = SystemTime::UNIX_EPOCH
786 + Duration::from_secs(
787 SystemTime::now()
788 .duration_since(SystemTime::UNIX_EPOCH)
789 .unwrap()
790 .as_secs(),
791 );
792 commit_entry_roundtrip(
793 &CommitEntry::builder()
794 .version(0)
795 .temp_path(Path::from("_delta_log/tmp/0_abc.json"))
796 .complete(true)
797 .expire_time(system_time)
798 .build(),
799 )?;
800 commit_entry_roundtrip(
801 &CommitEntry::builder()
802 .version(139)
803 .temp_path(Path::from("_delta_log/tmp/0_abc.json"))
804 .build(),
805 )?;
806 Ok(())
807 }
808
809 #[test]
812 #[serial]
813 fn test_logstore_factory_default() {
814 let factory = S3LogStoreFactory::default();
815 let store = Arc::new(InMemory::new());
816 let url = Url::parse("s3://test-bucket").unwrap();
817 unsafe {
818 std::env::remove_var(crate::constants::AWS_S3_LOCKING_PROVIDER);
819 }
820 let logstore = factory
821 .with_options(store.clone(), store, &url, &Default::default())
822 .unwrap();
823 assert_eq!(logstore.name(), "DefaultLogStore");
824 }
825
826 #[test]
827 #[serial]
828 fn test_logstore_factory_with_locking_provider() {
829 let factory = S3LogStoreFactory::default();
830 let store = Arc::new(InMemory::new());
831 let url = Url::parse("s3://test-bucket").unwrap();
832 unsafe {
833 std::env::set_var(crate::constants::AWS_S3_LOCKING_PROVIDER, "dynamodb");
834 }
835
836 let logstore = factory
837 .with_options(store.clone(), store, &url, &Default::default())
838 .unwrap();
839 assert_eq!(logstore.name(), "S3DynamoDbLogStore");
840 }
841
842 #[test]
843 #[serial]
844 fn test_create_dynamodb_sdk_config() {
845 let sdk_config = SdkConfig::builder()
846 .region(Region::from_static("eu-west-1"))
847 .endpoint_url("http://localhost:1234")
848 .build();
849 let dynamodb_sdk_config = DynamoDbLockClient::create_dynamodb_sdk_config(
850 &sdk_config,
851 Some("http://localhost:2345".to_string()),
852 None,
853 None,
854 None,
855 None,
856 );
857 assert_eq!(
858 dynamodb_sdk_config.endpoint_url(),
859 Some("http://localhost:2345"),
860 );
861 assert_eq!(
862 dynamodb_sdk_config.region().unwrap().to_string(),
863 "eu-west-1".to_string(),
864 );
865 let dynamodb_sdk_no_override_config = DynamoDbLockClient::create_dynamodb_sdk_config(
866 &sdk_config,
867 None,
868 None,
869 None,
870 None,
871 None,
872 );
873 assert_eq!(
874 dynamodb_sdk_no_override_config.endpoint_url(),
875 Some("http://localhost:1234"),
876 );
877 }
878
879 #[tokio::test]
880 #[serial]
881 async fn test_create_dynamodb_sdk_config_override_credentials() {
882 let sdk_config = SdkConfig::builder()
883 .region(Region::from_static("eu-west-1"))
884 .endpoint_url("http://localhost:1234")
885 .build();
886 let dynamodb_sdk_config = DynamoDbLockClient::create_dynamodb_sdk_config(
887 &sdk_config,
888 Some("http://localhost:2345".to_string()),
889 Some("us-west-1".to_string()),
890 Some("access_key_dynamodb".to_string()),
891 Some("secret_access_key_dynamodb".to_string()),
892 None,
893 );
894 assert_eq!(
895 dynamodb_sdk_config.endpoint_url(),
896 Some("http://localhost:2345"),
897 );
898 assert_eq!(
899 dynamodb_sdk_config.region().unwrap().to_string(),
900 "us-west-1".to_string(),
901 );
902
903 let credentials_provider = dynamodb_sdk_config
905 .credentials_provider()
906 .unwrap()
907 .provide_credentials()
908 .await
909 .unwrap();
910
911 assert_eq!(credentials_provider.access_key_id(), "access_key_dynamodb");
912 assert_eq!(
913 credentials_provider.secret_access_key(),
914 "secret_access_key_dynamodb"
915 );
916
917 let dynamodb_sdk_no_override_config = DynamoDbLockClient::create_dynamodb_sdk_config(
918 &sdk_config,
919 None,
920 None,
921 None,
922 None,
923 None,
924 );
925 assert_eq!(
926 dynamodb_sdk_no_override_config.endpoint_url(),
927 Some("http://localhost:1234"),
928 );
929 }
930}