Skip to content

Commit 9f68e24

Browse files
committed
refactor: group stdin object store helpers into StdinUtils
Move the stdin scheme constant, location helpers, and in-memory object store construction (plus their tests) out of object_storage.rs into a dedicated object_storage/stdin.rs submodule, encapsulated as StdinUtils.
1 parent 6eb3866 commit 9f68e24

3 files changed

Lines changed: 237 additions & 180 deletions

File tree

datafusion-cli/src/exec.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use crate::print_format::PrintFormat;
2323
use crate::{
2424
command::{Command, OutputFormat},
2525
helper::CliHelper,
26-
object_storage::{get_object_store, rewrite_stdin_location},
26+
object_storage::{get_object_store, stdin::StdinUtils},
2727
print_options::{MaxRows, PrintOptions},
2828
};
2929
use datafusion::common::instant::Instant;
@@ -424,7 +424,7 @@ async fn create_plan(
424424

425425
// Expose stdin (e.g. `cat data.csv | datafusion-cli`) as a `stdin://`
426426
// object store, registered like any other scheme in `get_object_store`.
427-
cmd.location = rewrite_stdin_location(&cmd.location, format.as_ref());
427+
cmd.location = StdinUtils::rewrite_location(&cmd.location, format.as_ref());
428428

429429
register_object_store_and_config_extensions(
430430
ctx,

datafusion-cli/src/object_storage.rs

Lines changed: 5 additions & 178 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
// under the License.
1717

1818
pub mod instrumented;
19+
pub(crate) mod stdin;
1920

2021
use async_trait::async_trait;
2122
use aws_config::BehaviorVersion;
@@ -28,22 +29,18 @@ use datafusion::{
2829
config::ExtensionOptions, config::TableOptions, config::Visit, config_err,
2930
exec_datafusion_err, exec_err,
3031
},
31-
config::ConfigFileType,
3232
error::{DataFusionError, Result},
3333
execution::context::SessionState,
3434
};
3535
use log::debug;
3636
use object_store::{
3737
ClientOptions, CredentialProvider,
3838
Error::Generic,
39-
ObjectStore, ObjectStoreExt,
39+
ObjectStore,
4040
aws::{AmazonS3Builder, AmazonS3ConfigKey, AwsCredential},
4141
gcp::GoogleCloudStorageBuilder,
4242
http::HttpBuilder,
43-
memory::InMemory,
44-
path::Path as ObjectStorePath,
4543
};
46-
use std::io::Read;
4744
use std::{
4845
any::Any,
4946
error::Error,
@@ -568,7 +565,9 @@ pub(crate) async fn get_object_store(
568565
.with_url(url.origin().ascii_serialization())
569566
.build()?,
570567
),
571-
STDIN_SCHEME => stdin_object_store(url).await?,
568+
_ if scheme == stdin::StdinUtils::SCHEME => {
569+
stdin::StdinUtils::object_store(url).await?
570+
}
572571
_ => {
573572
// For other types, try to get from `object_store_registry`:
574573
state
@@ -583,75 +582,6 @@ pub(crate) async fn get_object_store(
583582
Ok(store)
584583
}
585584

586-
/// The URL scheme used to expose the process's standard input as an object
587-
/// store, mirroring how `s3`, `gs`, `http`, etc. are addressed.
588-
const STDIN_SCHEME: &str = "stdin";
589-
590-
/// Filesystem paths that refer to the process's standard input.
591-
///
592-
/// These are intentionally limited to the well known pseudo-files exposed by
593-
/// the operating system so that ordinary files are never accidentally treated
594-
/// as stdin.
595-
const STDIN_LOCATIONS: [&str; 3] = ["/dev/stdin", "/dev/fd/0", "/proc/self/fd/0"];
596-
597-
/// Returns `true` if `location` refers to the process's standard input.
598-
fn is_stdin_location(location: &str) -> bool {
599-
STDIN_LOCATIONS.contains(&location)
600-
}
601-
602-
/// Rewrites the well known stdin pseudo-paths (e.g. `/dev/stdin`) to a canonical
603-
/// `stdin://` URL so that reading from standard input flows through the same
604-
/// object-store/listing code path as any other scheme. Non-stdin locations are
605-
/// returned unchanged.
606-
///
607-
/// The listing layer filters candidate files by extension, so the canonical
608-
/// object is named with the extension matching the declared `STORED AS` format.
609-
pub(crate) fn rewrite_stdin_location(
610-
location: &str,
611-
format: Option<&ConfigFileType>,
612-
) -> String {
613-
if !is_stdin_location(location) {
614-
return location.to_string();
615-
}
616-
617-
let object_name = match format {
618-
Some(ConfigFileType::CSV) => "stdin.csv",
619-
Some(ConfigFileType::JSON) => "stdin.json",
620-
Some(ConfigFileType::PARQUET) => "stdin.parquet",
621-
_ => "stdin",
622-
};
623-
format!("{STDIN_SCHEME}:///{object_name}")
624-
}
625-
626-
/// Builds the object store backing the `stdin://` scheme by reading all of
627-
/// standard input into memory.
628-
///
629-
/// A pipe (e.g. `cat data.csv | datafusion-cli`) is not seekable and reports a
630-
/// size of `0`, so it cannot be read directly by the file based formats (CSV
631-
/// requires seeking, Parquet needs the footer at the end of the file). Buffering
632-
/// the whole input up front sidesteps these limitations and lets the data be
633-
/// read like any other object, including being scanned more than once.
634-
async fn stdin_object_store(url: &Url) -> Result<Arc<dyn ObjectStore>> {
635-
let mut buffer = Vec::new();
636-
std::io::stdin()
637-
.lock()
638-
.read_to_end(&mut buffer)
639-
.map_err(|e| exec_datafusion_err!("Failed to read from stdin: {e}"))?;
640-
in_memory_object_store(url, buffer).await
641-
}
642-
643-
/// Stores `data` at the path referenced by `url` in a fresh [`InMemory`] store.
644-
async fn in_memory_object_store(
645-
url: &Url,
646-
data: Vec<u8>,
647-
) -> Result<Arc<dyn ObjectStore>> {
648-
let store = InMemory::new();
649-
store
650-
.put(&ObjectStorePath::from_url_path(url.path())?, data.into())
651-
.await?;
652-
Ok(Arc::new(store))
653-
}
654-
655585
#[cfg(test)]
656586
mod tests {
657587
use crate::cli_context::CliSessionContext;
@@ -991,107 +921,4 @@ mod tests {
991921
}
992922
Ok(())
993923
}
994-
995-
#[test]
996-
fn rewrites_stdin_locations() {
997-
// stdin pseudo-paths are rewritten to a `stdin://` URL carrying the
998-
// extension that matches the declared format.
999-
assert_eq!(
1000-
rewrite_stdin_location("/dev/stdin", Some(&ConfigFileType::CSV)),
1001-
"stdin:///stdin.csv"
1002-
);
1003-
assert_eq!(
1004-
rewrite_stdin_location("/dev/fd/0", Some(&ConfigFileType::JSON)),
1005-
"stdin:///stdin.json"
1006-
);
1007-
assert_eq!(
1008-
rewrite_stdin_location("/proc/self/fd/0", Some(&ConfigFileType::PARQUET)),
1009-
"stdin:///stdin.parquet"
1010-
);
1011-
assert_eq!(rewrite_stdin_location("/dev/stdin", None), "stdin:///stdin");
1012-
1013-
// Ordinary locations are left untouched.
1014-
for location in ["/dev/stdout", "data/stdin.csv", "stdin", "s3://b/f.csv"] {
1015-
assert_eq!(
1016-
rewrite_stdin_location(location, Some(&ConfigFileType::CSV)),
1017-
location
1018-
);
1019-
}
1020-
}
1021-
1022-
/// Buffers `data` into the `stdin://` object store and reads it back through
1023-
/// a `CREATE EXTERNAL TABLE`, returning the number of rows in the table.
1024-
///
1025-
/// This exercises the full path used for `/dev/stdin` short of the actual
1026-
/// stdin read, which cannot be driven from a unit test.
1027-
async fn count_stdin_rows(
1028-
data: Vec<u8>,
1029-
stored_as: &str,
1030-
format: Option<ConfigFileType>,
1031-
options: &str,
1032-
) -> Result<usize> {
1033-
let location = rewrite_stdin_location("/dev/stdin", format.as_ref());
1034-
let url = Url::parse(&location).unwrap();
1035-
let store = in_memory_object_store(&url, data).await?;
1036-
1037-
let ctx = SessionContext::new();
1038-
ctx.register_object_store(&url, store);
1039-
ctx.sql(&format!(
1040-
"CREATE EXTERNAL TABLE t STORED AS {stored_as} LOCATION '{location}' {options}"
1041-
))
1042-
.await?
1043-
.collect()
1044-
.await?;
1045-
1046-
ctx.sql("SELECT * FROM t").await?.count().await
1047-
}
1048-
1049-
#[tokio::test]
1050-
async fn stdin_object_store_reads_csv() -> Result<()> {
1051-
let data = b"a,b\n1,foo\n2,bar\n".to_vec();
1052-
let rows = count_stdin_rows(
1053-
data,
1054-
"CSV",
1055-
Some(ConfigFileType::CSV),
1056-
"OPTIONS ('format.has_header' 'true')",
1057-
)
1058-
.await?;
1059-
assert_eq!(rows, 2);
1060-
Ok(())
1061-
}
1062-
1063-
#[tokio::test]
1064-
async fn stdin_object_store_reads_json() -> Result<()> {
1065-
let data = b"{\"a\": 1, \"b\": \"foo\"}\n{\"a\": 2, \"b\": \"bar\"}\n".to_vec();
1066-
let rows = count_stdin_rows(data, "JSON", Some(ConfigFileType::JSON), "").await?;
1067-
assert_eq!(rows, 2);
1068-
Ok(())
1069-
}
1070-
1071-
#[tokio::test]
1072-
async fn stdin_object_store_reads_parquet() -> Result<()> {
1073-
use datafusion::arrow::array::Int32Array;
1074-
use datafusion::arrow::datatypes::{DataType, Field, Schema};
1075-
use datafusion::arrow::record_batch::RecordBatch;
1076-
use parquet::arrow::ArrowWriter;
1077-
1078-
// Parquet requires random access to the footer, which a real pipe cannot
1079-
// provide; the in-memory buffer makes this work.
1080-
let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
1081-
let batch = RecordBatch::try_new(
1082-
Arc::clone(&schema),
1083-
vec![Arc::new(Int32Array::from(vec![1, 2, 3]))],
1084-
)
1085-
.unwrap();
1086-
1087-
let mut data = Vec::new();
1088-
let mut writer = ArrowWriter::try_new(&mut data, schema, None).unwrap();
1089-
writer.write(&batch).unwrap();
1090-
writer.close().unwrap();
1091-
1092-
let rows =
1093-
count_stdin_rows(data, "PARQUET", Some(ConfigFileType::PARQUET), "").await?;
1094-
assert_eq!(rows, 3);
1095-
Ok(())
1096-
}
1097924
}

0 commit comments

Comments
 (0)