Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ fn main() -> Result<(), chdb_rust::error::Error> {
Err(e) => eprintln!("Error: {}", e),
}

// Borrow it as &str without copying (returns error if invalid UTF-8)
println!("&str: {}", result.data_str()?);

// Get result as UTF-8 string (lossy conversion for invalid UTF-8)
println!("Lossy UTF-8: {}", result.data_utf8_lossy());

Expand Down
14 changes: 11 additions & 3 deletions examples/03_query_results.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,33 @@ fn main() -> Result<(), chdb_rust::error::Error> {
}
println!();

println!("2. Borrowing the result as &str (no copy):");
// Checks the bytes in place instead of copying them into a new String
match result.data_str() {
Ok(text) => println!("&str: {text}"),
Err(e) => eprintln!("Error: {e}"),
}
println!();

// Note: We need to execute again because data_utf8() consumes the result
let result = execute(
"SELECT number, number * 2 AS doubled FROM numbers(5)",
Some(&[Arg::OutputFormat(OutputFormat::JSONEachRow)]),
)?;

println!("2. Getting result as UTF-8 string (lossy conversion):");
println!("3. Getting result as UTF-8 string (lossy conversion):");
// Get result as UTF-8 string (lossy conversion for invalid UTF-8)
println!("Lossy UTF-8: {}", result.data_utf8_lossy());
println!();

println!("3. Getting raw bytes:");
println!("4. Getting raw bytes:");
// Get raw bytes
let bytes = result.data_ref();
println!("Bytes length: {}", bytes.len());
println!("First 50 bytes: {:?}", &bytes[..bytes.len().min(50)]);
println!();

println!("4. Query statistics:");
println!("5. Query statistics:");
// Get query statistics
println!("Rows read: {}", result.rows_read());
println!("Bytes read: {}", result.bytes_read());
Expand Down
6 changes: 6 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ pub enum Error {
/// The data contains invalid UTF-8 sequences.
#[error("Non UTF-8 sequence: {0}")]
NonUtf8Sequence(FromUtf8Error),
/// The result data is not valid UTF-8.
///
/// Returned by [`QueryResult::data_str`](crate::query_result::QueryResult::data_str),
/// which checks the bytes in place rather than copying them first.
#[error("Invalid UTF-8 in result: {0}")]
InvalidUtf8(std::str::Utf8Error),
/// The linked library cannot report which chdb-core release it is.
///
/// `chdb_version()` arrived in chdb-core v26.7.0; an older library does not
Expand Down
28 changes: 27 additions & 1 deletion src/query_result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ impl QueryResult {
/// Get the result data as a UTF-8 string.
///
/// This method validates that the data is valid UTF-8. If the data contains
/// invalid UTF-8 sequences, it returns an error.
/// invalid UTF-8 sequences, it returns an error. It copies the data into a
/// new `String`; use [`data_str`](Self::data_str) to borrow it instead.
///
/// # Returns
///
Expand Down Expand Up @@ -86,6 +87,31 @@ impl QueryResult {
String::from_utf8(buf.to_vec()).map_err(Error::NonUtf8Sequence)
}

/// Borrow the result data as a `&str`, without copying it.
///
/// The bytes are checked in place, so this is the cheapest way to read a
/// text result: nothing is allocated. Use [`data_utf8`](Self::data_utf8) if
/// you need an owned `String`.
///
/// # Examples
///
/// ```no_run
/// use chdb_rust::execute;
///
/// let result = execute("SELECT 'Hello, World!' AS greeting", None)?;
/// let text: &str = result.data_str()?;
/// println!("{text}");
/// # Ok::<(), chdb_rust::error::Error>(())
/// ```
///
/// # Errors
///
/// Returns [`Error::InvalidUtf8`] if the data is not valid UTF-8, for
/// example with a binary output format such as `RowBinary` or `Parquet`.
pub fn data_str(&self) -> Result<&str> {
std::str::from_utf8(self.data_ref()).map_err(Error::InvalidUtf8)
}

/// Get the result data as a UTF-8 string, replacing invalid sequences.
///
/// This method converts the result data to a string, replacing any invalid UTF-8
Expand Down
40 changes: 40 additions & 0 deletions tests/query_result.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use chdb_rust::connection::Connection;
use chdb_rust::error::Error;
use chdb_rust::format::OutputFormat;

#[test]
fn data_str_borrows_the_result_without_copying() {
let conn = Connection::open_in_memory().expect("open");
let result = conn
.query("SELECT number FROM numbers(3)", OutputFormat::JSONEachRow)
.expect("query");

let text = result.data_str().expect("valid UTF-8");
assert_eq!(text, result.data_utf8().expect("valid UTF-8"));
// Same memory as the raw result buffer, so nothing was copied.
assert_eq!(text.as_ptr(), result.data_ref().as_ptr());
}

#[test]
fn data_str_rejects_binary_output() {
let conn = Connection::open_in_memory().expect("open");
// RowBinary writes the byte 0xFF as is, which is never valid UTF-8.
let result = conn
.query("SELECT unhex('FF') AS b", OutputFormat::RowBinary)
.expect("query");

match result.data_str() {
Err(Error::InvalidUtf8(_)) => {}
other => panic!("expected InvalidUtf8, got {other:?}"),
}
}

#[test]
fn data_str_on_an_empty_result_is_empty() {
let conn = Connection::open_in_memory().expect("open");
let result = conn
.query("SELECT 1 WHERE 0", OutputFormat::JSONEachRow)
.expect("query");

assert_eq!(result.data_str().expect("valid UTF-8"), "");
}
Loading