Skip to content
Open
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
14 changes: 14 additions & 0 deletions java/lance-jni/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions java/lance-jni/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ crate-type = ["cdylib"]

[features]
default = []
backtrace = ["lance/backtrace", "lance-core/backtrace"]

[dependencies]
lance = { path = "../../rust/lance", features = ["substrait"] }
Expand Down
122 changes: 115 additions & 7 deletions java/lance-jni/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,28 +175,35 @@ impl std::fmt::Display for Error {

impl From<LanceError> for Error {
fn from(err: LanceError) -> Self {
let backtrace_suffix = err
.backtrace()
.map(|bt| format!("\n\nRust backtrace:\n{}", bt))
.unwrap_or_default();
let message = format!("{}{}", err, backtrace_suffix);

match &err {
LanceError::DatasetNotFound { .. }
| LanceError::DatasetAlreadyExists { .. }
| LanceError::CommitConflict { .. }
| LanceError::InvalidInput { .. } => Self::input_error(err.to_string()),
LanceError::IO { .. } => Self::io_error(err.to_string()),
LanceError::NotSupported { .. } => Self::unsupported_error(err.to_string()),
LanceError::NotFound { .. } => Self::io_error(err.to_string()),
| LanceError::InvalidInput { .. } => Self::input_error(message),
LanceError::IO { .. } => Self::io_error(message),
LanceError::NotSupported { .. } => Self::unsupported_error(message),
LanceError::NotFound { .. } => Self::io_error(message),
LanceError::Namespace { source, .. } => {
// Try to downcast to NamespaceError and get the error code
if let Some(ns_err) = source.downcast_ref::<NamespaceError>() {
Self::namespace_error(ns_err.code().as_u32(), ns_err.to_string())
let ns_message = format!("{}{}", ns_err, backtrace_suffix);
Self::namespace_error(ns_err.code().as_u32(), ns_message)
} else {
log::warn!(
"Failed to downcast NamespaceError source, falling back to runtime error. \
This may indicate a version mismatch. Source type: {:?}",
source
);
Self::runtime_error(err.to_string())
Self::runtime_error(message)
}
}
_ => Self::runtime_error(err.to_string()),
_ => Self::runtime_error(message),
}
}
}
Expand Down Expand Up @@ -234,3 +241,104 @@ impl From<Utf8Error> for Error {
Self::input_error(err.to_string())
}
}

#[cfg(test)]
mod tests {
use super::*;

// Helper: extract the java_class from an Error via Display output
fn java_class(err: &Error) -> &JavaExceptionClass {
&err.java_class
}

#[test]
fn test_invalid_input_maps_to_illegal_argument() {
let lance_err = LanceError::invalid_input("bad input");
let jni_err: Error = lance_err.into();
assert_eq!(
*java_class(&jni_err),
JavaExceptionClass::IllegalArgumentException
);
assert!(jni_err.message.contains("bad input"));
}

#[test]
fn test_dataset_not_found_maps_to_illegal_argument() {
let lance_err = LanceError::dataset_not_found("my_dataset", "not found".to_string().into());
let jni_err: Error = lance_err.into();
assert_eq!(
*java_class(&jni_err),
JavaExceptionClass::IllegalArgumentException
);
assert!(jni_err.message.contains("my_dataset"));
}

#[test]
fn test_dataset_already_exists_maps_to_illegal_argument() {
let lance_err = LanceError::dataset_already_exists("my_dataset");
let jni_err: Error = lance_err.into();
assert_eq!(
*java_class(&jni_err),
JavaExceptionClass::IllegalArgumentException
);
assert!(jni_err.message.contains("my_dataset"));
}

#[test]
fn test_commit_conflict_maps_to_illegal_argument() {
let lance_err = LanceError::commit_conflict_source(42, "conflict".to_string().into());
let jni_err: Error = lance_err.into();
assert_eq!(
*java_class(&jni_err),
JavaExceptionClass::IllegalArgumentException
);
}

#[test]
fn test_io_maps_to_ioexception() {
let lance_err = LanceError::io("disk failure");
let jni_err: Error = lance_err.into();
assert_eq!(*java_class(&jni_err), JavaExceptionClass::IOException);
assert!(jni_err.message.contains("disk failure"));
}

#[test]
fn test_not_supported_maps_to_unsupported() {
let lance_err = LanceError::not_supported("nope");
let jni_err: Error = lance_err.into();
assert_eq!(
*java_class(&jni_err),
JavaExceptionClass::UnsupportedOperationException
);
assert!(jni_err.message.contains("nope"));
}

#[test]
fn test_not_found_maps_to_ioexception() {
let lance_err = LanceError::not_found("missing_uri");
let jni_err: Error = lance_err.into();
assert_eq!(*java_class(&jni_err), JavaExceptionClass::IOException);
assert!(jni_err.message.contains("missing_uri"));
}

#[test]
fn test_fallthrough_maps_to_runtime() {
let lance_err = LanceError::internal("internal oops");
let jni_err: Error = lance_err.into();
assert_eq!(*java_class(&jni_err), JavaExceptionClass::RuntimeException);
assert!(jni_err.message.contains("internal oops"));
}

#[test]
fn test_no_backtrace_suffix_when_backtrace_is_none() {
// Without the backtrace feature enabled in lance-core default tests,
// backtrace() returns None, so no suffix should be appended.
let lance_err = LanceError::io("clean message");
let jni_err: Error = lance_err.into();
assert!(
!jni_err.message.contains("Rust backtrace:"),
"Expected no backtrace suffix, got: {}",
jni_err.message
);
}
}
7 changes: 7 additions & 0 deletions java/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
<spotless.scala.scalafmt.version>3.7.5</spotless.scala.scalafmt.version>
<spotless.delimiter>package</spotless.delimiter>
<rust.release.build>false</rust.release.build>
<rust.features></rust.features>
<skip.build.jni>false</skip.build.jni>
<shade.base>org.lance.shaded</shade.base>
<spotless.license.header>
Expand Down Expand Up @@ -396,6 +397,9 @@
<configuration>
<path>lance-jni</path>
<release>${rust.release.build}</release>
<features>
<feature>${rust.features}</feature>
</features>
<!-- Copy native libraries to target/classes for runtime access -->
<copyTo>${project.build.directory}/classes/nativelib</copyTo>
<copyWithPlatformDir>true</copyWithPlatformDir>
Expand All @@ -409,6 +413,9 @@
<configuration>
<path>lance-jni</path>
<release>${rust.release.build}</release>
<features>
<feature>${rust.features}</feature>
</features>
<verbosity>-v</verbosity>
</configuration>
</execution>
Expand Down
4 changes: 4 additions & 0 deletions rust/lance-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ proptest.workspace = true
rstest.workspace = true

[features]
# Capture Rust backtraces in error types. When disabled (the default),
# the backtrace field is zero-sized with no overhead. At runtime, capture
# is still gated by RUST_BACKTRACE=1.
backtrace = []
datafusion = ["dep:datafusion-common", "dep:datafusion-sql"]

[lints]
Expand Down
Loading
Loading