1pub mod instrumented;
19pub(crate) mod stdin;
20
21pub use stdin::{StdinCarriesCommands, is_stdin_location};
22
23use async_trait::async_trait;
24use aws_config::BehaviorVersion;
25use aws_credential_types::provider::{
26 ProvideCredentials, SharedCredentialsProvider, error::CredentialsError,
27};
28use datafusion::{
29 common::{
30 config::ConfigEntry, config::ConfigExtension, config::ConfigField,
31 config::ExtensionOptions, config::TableOptions, config::Visit, config_err,
32 exec_datafusion_err, exec_err,
33 },
34 error::{DataFusionError, Result},
35 execution::context::SessionState,
36};
37use log::debug;
38use object_store::{
39 ClientOptions, CredentialProvider,
40 Error::Generic,
41 ObjectStore,
42 aws::{AmazonS3Builder, AmazonS3ConfigKey, AwsCredential},
43 gcp::GoogleCloudStorageBuilder,
44 http::HttpBuilder,
45};
46use std::{
47 any::Any,
48 error::Error,
49 fmt::{Debug, Display},
50 sync::Arc,
51};
52use url::Url;
53
54#[cfg(not(test))]
55use object_store::aws::resolve_bucket_region;
56
57#[cfg(test)]
59#[expect(
60 clippy::unused_async,
61 reason = "matches object_store::aws::resolve_bucket_region"
62)]
63async fn resolve_bucket_region(
64 _bucket: &str,
65 _client_options: &ClientOptions,
66) -> object_store::Result<String> {
67 Ok("eu-central-1".to_string())
68}
69
70pub async fn get_s3_object_store_builder(
71 url: &Url,
72 aws_options: &AwsOptions,
73 resolve_region: bool,
74) -> Result<AmazonS3Builder> {
75 Box::pin(get_s3_object_store_builder_inner(
78 url,
79 aws_options,
80 resolve_region,
81 ))
82 .await
83}
84
85async fn get_s3_object_store_builder_inner(
86 url: &Url,
87 aws_options: &AwsOptions,
88 resolve_region: bool,
89) -> Result<AmazonS3Builder> {
90 let AwsOptions {
91 access_key_id,
92 secret_access_key,
93 session_token,
94 region,
95 endpoint,
96 allow_http,
97 skip_signature,
98 } = aws_options;
99
100 let bucket_name = get_bucket_name(url)?;
101 let mut builder = AmazonS3Builder::from_env().with_bucket_name(bucket_name);
102
103 if let (Some(access_key_id), Some(secret_access_key)) =
104 (access_key_id, secret_access_key)
105 {
106 debug!("Using explicitly provided S3 access_key_id and secret_access_key");
107 builder = builder
108 .with_access_key_id(access_key_id)
109 .with_secret_access_key(secret_access_key);
110
111 if let Some(session_token) = session_token {
112 builder = builder.with_token(session_token);
113 }
114 } else {
115 debug!("Using AWS S3 SDK to determine credentials");
116 let CredentialsFromConfig {
117 region,
118 credentials,
119 } = CredentialsFromConfig::try_new().await?;
120 if let Some(region) = region {
121 builder = builder.with_region(region);
122 }
123 if let Some(credentials) = credentials {
124 let credentials = Arc::new(S3CredentialProvider { credentials });
125 builder = builder.with_credentials(credentials);
126 } else {
127 debug!("No credentials found, defaulting to skip signature ");
128 builder = builder.with_skip_signature(true);
129 }
130 }
131
132 if let Some(region) = region {
133 builder = builder.with_region(region);
134 }
135
136 if builder
138 .get_config_value(&AmazonS3ConfigKey::Region)
139 .is_none()
140 || resolve_region
141 {
142 let region = resolve_bucket_region(bucket_name, &ClientOptions::new()).await?;
143 builder = builder.with_region(region);
144 }
145
146 if let Some(endpoint) = endpoint {
147 if let Ok(endpoint_url) = Url::try_from(endpoint.as_str())
150 && !matches!(allow_http, Some(true))
151 && endpoint_url.scheme() == "http"
152 {
153 return config_err!(
154 "Invalid endpoint: {endpoint}. \
155 HTTP is not allowed for S3 endpoints. \
156 To allow HTTP, set 'aws.allow_http' to true"
157 );
158 }
159
160 builder = builder.with_endpoint(endpoint);
161 }
162
163 if let Some(allow_http) = allow_http {
164 builder = builder.with_allow_http(*allow_http);
165 }
166
167 if let Some(skip_signature) = skip_signature {
168 builder = builder.with_skip_signature(*skip_signature);
169 }
170
171 Ok(builder)
172}
173
174struct CredentialsFromConfig {
176 region: Option<String>,
177 credentials: Option<SharedCredentialsProvider>,
178}
179
180impl CredentialsFromConfig {
181 pub async fn try_new() -> Result<Self> {
183 let config =
186 Box::pin(aws_config::defaults(BehaviorVersion::latest()).load()).await;
187 let region = config.region().map(|r| r.to_string());
188
189 let credentials = config
190 .credentials_provider()
191 .ok_or_else(|| {
192 DataFusionError::ObjectStore(Box::new(Generic {
193 store: "S3",
194 source: "Failed to get S3 credentials aws_config".into(),
195 }))
196 })?
197 .clone();
198
199 let credentials = match credentials.provide_credentials().await {
203 Ok(_) => Some(credentials),
204 Err(CredentialsError::CredentialsNotLoaded(_)) => {
205 debug!("Could not use AWS SDK to get credentials");
206 None
207 }
208 Err(e) => {
211 let source_message = if let Some(source) = e.source() {
213 format!(": {source}")
214 } else {
215 String::new()
216 };
217
218 let message = format!(
219 "Error getting credentials from provider: {e}{source_message}",
220 );
221
222 return Err(DataFusionError::ObjectStore(Box::new(Generic {
223 store: "S3",
224 source: message.into(),
225 })));
226 }
227 };
228 Ok(Self {
229 region,
230 credentials,
231 })
232 }
233}
234
235#[derive(Debug)]
236struct S3CredentialProvider {
237 credentials: SharedCredentialsProvider,
238}
239
240#[async_trait]
241impl CredentialProvider for S3CredentialProvider {
242 type Credential = AwsCredential;
243
244 async fn get_credential(&self) -> object_store::Result<Arc<Self::Credential>> {
245 let creds =
246 self.credentials
247 .provide_credentials()
248 .await
249 .map_err(|e| Generic {
250 store: "S3",
251 source: Box::new(e),
252 })?;
253 Ok(Arc::new(AwsCredential {
254 key_id: creds.access_key_id().to_string(),
255 secret_key: creds.secret_access_key().to_string(),
256 token: creds.session_token().map(ToString::to_string),
257 }))
258 }
259}
260
261pub fn get_oss_object_store_builder(
262 url: &Url,
263 aws_options: &AwsOptions,
264) -> Result<AmazonS3Builder> {
265 get_object_store_builder(url, aws_options, true)
266}
267
268pub fn get_cos_object_store_builder(
269 url: &Url,
270 aws_options: &AwsOptions,
271) -> Result<AmazonS3Builder> {
272 get_object_store_builder(url, aws_options, false)
273}
274
275fn get_object_store_builder(
276 url: &Url,
277 aws_options: &AwsOptions,
278 virtual_hosted_style_request: bool,
279) -> Result<AmazonS3Builder> {
280 let bucket_name = get_bucket_name(url)?;
281 let mut builder = AmazonS3Builder::from_env()
282 .with_virtual_hosted_style_request(virtual_hosted_style_request)
283 .with_bucket_name(bucket_name)
284 .with_region("do_not_care");
286
287 if let (Some(access_key_id), Some(secret_access_key)) =
288 (&aws_options.access_key_id, &aws_options.secret_access_key)
289 {
290 builder = builder
291 .with_access_key_id(access_key_id)
292 .with_secret_access_key(secret_access_key);
293 }
294
295 if let Some(endpoint) = &aws_options.endpoint {
296 builder = builder.with_endpoint(endpoint);
297 }
298
299 Ok(builder)
300}
301
302pub fn get_gcs_object_store_builder(
303 url: &Url,
304 gs_options: &GcpOptions,
305) -> Result<GoogleCloudStorageBuilder> {
306 let bucket_name = get_bucket_name(url)?;
307 let mut builder = GoogleCloudStorageBuilder::from_env().with_bucket_name(bucket_name);
308
309 if let Some(service_account_path) = &gs_options.service_account_path {
310 builder = builder.with_service_account_path(service_account_path);
311 }
312
313 if let Some(service_account_key) = &gs_options.service_account_key {
314 builder = builder.with_service_account_key(service_account_key);
315 }
316
317 if let Some(application_credentials_path) = &gs_options.application_credentials_path {
318 builder = builder.with_application_credentials(application_credentials_path);
319 }
320
321 Ok(builder)
322}
323
324fn get_bucket_name(url: &Url) -> Result<&str> {
325 url.host_str().ok_or_else(|| {
326 exec_datafusion_err!("Not able to parse bucket name from url: {}", url.as_str())
327 })
328}
329
330#[derive(Default, Debug, Clone)]
332pub struct AwsOptions {
333 pub access_key_id: Option<String>,
335 pub secret_access_key: Option<String>,
337 pub session_token: Option<String>,
339 pub region: Option<String>,
341 pub endpoint: Option<String>,
343 pub allow_http: Option<bool>,
345 pub skip_signature: Option<bool>,
350}
351
352impl ExtensionOptions for AwsOptions {
353 fn as_any(&self) -> &dyn Any {
354 self
355 }
356
357 fn as_any_mut(&mut self) -> &mut dyn Any {
358 self
359 }
360
361 fn cloned(&self) -> Box<dyn ExtensionOptions> {
362 Box::new(self.clone())
363 }
364
365 fn set(&mut self, key: &str, value: &str) -> Result<()> {
366 let (_key, aws_key) = key.split_once('.').unwrap_or((key, ""));
367 let (key, rem) = aws_key.split_once('.').unwrap_or((aws_key, ""));
368 match key {
369 "access_key_id" => {
370 self.access_key_id.set(rem, value)?;
371 }
372 "secret_access_key" => {
373 self.secret_access_key.set(rem, value)?;
374 }
375 "session_token" => {
376 self.session_token.set(rem, value)?;
377 }
378 "region" => {
379 self.region.set(rem, value)?;
380 }
381 "oss" | "cos" | "endpoint" => {
382 self.endpoint.set(rem, value)?;
383 }
384 "allow_http" => {
385 self.allow_http.set(rem, value)?;
386 }
387 "skip_signature" | "nosign" => {
388 self.skip_signature.set(rem, value)?;
389 }
390 _ => {
391 return config_err!("Config value \"{}\" not found on AwsOptions", rem);
392 }
393 }
394 Ok(())
395 }
396
397 fn entries(&self) -> Vec<ConfigEntry> {
398 struct Visitor(Vec<ConfigEntry>);
399
400 impl Visit for Visitor {
401 fn some<V: Display>(
402 &mut self,
403 key: &str,
404 value: V,
405 description: &'static str,
406 ) {
407 self.0.push(ConfigEntry {
408 key: key.to_string(),
409 value: Some(value.to_string()),
410 description,
411 })
412 }
413
414 fn none(&mut self, key: &str, description: &'static str) {
415 self.0.push(ConfigEntry {
416 key: key.to_string(),
417 value: None,
418 description,
419 })
420 }
421 }
422
423 let mut v = Visitor(vec![]);
424 self.access_key_id.visit(&mut v, "access_key_id", "");
425 self.secret_access_key
426 .visit(&mut v, "secret_access_key", "");
427 self.session_token.visit(&mut v, "session_token", "");
428 self.region.visit(&mut v, "region", "");
429 self.endpoint.visit(&mut v, "endpoint", "");
430 self.allow_http.visit(&mut v, "allow_http", "");
431 v.0
432 }
433}
434
435impl ConfigExtension for AwsOptions {
436 const PREFIX: &'static str = "aws";
437}
438
439#[derive(Debug, Clone, Default)]
441pub struct GcpOptions {
442 pub service_account_path: Option<String>,
444 pub service_account_key: Option<String>,
446 pub application_credentials_path: Option<String>,
448}
449
450impl ExtensionOptions for GcpOptions {
451 fn as_any(&self) -> &dyn Any {
452 self
453 }
454
455 fn as_any_mut(&mut self) -> &mut dyn Any {
456 self
457 }
458
459 fn cloned(&self) -> Box<dyn ExtensionOptions> {
460 Box::new(self.clone())
461 }
462
463 fn set(&mut self, key: &str, value: &str) -> Result<()> {
464 let (_key, rem) = key.split_once('.').unwrap_or((key, ""));
465 match rem {
466 "service_account_path" => {
467 self.service_account_path.set(rem, value)?;
468 }
469 "service_account_key" => {
470 self.service_account_key.set(rem, value)?;
471 }
472 "application_credentials_path" => {
473 self.application_credentials_path.set(rem, value)?;
474 }
475 _ => {
476 return config_err!("Config value \"{}\" not found on GcpOptions", rem);
477 }
478 }
479 Ok(())
480 }
481
482 fn entries(&self) -> Vec<ConfigEntry> {
483 struct Visitor(Vec<ConfigEntry>);
484
485 impl Visit for Visitor {
486 fn some<V: Display>(
487 &mut self,
488 key: &str,
489 value: V,
490 description: &'static str,
491 ) {
492 self.0.push(ConfigEntry {
493 key: key.to_string(),
494 value: Some(value.to_string()),
495 description,
496 })
497 }
498
499 fn none(&mut self, key: &str, description: &'static str) {
500 self.0.push(ConfigEntry {
501 key: key.to_string(),
502 value: None,
503 description,
504 })
505 }
506 }
507
508 let mut v = Visitor(vec![]);
509 self.service_account_path
510 .visit(&mut v, "service_account_path", "");
511 self.service_account_key
512 .visit(&mut v, "service_account_key", "");
513 self.application_credentials_path.visit(
514 &mut v,
515 "application_credentials_path",
516 "",
517 );
518 v.0
519 }
520}
521
522impl ConfigExtension for GcpOptions {
523 const PREFIX: &'static str = "gcp";
524}
525
526pub(crate) async fn get_object_store(
527 state: &SessionState,
528 scheme: &str,
529 url: &Url,
530 table_options: &TableOptions,
531 resolve_region: bool,
532) -> Result<Arc<dyn ObjectStore>, DataFusionError> {
533 let store: Arc<dyn ObjectStore> = match scheme {
534 "s3" => {
535 let Some(options) = table_options.extensions.get::<AwsOptions>() else {
536 return exec_err!(
537 "Given table options incompatible with the 's3' scheme"
538 );
539 };
540 let builder =
541 get_s3_object_store_builder(url, options, resolve_region).await?;
542 Arc::new(builder.build()?)
543 }
544 "oss" => {
545 let Some(options) = table_options.extensions.get::<AwsOptions>() else {
546 return exec_err!(
547 "Given table options incompatible with the 'oss' scheme"
548 );
549 };
550 let builder = get_oss_object_store_builder(url, options)?;
551 Arc::new(builder.build()?)
552 }
553 "cos" => {
554 let Some(options) = table_options.extensions.get::<AwsOptions>() else {
555 return exec_err!(
556 "Given table options incompatible with the 'cos' scheme"
557 );
558 };
559 let builder = get_cos_object_store_builder(url, options)?;
560 Arc::new(builder.build()?)
561 }
562 "gs" | "gcs" => {
563 let Some(options) = table_options.extensions.get::<GcpOptions>() else {
564 return exec_err!(
565 "Given table options incompatible with the 'gs'/'gcs' scheme"
566 );
567 };
568 let builder = get_gcs_object_store_builder(url, options)?;
569 Arc::new(builder.build()?)
570 }
571 "http" | "https" => Arc::new(
572 HttpBuilder::new()
573 .with_client_options(ClientOptions::new().with_allow_http(true))
574 .with_url(url.origin().ascii_serialization())
575 .build()?,
576 ),
577 _ if scheme == stdin::StdinUtils::SCHEME => {
578 stdin::StdinUtils::get_or_create(state, url).await?
579 }
580 _ => {
581 state
583 .runtime_env()
584 .object_store_registry
585 .get_store(url)
586 .map_err(|_| {
587 exec_datafusion_err!("Unsupported object store scheme: {}", scheme)
588 })?
589 }
590 };
591 Ok(store)
592}
593
594#[cfg(test)]
595mod tests {
596 use crate::cli_context::CliSessionContext;
597
598 use super::*;
599
600 use datafusion::{
601 datasource::listing::ListingTableUrl,
602 logical_expr::{DdlStatement, LogicalPlan},
603 prelude::SessionContext,
604 };
605
606 use object_store::{aws::AmazonS3ConfigKey, gcp::GoogleConfigKey};
607
608 #[tokio::test]
609 async fn s3_object_store_builder_default() -> Result<()> {
610 if let Err(DataFusionError::Execution(e)) = check_aws_envs() {
611 eprintln!("{e}");
613 return Ok(());
614 }
615
616 let location = "s3://bucket/path/FAKE/file.parquet";
617 unsafe {
619 std::env::set_var("AWS_CONFIG_FILE", "data/aws.config");
620 std::env::set_var("AWS_SHARED_CREDENTIALS_FILE", "data/aws.credentials");
621 }
622
623 let table_url = ListingTableUrl::parse(location)?;
625 let scheme = table_url.scheme();
626 let sql =
627 format!("CREATE EXTERNAL TABLE test STORED AS PARQUET LOCATION '{location}'");
628
629 let ctx = SessionContext::new();
630 ctx.register_table_options_extension_from_scheme(scheme);
631 let table_options = get_table_options(&ctx, &sql).await;
632 let aws_options = table_options.extensions.get::<AwsOptions>().unwrap();
633 let builder =
634 get_s3_object_store_builder(table_url.as_ref(), aws_options, false).await?;
635
636 let expected_access_key_id = std::env::var("AWS_ACCESS_KEY_ID").ok();
638 let expected_secret_access_key = std::env::var("AWS_SECRET_ACCESS_KEY").ok();
639 let expected_region = Some(
640 std::env::var("AWS_REGION").unwrap_or_else(|_| "eu-central-1".to_string()),
641 );
642 let expected_endpoint = std::env::var("AWS_ENDPOINT").ok();
643
644 assert_eq!(
646 builder.get_config_value(&AmazonS3ConfigKey::AccessKeyId),
647 expected_access_key_id
648 );
649 assert_eq!(
650 builder.get_config_value(&AmazonS3ConfigKey::SecretAccessKey),
651 expected_secret_access_key
652 );
653 let expected_skip_signature =
655 if expected_access_key_id.is_none() && expected_secret_access_key.is_none() {
656 Some(String::from("true"))
657 } else {
658 Some(String::from("false"))
659 };
660 assert_eq!(
661 builder.get_config_value(&AmazonS3ConfigKey::Region),
662 expected_region
663 );
664 assert_eq!(
665 builder.get_config_value(&AmazonS3ConfigKey::Endpoint),
666 expected_endpoint
667 );
668 assert_eq!(builder.get_config_value(&AmazonS3ConfigKey::Token), None);
669 assert_eq!(
670 builder.get_config_value(&AmazonS3ConfigKey::SkipSignature),
671 expected_skip_signature
672 );
673 Ok(())
674 }
675
676 #[tokio::test]
677 async fn s3_object_store_builder() -> Result<()> {
678 let access_key_id = "FAKE_access_key_id";
680 let secret_access_key = "FAKE_secret_access_key";
681 let region = "fake_us-east-2";
682 let endpoint = "endpoint33";
683 let session_token = "FAKE_session_token";
684 let location = "s3://bucket/path/FAKE/file.parquet";
685
686 let table_url = ListingTableUrl::parse(location)?;
687 let scheme = table_url.scheme();
688 let sql = format!(
689 "CREATE EXTERNAL TABLE test STORED AS PARQUET OPTIONS\
690 ('aws.access_key_id' '{access_key_id}', \
691 'aws.secret_access_key' '{secret_access_key}', \
692 'aws.region' '{region}', \
693 'aws.session_token' {session_token}, \
694 'aws.endpoint' '{endpoint}'\
695 ) LOCATION '{location}'"
696 );
697
698 let ctx = SessionContext::new();
699 ctx.register_table_options_extension_from_scheme(scheme);
700 let table_options = get_table_options(&ctx, &sql).await;
701 let aws_options = table_options.extensions.get::<AwsOptions>().unwrap();
702 let builder =
703 get_s3_object_store_builder(table_url.as_ref(), aws_options, false).await?;
704 let config = [
706 (AmazonS3ConfigKey::AccessKeyId, access_key_id),
707 (AmazonS3ConfigKey::SecretAccessKey, secret_access_key),
708 (AmazonS3ConfigKey::Region, region),
709 (AmazonS3ConfigKey::Endpoint, endpoint),
710 (AmazonS3ConfigKey::Token, session_token),
711 ];
712 for (key, value) in config {
713 assert_eq!(value, builder.get_config_value(&key).unwrap());
714 }
715 assert_eq!(
717 builder.get_config_value(&AmazonS3ConfigKey::SkipSignature),
718 Some("false".into())
719 );
720
721 Ok(())
722 }
723
724 #[tokio::test]
725 async fn s3_object_store_builder_allow_http_error() -> Result<()> {
726 let access_key_id = "fake_access_key_id";
727 let secret_access_key = "fake_secret_access_key";
728 let endpoint = "http://endpoint33";
729 let location = "s3://bucket/path/file.parquet";
730
731 let table_url = ListingTableUrl::parse(location)?;
732 let scheme = table_url.scheme();
733 let sql = format!(
734 "CREATE EXTERNAL TABLE test STORED AS PARQUET OPTIONS\
735 ('aws.access_key_id' '{access_key_id}', \
736 'aws.secret_access_key' '{secret_access_key}', \
737 'aws.endpoint' '{endpoint}'\
738 ) LOCATION '{location}'"
739 );
740
741 let ctx = SessionContext::new();
742 ctx.register_table_options_extension_from_scheme(scheme);
743
744 let table_options = get_table_options(&ctx, &sql).await;
745 let aws_options = table_options.extensions.get::<AwsOptions>().unwrap();
746 let err = get_s3_object_store_builder(table_url.as_ref(), aws_options, false)
747 .await
748 .unwrap_err();
749
750 assert_eq!(
751 err.to_string().lines().next().unwrap_or_default(),
752 "Invalid or Unsupported Configuration: Invalid endpoint: http://endpoint33. HTTP is not allowed for S3 endpoints. To allow HTTP, set 'aws.allow_http' to true"
753 );
754
755 let sql = format!(
757 "CREATE EXTERNAL TABLE test STORED AS PARQUET OPTIONS\
758 ('aws.access_key_id' '{access_key_id}', \
759 'aws.secret_access_key' '{secret_access_key}', \
760 'aws.endpoint' '{endpoint}',\
761 'aws.allow_http' 'true'\
762 ) LOCATION '{location}'"
763 );
764 let table_options = get_table_options(&ctx, &sql).await;
765
766 let aws_options = table_options.extensions.get::<AwsOptions>().unwrap();
767 get_s3_object_store_builder(table_url.as_ref(), aws_options, false).await?;
769
770 Ok(())
771 }
772
773 #[tokio::test]
774 async fn s3_object_store_builder_resolves_region_when_none_provided() -> Result<()> {
775 if let Err(DataFusionError::Execution(e)) = check_aws_envs() {
776 eprintln!("{e}");
778 return Ok(());
779 }
780 let location = "s3://test-bucket/path/file.parquet";
781 unsafe {
783 std::env::set_var("AWS_CONFIG_FILE", "data/aws.config");
784 }
785
786 let table_url = ListingTableUrl::parse(location)?;
787 let aws_options = AwsOptions {
788 region: None, ..Default::default()
790 };
791
792 let builder =
793 get_s3_object_store_builder(table_url.as_ref(), &aws_options, false).await?;
794
795 assert!(
797 builder
798 .get_config_value(&AmazonS3ConfigKey::Region)
799 .is_some()
800 );
801
802 Ok(())
803 }
804
805 #[tokio::test]
806 async fn s3_object_store_builder_overrides_region_when_resolve_region_enabled()
807 -> Result<()> {
808 if let Err(DataFusionError::Execution(e)) = check_aws_envs() {
809 eprintln!("{e}");
811 return Ok(());
812 }
813
814 let original_region = "us-east-1";
815 let expected_region = "eu-central-1"; let location = "s3://test-bucket/path/file.parquet";
817
818 let table_url = ListingTableUrl::parse(location)?;
819 let aws_options = AwsOptions {
820 region: Some(original_region.to_string()), ..Default::default()
822 };
823
824 let builder =
825 get_s3_object_store_builder(table_url.as_ref(), &aws_options, true).await?;
826
827 assert_eq!(
829 builder.get_config_value(&AmazonS3ConfigKey::Region),
830 Some(expected_region.to_string())
831 );
832
833 Ok(())
834 }
835
836 #[tokio::test]
837 async fn oss_object_store_builder() -> Result<()> {
838 let access_key_id = "fake_access_key_id";
839 let secret_access_key = "fake_secret_access_key";
840 let endpoint = "fake_endpoint";
841 let location = "oss://bucket/path/file.parquet";
842
843 let table_url = ListingTableUrl::parse(location)?;
844 let scheme = table_url.scheme();
845 let sql = format!(
846 "CREATE EXTERNAL TABLE test STORED AS PARQUET OPTIONS('aws.access_key_id' '{access_key_id}', 'aws.secret_access_key' '{secret_access_key}', 'aws.oss.endpoint' '{endpoint}') LOCATION '{location}'"
847 );
848
849 let ctx = SessionContext::new();
850 ctx.register_table_options_extension_from_scheme(scheme);
851 let table_options = get_table_options(&ctx, &sql).await;
852
853 let aws_options = table_options.extensions.get::<AwsOptions>().unwrap();
854 let builder = get_oss_object_store_builder(table_url.as_ref(), aws_options)?;
855 let config = [
857 (AmazonS3ConfigKey::AccessKeyId, access_key_id),
858 (AmazonS3ConfigKey::SecretAccessKey, secret_access_key),
859 (AmazonS3ConfigKey::Endpoint, endpoint),
860 ];
861 for (key, value) in config {
862 assert_eq!(value, builder.get_config_value(&key).unwrap());
863 }
864
865 Ok(())
866 }
867
868 #[tokio::test]
869 async fn gcs_object_store_builder() -> Result<()> {
870 let service_account_path = "fake_service_account_path";
871 let service_account_key = "{\"private_key\": \"fake_private_key.pem\",\"client_email\":\"fake_client_email\"}";
872 let application_credentials_path = "fake_application_credentials_path";
873 let location = "gcs://bucket/path/file.parquet";
874
875 let table_url = ListingTableUrl::parse(location)?;
876 let scheme = table_url.scheme();
877 let sql = format!(
878 "CREATE EXTERNAL TABLE test STORED AS PARQUET OPTIONS('gcp.service_account_path' '{service_account_path}', 'gcp.service_account_key' '{service_account_key}', 'gcp.application_credentials_path' '{application_credentials_path}') LOCATION '{location}'"
879 );
880
881 let ctx = SessionContext::new();
882 ctx.register_table_options_extension_from_scheme(scheme);
883 let table_options = get_table_options(&ctx, &sql).await;
884
885 let gcp_options = table_options.extensions.get::<GcpOptions>().unwrap();
886 let builder = get_gcs_object_store_builder(table_url.as_ref(), gcp_options)?;
887 let config = [
889 (GoogleConfigKey::ServiceAccount, service_account_path),
890 (GoogleConfigKey::ServiceAccountKey, service_account_key),
891 (
892 GoogleConfigKey::ApplicationCredentials,
893 application_credentials_path,
894 ),
895 ];
896 for (key, value) in config {
897 assert_eq!(value, builder.get_config_value(&key).unwrap());
898 }
899
900 Ok(())
901 }
902
903 async fn get_table_options(ctx: &SessionContext, sql: &str) -> TableOptions {
906 let mut plan = ctx.state().create_logical_plan(sql).await.unwrap();
907
908 let LogicalPlan::Ddl(DdlStatement::CreateExternalTable(cmd)) = &mut plan else {
909 panic!("plan is not a CreateExternalTable");
910 };
911
912 let mut table_options = ctx.state().default_table_options();
913 table_options
914 .alter_with_string_hash_map(&cmd.options)
915 .unwrap();
916 table_options
917 }
918
919 fn check_aws_envs() -> Result<()> {
920 let aws_envs = [
921 "AWS_ACCESS_KEY_ID",
922 "AWS_SECRET_ACCESS_KEY",
923 "AWS_REGION",
924 "AWS_ALLOW_HTTP",
925 ];
926 for aws_env in aws_envs {
927 std::env::var(aws_env).map_err(|_| {
928 exec_datafusion_err!("aws envs not set, skipping s3 tests")
929 })?;
930 }
931 Ok(())
932 }
933}