Skip to content

Commit ff91614

Browse files
Cappu7inoSTARGAZER
andauthored
feat: expose typed native error codes for V3 (#18)
* feat: expose typed native error codes for V3 * test: make async native error code test platform-stable --------- Co-authored-by: STARGAZER <stargazer@XingdeMacBook-Air.local>
1 parent 9f2c551 commit ff91614

7 files changed

Lines changed: 378 additions & 65 deletions

File tree

docs/architecture/native-interop.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,5 @@ Multiple V3 clients share the same process-wide Tokio runtime. This reduces thre
8686
## Error Handling
8787

8888
Native failures are surfaced as managed exceptions that include operation context and the native last-error message when available. Agents should preserve these messages in diagnostics and not replace them with generic errors.
89+
90+
The native ABI also exposes stable integer error codes for engine-level last errors and async operation failures. `0` means success/no error; non-zero values distinguish invalid requests, missing tables, Delta/DataFusion/Arrow/JSON failures, internal failures, and cancellation. Managed code still includes the native message in exceptions, but cancellation detection should prefer the typed async error code and use message matching only as a compatibility fallback.

src/DeltaLakeSharp.Client/Internal/Native/NativeMethods.net472.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ static NativeMethods()
2828
[DllImport(LibraryName, EntryPoint = "dts_get_last_error", CallingConvention = CallingConvention.Cdecl)]
2929
internal static extern IntPtr GetLastError(IntPtr engine);
3030

31+
[DllImport(LibraryName, EntryPoint = "dts_get_last_error_code", CallingConvention = CallingConvention.Cdecl)]
32+
internal static extern int GetLastErrorCode(IntPtr engine);
33+
3134
[DllImport(LibraryName, EntryPoint = "dts_get_schema_async_with_callback", CallingConvention = CallingConvention.Cdecl)]
3235
private static extern IntPtr GetSchemaAsyncWithCallbackNative(
3336
IntPtr engine,
@@ -122,6 +125,9 @@ private static extern IntPtr MergeStreamAsyncWithCallbackNative(
122125
[DllImport(LibraryName, EntryPoint = "dts_async_operation_get_error", CallingConvention = CallingConvention.Cdecl)]
123126
internal static extern IntPtr AsyncOperationGetError(IntPtr operation);
124127

128+
[DllImport(LibraryName, EntryPoint = "dts_async_operation_get_error_code", CallingConvention = CallingConvention.Cdecl)]
129+
internal static extern int AsyncOperationGetErrorCode(IntPtr operation);
130+
125131
[DllImport(LibraryName, EntryPoint = "dts_async_operation_cancel", CallingConvention = CallingConvention.Cdecl)]
126132
internal static extern void AsyncOperationCancel(IntPtr operation);
127133

src/DeltaLakeSharp.Client/Internal/Native/NativeMethods.net8.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ static NativeMethods()
3232
[LibraryImport(LibraryName, EntryPoint = "dts_get_last_error")]
3333
internal static partial IntPtr GetLastError(IntPtr engine);
3434

35+
[LibraryImport(LibraryName, EntryPoint = "dts_get_last_error_code")]
36+
[return: MarshalAs(UnmanagedType.I4)]
37+
internal static partial int GetLastErrorCode(IntPtr engine);
38+
3539
[DllImport(LibraryName, EntryPoint = "dts_get_schema_async_with_callback", CallingConvention = CallingConvention.Cdecl)]
3640
private static extern IntPtr GetSchemaAsyncWithCallbackNative(
3741
IntPtr engine,
@@ -230,6 +234,10 @@ internal static IntPtr MergeStreamAsyncWithCallback(
230234
[LibraryImport(LibraryName, EntryPoint = "dts_async_operation_get_error")]
231235
internal static partial IntPtr AsyncOperationGetError(IntPtr operation);
232236

237+
[LibraryImport(LibraryName, EntryPoint = "dts_async_operation_get_error_code")]
238+
[return: MarshalAs(UnmanagedType.I4)]
239+
internal static partial int AsyncOperationGetErrorCode(IntPtr operation);
240+
233241
[LibraryImport(LibraryName, EntryPoint = "dts_async_operation_cancel")]
234242
internal static partial void AsyncOperationCancel(IntPtr operation);
235243

src/DeltaLakeSharp.Client/Internal/NativeRustBackend.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ internal sealed class NativeRustBackend : IDeltaLakeBackend
3030
private const int NativeAsyncOperationSucceeded = 1;
3131
private const int NativeAsyncOperationFailed = 2;
3232
private const int NativeAsyncOperationCancelled = 3;
33+
private const int NativeServiceErrorOk = 0;
34+
private const int NativeServiceErrorCancelled = 8;
3335

3436
private static readonly NativeMethods.NativeAsyncOperationCompletedCallback NativeAsyncOperationCompleted = OnNativeAsyncOperationCompleted;
3537

@@ -610,9 +612,15 @@ public void Dispose()
610612

611613
private InvalidOperationException CreateNativeOperationFailedException(string operation)
612614
{
615+
int lastErrorCode = NativeMethods.GetLastErrorCode(_engine.DangerousGetHandle());
613616
string? lastError = GetLastErrorMessage();
614617
string message = $"Native V3 backend operation '{operation}' failed.";
615618

619+
if (lastErrorCode != NativeServiceErrorOk)
620+
{
621+
message += $" Native error code: {lastErrorCode}.";
622+
}
623+
616624
if (!string.IsNullOrWhiteSpace(lastError))
617625
{
618626
message += $" Native error: {lastError}";
@@ -625,10 +633,16 @@ private static InvalidOperationException CreateNativeAsyncOperationFailedExcepti
625633
IntPtr operation,
626634
string operationName)
627635
{
636+
int errorCode = NativeMethods.AsyncOperationGetErrorCode(operation);
628637
IntPtr errorPtr = NativeMethods.AsyncOperationGetError(operation);
629638
string? error = NativeMethods.PtrToStringUtf8(errorPtr);
630639
string message = $"Native V3 backend operation '{operationName}' failed.";
631640

641+
if (errorCode != NativeServiceErrorOk)
642+
{
643+
message += $" Native error code: {errorCode}.";
644+
}
645+
632646
if (!string.IsNullOrWhiteSpace(error))
633647
{
634648
message += $" Native error: {error}";
@@ -1410,6 +1424,12 @@ private void Cancel()
14101424

14111425
private static bool IsNativeCancellationFailure(IntPtr operation)
14121426
{
1427+
int errorCode = NativeMethods.AsyncOperationGetErrorCode(operation);
1428+
if (errorCode == NativeServiceErrorCancelled)
1429+
{
1430+
return true;
1431+
}
1432+
14131433
string? error = NativeMethods.PtrToStringUtf8(NativeMethods.AsyncOperationGetError(operation));
14141434
return error != null
14151435
&& error.IndexOf("cancel", StringComparison.OrdinalIgnoreCase) >= 0;

src/DeltaLakeSharp.Server/v3/src/error.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,21 @@
88
99
use std::fmt;
1010

11+
/// Stable service error codes exposed through the native ABI.
12+
#[repr(i32)]
13+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14+
pub enum ServiceErrorCode {
15+
Ok = 0,
16+
InvalidRequest = 1,
17+
TableNotFound = 2,
18+
Delta = 3,
19+
DataFusion = 4,
20+
Arrow = 5,
21+
Json = 6,
22+
Internal = 7,
23+
Cancelled = 8,
24+
}
25+
1126
/// Enumerates all error kinds that can occur in the service.
1227
#[derive(Debug)]
1328
pub enum ServiceError {
@@ -33,6 +48,21 @@ pub enum ServiceError {
3348
Internal(String),
3449
}
3550

51+
impl ServiceError {
52+
/// Returns the stable native ABI code for this error.
53+
pub fn code(&self) -> ServiceErrorCode {
54+
match self {
55+
Self::InvalidRequest(_) => ServiceErrorCode::InvalidRequest,
56+
Self::TableNotFound(_) => ServiceErrorCode::TableNotFound,
57+
Self::Delta(_) => ServiceErrorCode::Delta,
58+
Self::DataFusion(_) => ServiceErrorCode::DataFusion,
59+
Self::Arrow(_) => ServiceErrorCode::Arrow,
60+
Self::Json(_) => ServiceErrorCode::Json,
61+
Self::Internal(_) => ServiceErrorCode::Internal,
62+
}
63+
}
64+
}
65+
3666
impl fmt::Display for ServiceError {
3767
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3868
match self {
@@ -84,3 +114,60 @@ impl From<serde_json::Error> for ServiceError {
84114
Self::Json(e)
85115
}
86116
}
117+
118+
#[cfg(test)]
119+
mod tests {
120+
use super::*;
121+
122+
#[test]
123+
fn service_error_code_numeric_values_are_stable() {
124+
assert_eq!(0, ServiceErrorCode::Ok as i32);
125+
assert_eq!(1, ServiceErrorCode::InvalidRequest as i32);
126+
assert_eq!(2, ServiceErrorCode::TableNotFound as i32);
127+
assert_eq!(3, ServiceErrorCode::Delta as i32);
128+
assert_eq!(4, ServiceErrorCode::DataFusion as i32);
129+
assert_eq!(5, ServiceErrorCode::Arrow as i32);
130+
assert_eq!(6, ServiceErrorCode::Json as i32);
131+
assert_eq!(7, ServiceErrorCode::Internal as i32);
132+
assert_eq!(8, ServiceErrorCode::Cancelled as i32);
133+
}
134+
135+
#[test]
136+
fn service_error_maps_to_stable_codes() {
137+
assert_eq!(
138+
ServiceErrorCode::InvalidRequest,
139+
ServiceError::InvalidRequest("missing path".to_string()).code()
140+
);
141+
assert_eq!(
142+
ServiceErrorCode::TableNotFound,
143+
ServiceError::TableNotFound("/tmp/table".to_string()).code()
144+
);
145+
assert_eq!(
146+
ServiceErrorCode::Delta,
147+
ServiceError::Delta(deltalake::DeltaTableError::Generic("delta failure".to_string()))
148+
.code()
149+
);
150+
assert_eq!(
151+
ServiceErrorCode::DataFusion,
152+
ServiceError::DataFusion(datafusion::error::DataFusionError::Execution(
153+
"datafusion failure".to_string()
154+
))
155+
.code()
156+
);
157+
assert_eq!(
158+
ServiceErrorCode::Arrow,
159+
ServiceError::Arrow(arrow::error::ArrowError::ExternalError(Box::new(
160+
std::io::Error::other("arrow failure")
161+
)))
162+
.code()
163+
);
164+
assert_eq!(
165+
ServiceErrorCode::Json,
166+
ServiceError::Json(serde_json::from_str::<serde_json::Value>("{").unwrap_err()).code()
167+
);
168+
assert_eq!(
169+
ServiceErrorCode::Internal,
170+
ServiceError::Internal("unexpected".to_string()).code()
171+
);
172+
}
173+
}

0 commit comments

Comments
 (0)