@databricks/zerobus-ingest-sdk 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +196 -107
- package/Cargo.toml +21 -21
- package/LICENSE +1 -1
- package/README.md +4 -5
- package/package.json +8 -7
- package/src/lib.rs +313 -265
package/src/lib.rs
CHANGED
|
@@ -13,22 +13,18 @@
|
|
|
13
13
|
#![deny(clippy::all)]
|
|
14
14
|
|
|
15
15
|
use napi::bindgen_prelude::*;
|
|
16
|
-
use napi::threadsafe_function::{
|
|
17
|
-
use napi::{Env,
|
|
16
|
+
use napi::threadsafe_function::{ErrorStrategy, ThreadsafeFunction};
|
|
17
|
+
use napi::{Env, JsFunction, JsGlobal, JsObject, JsString, JsUnknown, ValueType};
|
|
18
18
|
use napi_derive::napi;
|
|
19
19
|
|
|
20
|
+
use async_trait::async_trait;
|
|
21
|
+
use databricks_zerobus_ingest_sdk::databricks::zerobus::RecordType as RustRecordType;
|
|
20
22
|
use databricks_zerobus_ingest_sdk::{
|
|
21
|
-
EncodedRecord as RustRecordPayload,
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
DefaultTokenFactory, EncodedRecord as RustRecordPayload,
|
|
24
|
+
HeadersProvider as RustHeadersProvider, ZerobusError as RustZerobusError,
|
|
25
|
+
ZerobusResult as RustZerobusResult, ZerobusSdk as RustZerobusSdk,
|
|
24
26
|
ZerobusStream as RustZerobusStream,
|
|
25
|
-
HeadersProvider as RustHeadersProvider,
|
|
26
|
-
ZerobusResult as RustZerobusResult,
|
|
27
|
-
ZerobusError as RustZerobusError,
|
|
28
|
-
DefaultTokenFactory,
|
|
29
27
|
};
|
|
30
|
-
use databricks_zerobus_ingest_sdk::databricks::zerobus::RecordType as RustRecordType;
|
|
31
|
-
use async_trait::async_trait;
|
|
32
28
|
use prost_types;
|
|
33
29
|
use std::collections::HashMap;
|
|
34
30
|
use std::sync::Arc;
|
|
@@ -96,32 +92,6 @@ pub struct StreamConfigurationOptions {
|
|
|
96
92
|
pub stream_paused_max_wait_time_ms: Option<u32>,
|
|
97
93
|
}
|
|
98
94
|
|
|
99
|
-
impl From<StreamConfigurationOptions> for RustStreamOptions {
|
|
100
|
-
fn from(opts: StreamConfigurationOptions) -> Self {
|
|
101
|
-
let default = RustStreamOptions::default();
|
|
102
|
-
|
|
103
|
-
let record_type = match opts.record_type {
|
|
104
|
-
Some(0) => RustRecordType::Json,
|
|
105
|
-
Some(1) => RustRecordType::Proto,
|
|
106
|
-
_ => RustRecordType::Proto,
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
RustStreamOptions {
|
|
110
|
-
max_inflight_requests: opts.max_inflight_requests.unwrap_or(default.max_inflight_requests as u32) as usize,
|
|
111
|
-
recovery: opts.recovery.unwrap_or(default.recovery),
|
|
112
|
-
recovery_timeout_ms: opts.recovery_timeout_ms.map(|v| v as u64).unwrap_or(default.recovery_timeout_ms),
|
|
113
|
-
recovery_backoff_ms: opts.recovery_backoff_ms.map(|v| v as u64).unwrap_or(default.recovery_backoff_ms),
|
|
114
|
-
recovery_retries: opts.recovery_retries.unwrap_or(default.recovery_retries),
|
|
115
|
-
flush_timeout_ms: opts.flush_timeout_ms.map(|v| v as u64).unwrap_or(default.flush_timeout_ms),
|
|
116
|
-
server_lack_of_ack_timeout_ms: opts.server_lack_of_ack_timeout_ms.map(|v| v as u64).unwrap_or(default.server_lack_of_ack_timeout_ms),
|
|
117
|
-
record_type,
|
|
118
|
-
callback_max_wait_time_ms: None, // Callbacks not supported in TS SDK
|
|
119
|
-
stream_paused_max_wait_time_ms: opts.stream_paused_max_wait_time_ms.map(|v| v as u64),
|
|
120
|
-
ack_callback: None, // Callbacks not supported in TS SDK
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
95
|
/// Properties of the target Delta table for ingestion.
|
|
126
96
|
///
|
|
127
97
|
/// Specifies which Unity Catalog table to write to and optionally the schema descriptor
|
|
@@ -137,27 +107,6 @@ pub struct TableProperties {
|
|
|
137
107
|
pub descriptor_proto: Option<String>,
|
|
138
108
|
}
|
|
139
109
|
|
|
140
|
-
impl TableProperties {
|
|
141
|
-
fn to_rust(&self) -> Result<RustTableProperties> {
|
|
142
|
-
let descriptor: Option<prost_types::DescriptorProto> = if let Some(ref desc_str) = self.descriptor_proto {
|
|
143
|
-
let bytes = base64_decode(desc_str)
|
|
144
|
-
.map_err(|e| Error::from_reason(format!("Failed to decode descriptor: {}", e)))?;
|
|
145
|
-
|
|
146
|
-
let descriptor_proto: prost_types::DescriptorProto = prost::Message::decode(&bytes[..])
|
|
147
|
-
.map_err(|e| Error::from_reason(format!("Failed to parse descriptor proto: {}", e)))?;
|
|
148
|
-
|
|
149
|
-
Some(descriptor_proto)
|
|
150
|
-
} else {
|
|
151
|
-
None
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
Ok(RustTableProperties {
|
|
155
|
-
table_name: self.table_name.clone(),
|
|
156
|
-
descriptor_proto: descriptor,
|
|
157
|
-
})
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
110
|
/// Custom error type for Zerobus operations.
|
|
162
111
|
///
|
|
163
112
|
/// This error type includes information about whether the error is retryable,
|
|
@@ -183,6 +132,19 @@ impl ZerobusError {
|
|
|
183
132
|
}
|
|
184
133
|
}
|
|
185
134
|
|
|
135
|
+
/// Convert a JS `BigInt` to `i64`, erroring if it can't be represented losslessly.
|
|
136
|
+
/// Used by `waitForOffset` to avoid the precision loss of the old
|
|
137
|
+
/// `Number(bigint)` round-trip past 2^53.
|
|
138
|
+
fn bigint_to_i64(value: BigInt) -> Result<i64> {
|
|
139
|
+
let (n, lossless) = value.get_i64();
|
|
140
|
+
if !lossless {
|
|
141
|
+
return Err(Error::from_reason(
|
|
142
|
+
"offsetId exceeds i64 range; cannot be represented without loss",
|
|
143
|
+
));
|
|
144
|
+
}
|
|
145
|
+
Ok(n)
|
|
146
|
+
}
|
|
147
|
+
|
|
186
148
|
/// Helper function to convert a JavaScript value to a RustRecordPayload.
|
|
187
149
|
///
|
|
188
150
|
/// Supports:
|
|
@@ -209,17 +171,19 @@ fn convert_js_to_record_payload(env: &Env, payload: Unknown) -> Result<RustRecor
|
|
|
209
171
|
if constructor_obj.has_named_property("encode")? {
|
|
210
172
|
let encode_fn: JsFunction = constructor_obj.get_named_property("encode")?;
|
|
211
173
|
let obj_as_unknown = obj.into_unknown();
|
|
212
|
-
let encode_result: JsUnknown =
|
|
174
|
+
let encode_result: JsUnknown =
|
|
175
|
+
encode_fn.call::<JsUnknown>(Some(&constructor_obj), &[obj_as_unknown])?;
|
|
213
176
|
let encode_obj = JsObject::from_unknown(encode_result)?;
|
|
214
177
|
|
|
215
178
|
if encode_obj.has_named_property("finish")? {
|
|
216
179
|
let finish_fn: JsFunction = encode_obj.get_named_property("finish")?;
|
|
217
|
-
let buffer_result: JsUnknown =
|
|
180
|
+
let buffer_result: JsUnknown =
|
|
181
|
+
finish_fn.call::<JsUnknown>(Some(&encode_obj), &[])?;
|
|
218
182
|
let buffer: Buffer = Buffer::from_unknown(buffer_result)?;
|
|
219
183
|
Ok(RustRecordPayload::Proto(buffer.to_vec()))
|
|
220
184
|
} else {
|
|
221
185
|
Err(Error::from_reason(
|
|
222
|
-
"Protobuf message .encode() must return an object with .finish() method"
|
|
186
|
+
"Protobuf message .encode() must return an object with .finish() method",
|
|
223
187
|
))
|
|
224
188
|
}
|
|
225
189
|
} else {
|
|
@@ -227,7 +191,8 @@ fn convert_js_to_record_payload(env: &Env, payload: Unknown) -> Result<RustRecor
|
|
|
227
191
|
let json_obj: JsObject = global.get_named_property("JSON")?;
|
|
228
192
|
let stringify: JsFunction = json_obj.get_named_property("stringify")?;
|
|
229
193
|
let obj_as_unknown = obj.into_unknown();
|
|
230
|
-
let str_result: JsUnknown =
|
|
194
|
+
let str_result: JsUnknown =
|
|
195
|
+
stringify.call::<JsUnknown>(Some(&json_obj), &[obj_as_unknown])?;
|
|
231
196
|
let js_string = JsString::from_unknown(str_result)?;
|
|
232
197
|
let json_string = js_string.into_utf8()?.as_str()?.to_string();
|
|
233
198
|
|
|
@@ -240,11 +205,9 @@ fn convert_js_to_record_payload(env: &Env, payload: Unknown) -> Result<RustRecor
|
|
|
240
205
|
let json_string = js_string.into_utf8()?.as_str()?.to_string();
|
|
241
206
|
Ok(RustRecordPayload::Json(json_string))
|
|
242
207
|
}
|
|
243
|
-
_ =>
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
))
|
|
247
|
-
}
|
|
208
|
+
_ => Err(Error::from_reason(
|
|
209
|
+
"Payload must be a Buffer, string, protobuf message object, or plain JavaScript object",
|
|
210
|
+
)),
|
|
248
211
|
}
|
|
249
212
|
}
|
|
250
213
|
|
|
@@ -309,35 +272,37 @@ impl ZerobusStream {
|
|
|
309
272
|
#[allow(deprecated)]
|
|
310
273
|
pub fn ingest_record(&self, env: Env, payload: Unknown) -> Result<JsObject> {
|
|
311
274
|
let record_payload = convert_js_to_record_payload(&env, payload)?;
|
|
312
|
-
|
|
313
|
-
let ack_future = {
|
|
314
|
-
let handle = tokio::runtime::Handle::current();
|
|
315
|
-
let stream = self.inner.clone();
|
|
316
|
-
|
|
317
|
-
handle.block_on(async move {
|
|
318
|
-
let mut guard = stream.lock().await;
|
|
319
|
-
let stream_ref = guard
|
|
320
|
-
.as_mut()
|
|
321
|
-
.ok_or_else(|| Error::from_reason("Stream has been closed"))?;
|
|
322
|
-
|
|
323
|
-
stream_ref
|
|
324
|
-
.ingest_record(record_payload)
|
|
325
|
-
.await
|
|
326
|
-
.map_err(|e| Error::from_reason(format!("Failed to ingest record: {}", e)))
|
|
327
|
-
})?
|
|
328
|
-
};
|
|
275
|
+
let stream = self.inner.clone();
|
|
329
276
|
|
|
330
277
|
env.execute_tokio_future(
|
|
331
278
|
async move {
|
|
332
|
-
|
|
333
|
-
.await
|
|
334
|
-
|
|
279
|
+
let offset = {
|
|
280
|
+
let mut guard = stream.lock().await;
|
|
281
|
+
let stream_ref = guard
|
|
282
|
+
.as_mut()
|
|
283
|
+
.ok_or_else(|| napi::Error::from_reason("Stream has been closed"))?;
|
|
284
|
+
stream_ref
|
|
285
|
+
.ingest_record_offset(record_payload)
|
|
286
|
+
.await
|
|
287
|
+
.map_err(|e| {
|
|
288
|
+
napi::Error::from_reason(format!("Failed to ingest record: {}", e))
|
|
289
|
+
})?
|
|
290
|
+
};
|
|
291
|
+
{
|
|
292
|
+
let guard = stream.lock().await;
|
|
293
|
+
let stream_ref = guard
|
|
294
|
+
.as_ref()
|
|
295
|
+
.ok_or_else(|| napi::Error::from_reason("Stream has been closed"))?;
|
|
296
|
+
stream_ref.wait_for_offset(offset).await.map_err(|e| {
|
|
297
|
+
napi::Error::from_reason(format!("Acknowledgment failed: {}", e))
|
|
298
|
+
})?;
|
|
299
|
+
}
|
|
300
|
+
Ok(offset)
|
|
335
301
|
},
|
|
336
|
-
|env,
|
|
337
|
-
let result_str = result.to_string();
|
|
302
|
+
|env, offset_id| {
|
|
338
303
|
let global: JsGlobal = env.get_global()?;
|
|
339
304
|
let bigint_ctor: JsFunction = global.get_named_property("BigInt")?;
|
|
340
|
-
let js_str = env.create_string(&
|
|
305
|
+
let js_str = env.create_string(&offset_id.to_string())?;
|
|
341
306
|
bigint_ctor.call(None, &[js_str.into_unknown()])
|
|
342
307
|
},
|
|
343
308
|
)
|
|
@@ -378,50 +343,49 @@ impl ZerobusStream {
|
|
|
378
343
|
#[napi(ts_return_type = "Promise<bigint | null>")]
|
|
379
344
|
#[allow(deprecated)]
|
|
380
345
|
pub fn ingest_records(&self, env: Env, records: Vec<Unknown>) -> Result<JsObject> {
|
|
346
|
+
// Rust SDK 2.0 removed the blocking `ingest_records`. v1 semantics
|
|
347
|
+
// (Promise resolves after server ack; `null` for empty batches) are
|
|
348
|
+
// preserved via `ingest_records_offset` + `wait_for_offset`.
|
|
381
349
|
let record_payloads: Result<Vec<RustRecordPayload>> = records
|
|
382
350
|
.into_iter()
|
|
383
351
|
.map(|payload| convert_js_to_record_payload(&env, payload))
|
|
384
352
|
.collect();
|
|
385
|
-
|
|
386
353
|
let record_payloads = record_payloads?;
|
|
387
|
-
|
|
388
|
-
let ack_future_option = {
|
|
389
|
-
let handle = tokio::runtime::Handle::current();
|
|
390
|
-
let stream = self.inner.clone();
|
|
391
|
-
|
|
392
|
-
handle.block_on(async move {
|
|
393
|
-
let mut guard = stream.lock().await;
|
|
394
|
-
let stream_ref = guard
|
|
395
|
-
.as_mut()
|
|
396
|
-
.ok_or_else(|| Error::from_reason("Stream has been closed"))?;
|
|
397
|
-
|
|
398
|
-
// Send batch to SDK
|
|
399
|
-
stream_ref
|
|
400
|
-
.ingest_records(record_payloads)
|
|
401
|
-
.await
|
|
402
|
-
.map_err(|e| Error::from_reason(format!("Failed to ingest batch: {}", e)))
|
|
403
|
-
})?
|
|
404
|
-
};
|
|
354
|
+
let stream = self.inner.clone();
|
|
405
355
|
|
|
406
356
|
env.execute_tokio_future(
|
|
407
357
|
async move {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
358
|
+
let offset_opt = {
|
|
359
|
+
let mut guard = stream.lock().await;
|
|
360
|
+
let stream_ref = guard
|
|
361
|
+
.as_mut()
|
|
362
|
+
.ok_or_else(|| napi::Error::from_reason("Stream has been closed"))?;
|
|
363
|
+
stream_ref
|
|
364
|
+
.ingest_records_offset(record_payloads)
|
|
365
|
+
.await
|
|
366
|
+
.map_err(|e| {
|
|
367
|
+
napi::Error::from_reason(format!("Failed to ingest batch: {}", e))
|
|
368
|
+
})?
|
|
369
|
+
};
|
|
370
|
+
if let Some(offset) = offset_opt {
|
|
371
|
+
let guard = stream.lock().await;
|
|
372
|
+
let stream_ref = guard
|
|
373
|
+
.as_ref()
|
|
374
|
+
.ok_or_else(|| napi::Error::from_reason("Stream has been closed"))?;
|
|
375
|
+
stream_ref.wait_for_offset(offset).await.map_err(|e| {
|
|
376
|
+
napi::Error::from_reason(format!("Batch acknowledgment failed: {}", e))
|
|
377
|
+
})?;
|
|
414
378
|
}
|
|
379
|
+
Ok(offset_opt)
|
|
415
380
|
},
|
|
416
381
|
|env, result| match result {
|
|
417
382
|
Some(offset_id) => {
|
|
418
|
-
let offset_str = offset_id.to_string();
|
|
419
383
|
let global: JsGlobal = env.get_global()?;
|
|
420
384
|
let bigint_ctor: JsFunction = global.get_named_property("BigInt")?;
|
|
421
|
-
let js_str = env.create_string(&
|
|
385
|
+
let js_str = env.create_string(&offset_id.to_string())?;
|
|
422
386
|
let bigint = bigint_ctor.call(None, &[js_str.into_unknown()])?;
|
|
423
387
|
Ok(bigint.into_unknown())
|
|
424
|
-
}
|
|
388
|
+
}
|
|
425
389
|
None => env.get_null().map(|v| v.into_unknown()),
|
|
426
390
|
},
|
|
427
391
|
)
|
|
@@ -470,7 +434,9 @@ impl ZerobusStream {
|
|
|
470
434
|
stream_ref
|
|
471
435
|
.ingest_record_offset(record_payload)
|
|
472
436
|
.await
|
|
473
|
-
.map_err(|e|
|
|
437
|
+
.map_err(|e| {
|
|
438
|
+
napi::Error::from_reason(format!("Failed to ingest record: {}", e))
|
|
439
|
+
})
|
|
474
440
|
},
|
|
475
441
|
|env, offset_id| {
|
|
476
442
|
let offset_str = offset_id.to_string();
|
|
@@ -537,7 +503,7 @@ impl ZerobusStream {
|
|
|
537
503
|
let js_str = env.create_string(&offset_str)?;
|
|
538
504
|
let bigint = bigint_ctor.call(None, &[js_str.into_unknown()])?;
|
|
539
505
|
Ok(bigint.into_unknown())
|
|
540
|
-
}
|
|
506
|
+
}
|
|
541
507
|
None => env.get_null().map(|v| v.into_unknown()),
|
|
542
508
|
},
|
|
543
509
|
)
|
|
@@ -568,12 +534,9 @@ impl ZerobusStream {
|
|
|
568
534
|
/// // Wait for the last offset (implies all previous are also acknowledged)
|
|
569
535
|
/// await stream.waitForOffset(offsets[offsets.length - 1]);
|
|
570
536
|
/// ```
|
|
571
|
-
#[napi(
|
|
572
|
-
pub fn wait_for_offset(&self, env: Env, offset_id:
|
|
573
|
-
let
|
|
574
|
-
let number_ctor: JsFunction = global.get_named_property("Number")?;
|
|
575
|
-
let num_result: JsUnknown = number_ctor.call(None, &[offset_id])?;
|
|
576
|
-
let offset: i64 = num_result.coerce_to_number()?.get_int64()?;
|
|
537
|
+
#[napi(ts_return_type = "Promise<void>")]
|
|
538
|
+
pub fn wait_for_offset(&self, env: Env, offset_id: BigInt) -> Result<JsObject> {
|
|
539
|
+
let offset = bigint_to_i64(offset_id)?;
|
|
577
540
|
|
|
578
541
|
let stream = self.inner.clone();
|
|
579
542
|
|
|
@@ -584,10 +547,9 @@ impl ZerobusStream {
|
|
|
584
547
|
.as_ref()
|
|
585
548
|
.ok_or_else(|| napi::Error::from_reason("Stream has been closed"))?;
|
|
586
549
|
|
|
587
|
-
stream_ref
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
.map_err(|e| napi::Error::from_reason(format!("Failed to wait for offset: {}", e)))
|
|
550
|
+
stream_ref.wait_for_offset(offset).await.map_err(|e| {
|
|
551
|
+
napi::Error::from_reason(format!("Failed to wait for offset: {}", e))
|
|
552
|
+
})
|
|
591
553
|
},
|
|
592
554
|
|_env, _| Ok(()),
|
|
593
555
|
)
|
|
@@ -750,12 +712,12 @@ impl StaticHeadersProvider {
|
|
|
750
712
|
|
|
751
713
|
if !map.contains_key("authorization") {
|
|
752
714
|
return Err(RustZerobusError::InvalidArgument(
|
|
753
|
-
"HeadersProvider must include 'authorization' header with Bearer token".to_string()
|
|
715
|
+
"HeadersProvider must include 'authorization' header with Bearer token".to_string(),
|
|
754
716
|
));
|
|
755
717
|
}
|
|
756
718
|
if !map.contains_key("x-databricks-zerobus-table-name") {
|
|
757
719
|
return Err(RustZerobusError::InvalidArgument(
|
|
758
|
-
"HeadersProvider must include 'x-databricks-zerobus-table-name' header".to_string()
|
|
720
|
+
"HeadersProvider must include 'x-databricks-zerobus-table-name' header".to_string(),
|
|
759
721
|
));
|
|
760
722
|
}
|
|
761
723
|
|
|
@@ -776,13 +738,18 @@ impl RustHeadersProvider for StaticHeadersProvider {
|
|
|
776
738
|
}
|
|
777
739
|
|
|
778
740
|
/// Helper to create a threadsafe function from JavaScript callback
|
|
779
|
-
fn create_headers_tsfn(
|
|
741
|
+
fn create_headers_tsfn(
|
|
742
|
+
js_func: JsFunction,
|
|
743
|
+
) -> Result<ThreadsafeFunction<(), ErrorStrategy::Fatal>> {
|
|
780
744
|
js_func.create_threadsafe_function(0, |ctx| Ok(vec![ctx.value]))
|
|
781
745
|
}
|
|
782
746
|
|
|
783
747
|
/// Helper to call headers callback and get result
|
|
784
|
-
async fn call_headers_tsfn(
|
|
785
|
-
|
|
748
|
+
async fn call_headers_tsfn(
|
|
749
|
+
tsfn: ThreadsafeFunction<(), ErrorStrategy::Fatal>,
|
|
750
|
+
) -> Result<Vec<(String, String)>> {
|
|
751
|
+
let raw_headers: Vec<Vec<String>> = tsfn
|
|
752
|
+
.call_async(())
|
|
786
753
|
.await
|
|
787
754
|
.map_err(|e| Error::from_reason(format!("Failed to call headers callback: {}", e)))?;
|
|
788
755
|
|
|
@@ -898,7 +865,9 @@ impl ZerobusSdk {
|
|
|
898
865
|
.and_then(|s| s.split('.').next())
|
|
899
866
|
.map(|s| s.to_string())
|
|
900
867
|
.ok_or_else(|| {
|
|
901
|
-
Error::from_reason(
|
|
868
|
+
Error::from_reason(
|
|
869
|
+
"Failed to extract workspace_id from zerobus_endpoint".to_string(),
|
|
870
|
+
)
|
|
902
871
|
})?;
|
|
903
872
|
|
|
904
873
|
let inner = RustZerobusSdk::builder()
|
|
@@ -975,13 +944,45 @@ impl ZerobusSdk {
|
|
|
975
944
|
options: Option<StreamConfigurationOptions>,
|
|
976
945
|
headers_provider: Option<JsHeadersProvider>,
|
|
977
946
|
) -> Result<JsObject> {
|
|
978
|
-
|
|
979
|
-
|
|
947
|
+
// Decode the optional protobuf descriptor up-front so we can hand it
|
|
948
|
+
// to the builder's `.compiled_proto()` setter; the builder constructs
|
|
949
|
+
// the (now-private) `TableProperties` itself.
|
|
950
|
+
let descriptor_proto: Option<prost_types::DescriptorProto> =
|
|
951
|
+
if let Some(ref desc_str) = table_properties.descriptor_proto {
|
|
952
|
+
let bytes = base64_decode(desc_str).map_err(|e| {
|
|
953
|
+
Error::from_reason(format!("Failed to decode descriptor: {}", e))
|
|
954
|
+
})?;
|
|
955
|
+
let dp: prost_types::DescriptorProto = prost::Message::decode(&bytes[..])
|
|
956
|
+
.map_err(|e| {
|
|
957
|
+
Error::from_reason(format!("Failed to parse descriptor proto: {}", e))
|
|
958
|
+
})?;
|
|
959
|
+
Some(dp)
|
|
960
|
+
} else {
|
|
961
|
+
None
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
let opts = options.unwrap_or(StreamConfigurationOptions {
|
|
965
|
+
max_inflight_requests: None,
|
|
966
|
+
recovery: None,
|
|
967
|
+
recovery_timeout_ms: None,
|
|
968
|
+
recovery_backoff_ms: None,
|
|
969
|
+
recovery_retries: None,
|
|
970
|
+
flush_timeout_ms: None,
|
|
971
|
+
server_lack_of_ack_timeout_ms: None,
|
|
972
|
+
record_type: None,
|
|
973
|
+
stream_paused_max_wait_time_ms: None,
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
let record_type = match opts.record_type {
|
|
977
|
+
Some(0) => RustRecordType::Json,
|
|
978
|
+
Some(1) => RustRecordType::Proto,
|
|
979
|
+
_ => RustRecordType::Proto,
|
|
980
|
+
};
|
|
980
981
|
|
|
981
982
|
let headers_tsfn = match headers_provider {
|
|
982
|
-
Some(JsHeadersProvider {
|
|
983
|
-
|
|
984
|
-
}
|
|
983
|
+
Some(JsHeadersProvider {
|
|
984
|
+
get_headers_callback,
|
|
985
|
+
}) => Some(create_headers_tsfn(get_headers_callback)?),
|
|
985
986
|
None => None,
|
|
986
987
|
};
|
|
987
988
|
|
|
@@ -992,34 +993,70 @@ impl ZerobusSdk {
|
|
|
992
993
|
|
|
993
994
|
env.execute_tokio_future(
|
|
994
995
|
async move {
|
|
995
|
-
let headers_provider_arc: Arc<dyn RustHeadersProvider> = if let Some(tsfn) =
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
996
|
+
let headers_provider_arc: Arc<dyn RustHeadersProvider> = if let Some(tsfn) =
|
|
997
|
+
headers_tsfn
|
|
998
|
+
{
|
|
999
|
+
let headers = call_headers_tsfn(tsfn).await.map_err(|e| {
|
|
1000
|
+
napi::Error::from_reason(format!("Headers callback failed: {}", e))
|
|
1001
|
+
})?;
|
|
1000
1002
|
let static_provider = StaticHeadersProvider::new(headers)
|
|
1001
1003
|
.map_err(|e| napi::Error::from_reason(format!("Invalid headers: {}", e)))?;
|
|
1002
|
-
|
|
1003
1004
|
Arc::new(static_provider)
|
|
1004
1005
|
} else {
|
|
1005
|
-
// Default OAuth with TS user agent
|
|
1006
1006
|
Arc::new(TsOAuthHeadersProvider::new(
|
|
1007
1007
|
client_id,
|
|
1008
1008
|
client_secret,
|
|
1009
|
-
table_name,
|
|
1009
|
+
table_name.clone(),
|
|
1010
1010
|
workspace_id,
|
|
1011
1011
|
unity_catalog_url,
|
|
1012
1012
|
))
|
|
1013
1013
|
};
|
|
1014
1014
|
|
|
1015
|
-
let
|
|
1016
|
-
.
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
.
|
|
1022
|
-
|
|
1015
|
+
let mut builder = sdk
|
|
1016
|
+
.stream_builder()
|
|
1017
|
+
.table(table_name)
|
|
1018
|
+
.headers_provider(headers_provider_arc);
|
|
1019
|
+
|
|
1020
|
+
if let Some(v) = opts.max_inflight_requests {
|
|
1021
|
+
builder = builder.max_inflight_requests(v as usize);
|
|
1022
|
+
}
|
|
1023
|
+
if let Some(v) = opts.recovery {
|
|
1024
|
+
builder = builder.recovery(v);
|
|
1025
|
+
}
|
|
1026
|
+
if let Some(v) = opts.recovery_timeout_ms {
|
|
1027
|
+
builder = builder.recovery_timeout_ms(v as u64);
|
|
1028
|
+
}
|
|
1029
|
+
if let Some(v) = opts.recovery_backoff_ms {
|
|
1030
|
+
builder = builder.recovery_backoff_ms(v as u64);
|
|
1031
|
+
}
|
|
1032
|
+
if let Some(v) = opts.recovery_retries {
|
|
1033
|
+
builder = builder.recovery_retries(v);
|
|
1034
|
+
}
|
|
1035
|
+
if let Some(v) = opts.flush_timeout_ms {
|
|
1036
|
+
builder = builder.flush_timeout_ms(v as u64);
|
|
1037
|
+
}
|
|
1038
|
+
if let Some(v) = opts.server_lack_of_ack_timeout_ms {
|
|
1039
|
+
builder = builder.server_lack_of_ack_timeout_ms(v as u64);
|
|
1040
|
+
}
|
|
1041
|
+
if let Some(v) = opts.stream_paused_max_wait_time_ms {
|
|
1042
|
+
builder = builder.stream_paused_max_wait_time_ms(Some(v as u64));
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
let builder = match record_type {
|
|
1046
|
+
RustRecordType::Json => builder.json(),
|
|
1047
|
+
RustRecordType::Proto | RustRecordType::Unspecified => {
|
|
1048
|
+
let desc = descriptor_proto.ok_or_else(|| {
|
|
1049
|
+
napi::Error::from_reason(
|
|
1050
|
+
"Proto record type requires descriptor_proto on TableProperties",
|
|
1051
|
+
)
|
|
1052
|
+
})?;
|
|
1053
|
+
builder.compiled_proto(desc)
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
let stream = builder.build().await.map_err(|e| {
|
|
1058
|
+
napi::Error::from_reason(format!("Failed to create stream: {}", e))
|
|
1059
|
+
})?;
|
|
1023
1060
|
|
|
1024
1061
|
Ok(ZerobusStream {
|
|
1025
1062
|
inner: Arc::new(Mutex::new(Some(stream))),
|
|
@@ -1091,31 +1128,24 @@ fn base64_decode(input: &str) -> std::result::Result<Vec<u8>, String> {
|
|
|
1091
1128
|
}
|
|
1092
1129
|
|
|
1093
1130
|
// =============================================================================
|
|
1094
|
-
// Arrow Flight Support (
|
|
1131
|
+
// Arrow Flight Support (Beta)
|
|
1095
1132
|
// Enabled with feature flag: cargo build --features arrow-flight
|
|
1096
1133
|
// =============================================================================
|
|
1097
1134
|
|
|
1098
|
-
#[cfg(feature = "arrow-flight")]
|
|
1099
|
-
use databricks_zerobus_ingest_sdk::{
|
|
1100
|
-
ArrowStreamConfigurationOptions as RustArrowStreamOptions,
|
|
1101
|
-
ArrowTableProperties as RustArrowTableProperties,
|
|
1102
|
-
ZerobusArrowStream as RustZerobusArrowStream,
|
|
1103
|
-
ArrowSchema as RustArrowSchema,
|
|
1104
|
-
RecordBatch as RustRecordBatch,
|
|
1105
|
-
Field as RustField,
|
|
1106
|
-
DataType as RustDataType,
|
|
1107
|
-
};
|
|
1108
|
-
#[cfg(feature = "arrow-flight")]
|
|
1109
|
-
use arrow_ipc::reader::StreamReader;
|
|
1110
1135
|
#[cfg(feature = "arrow-flight")]
|
|
1111
1136
|
use arrow_ipc::writer::StreamWriter;
|
|
1112
1137
|
#[cfg(feature = "arrow-flight")]
|
|
1113
|
-
use
|
|
1138
|
+
use bytes::Bytes;
|
|
1139
|
+
#[cfg(feature = "arrow-flight")]
|
|
1140
|
+
use databricks_zerobus_ingest_sdk::{
|
|
1141
|
+
ArrowSchema as RustArrowSchema, DataType as RustDataType, Field as RustField,
|
|
1142
|
+
RecordBatch as RustRecordBatch, ZerobusArrowStream as RustZerobusArrowStream,
|
|
1143
|
+
};
|
|
1114
1144
|
|
|
1115
1145
|
/// IPC compression type for Arrow Flight streams.
|
|
1116
1146
|
///
|
|
1117
|
-
/// **
|
|
1118
|
-
///
|
|
1147
|
+
/// **Beta**: Arrow Flight support is in Beta. The API is stabilising but
|
|
1148
|
+
/// may still change before reaching GA.
|
|
1119
1149
|
#[cfg(feature = "arrow-flight")]
|
|
1120
1150
|
#[napi]
|
|
1121
1151
|
pub enum IpcCompressionType {
|
|
@@ -1127,8 +1157,8 @@ pub enum IpcCompressionType {
|
|
|
1127
1157
|
|
|
1128
1158
|
/// Configuration options for Arrow Flight streams.
|
|
1129
1159
|
///
|
|
1130
|
-
/// **
|
|
1131
|
-
///
|
|
1160
|
+
/// **Beta**: Arrow Flight support is in Beta. The API is stabilising but
|
|
1161
|
+
/// may still change before reaching GA.
|
|
1132
1162
|
#[cfg(feature = "arrow-flight")]
|
|
1133
1163
|
#[napi(object)]
|
|
1134
1164
|
#[derive(Debug, Clone)]
|
|
@@ -1169,34 +1199,23 @@ pub struct ArrowStreamConfigurationOptions {
|
|
|
1169
1199
|
pub ipc_compression: Option<i32>,
|
|
1170
1200
|
}
|
|
1171
1201
|
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
let ipc_compression = match opts.ipc_compression {
|
|
1178
|
-
Some(0) => Some(arrow_ipc::CompressionType::LZ4_FRAME),
|
|
1179
|
-
Some(1) => Some(arrow_ipc::CompressionType::ZSTD),
|
|
1180
|
-
_ => None,
|
|
1181
|
-
};
|
|
1202
|
+
// Rust SDK 2.0 `ArrowStreamConfigurationOptions` is `#[non_exhaustive]` and
|
|
1203
|
+
// cannot be constructed via struct literal from this crate. Arrow options are
|
|
1204
|
+
// applied via setters on `sdk.stream_builder()` inside `create_arrow_stream`
|
|
1205
|
+
// below.
|
|
1182
1206
|
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
server_lack_of_ack_timeout_ms: opts.server_lack_of_ack_timeout_ms.map(|v| v as u64).unwrap_or(default.server_lack_of_ack_timeout_ms),
|
|
1190
|
-
flush_timeout_ms: opts.flush_timeout_ms.map(|v| v as u64).unwrap_or(default.flush_timeout_ms),
|
|
1191
|
-
connection_timeout_ms: opts.connection_timeout_ms.map(|v| v as u64).unwrap_or(default.connection_timeout_ms),
|
|
1192
|
-
ipc_compression,
|
|
1193
|
-
}
|
|
1207
|
+
#[cfg(feature = "arrow-flight")]
|
|
1208
|
+
fn map_ipc_compression(value: Option<i32>) -> Option<arrow_ipc::CompressionType> {
|
|
1209
|
+
match value {
|
|
1210
|
+
Some(0) => Some(arrow_ipc::CompressionType::LZ4_FRAME),
|
|
1211
|
+
Some(1) => Some(arrow_ipc::CompressionType::ZSTD),
|
|
1212
|
+
_ => None,
|
|
1194
1213
|
}
|
|
1195
1214
|
}
|
|
1196
1215
|
|
|
1197
1216
|
/// Arrow data type enum for schema definition.
|
|
1198
1217
|
///
|
|
1199
|
-
/// **
|
|
1218
|
+
/// **Beta**: Arrow Flight support is in Beta.
|
|
1200
1219
|
#[cfg(feature = "arrow-flight")]
|
|
1201
1220
|
#[napi]
|
|
1202
1221
|
pub enum ArrowDataType {
|
|
@@ -1268,7 +1287,7 @@ fn convert_arrow_data_type(dt: i32) -> RustDataType {
|
|
|
1268
1287
|
|
|
1269
1288
|
/// Arrow field definition for schema.
|
|
1270
1289
|
///
|
|
1271
|
-
/// **
|
|
1290
|
+
/// **Beta**: Arrow Flight support is in Beta.
|
|
1272
1291
|
#[cfg(feature = "arrow-flight")]
|
|
1273
1292
|
#[napi(object)]
|
|
1274
1293
|
#[derive(Debug, Clone)]
|
|
@@ -1286,8 +1305,8 @@ pub struct ArrowField {
|
|
|
1286
1305
|
/// Unlike `TableProperties` which uses Protocol Buffers, Arrow Flight streams
|
|
1287
1306
|
/// require an Arrow schema definition.
|
|
1288
1307
|
///
|
|
1289
|
-
/// **
|
|
1290
|
-
///
|
|
1308
|
+
/// **Beta**: Arrow Flight support is in Beta. The API is stabilising but
|
|
1309
|
+
/// may still change before reaching GA.
|
|
1291
1310
|
#[cfg(feature = "arrow-flight")]
|
|
1292
1311
|
#[napi(object)]
|
|
1293
1312
|
#[derive(Debug, Clone)]
|
|
@@ -1298,24 +1317,22 @@ pub struct ArrowTableProperties {
|
|
|
1298
1317
|
pub schema_fields: Vec<ArrowField>,
|
|
1299
1318
|
}
|
|
1300
1319
|
|
|
1320
|
+
// Rust SDK 2.0 made `ArrowTableProperties` non-public; the table name and
|
|
1321
|
+
// schema are passed to `sdk.stream_builder().table(...).arrow(schema)`
|
|
1322
|
+
// directly. This helper builds just the Arrow `Schema`.
|
|
1301
1323
|
#[cfg(feature = "arrow-flight")]
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1324
|
+
fn build_arrow_schema(fields: &[ArrowField]) -> Arc<RustArrowSchema> {
|
|
1325
|
+
let rust_fields: Vec<RustField> = fields
|
|
1326
|
+
.iter()
|
|
1327
|
+
.map(|f| {
|
|
1305
1328
|
RustField::new(
|
|
1306
1329
|
&f.name,
|
|
1307
1330
|
convert_arrow_data_type(f.data_type),
|
|
1308
1331
|
f.nullable.unwrap_or(true),
|
|
1309
1332
|
)
|
|
1310
|
-
}).collect();
|
|
1311
|
-
|
|
1312
|
-
let schema = Arc::new(RustArrowSchema::new(fields));
|
|
1313
|
-
|
|
1314
|
-
Ok(RustArrowTableProperties {
|
|
1315
|
-
table_name: self.table_name.clone(),
|
|
1316
|
-
schema,
|
|
1317
1333
|
})
|
|
1318
|
-
|
|
1334
|
+
.collect();
|
|
1335
|
+
Arc::new(RustArrowSchema::new(rust_fields))
|
|
1319
1336
|
}
|
|
1320
1337
|
|
|
1321
1338
|
/// An Arrow Flight stream for ingesting Arrow RecordBatches into a Delta table.
|
|
@@ -1323,8 +1340,8 @@ impl ArrowTableProperties {
|
|
|
1323
1340
|
/// This stream provides a high-performance interface for streaming Arrow data
|
|
1324
1341
|
/// to Databricks Delta tables using the Arrow Flight protocol.
|
|
1325
1342
|
///
|
|
1326
|
-
/// **
|
|
1327
|
-
///
|
|
1343
|
+
/// **Beta**: Arrow Flight support is in Beta. The API is stabilising but
|
|
1344
|
+
/// may still change before reaching GA.
|
|
1328
1345
|
///
|
|
1329
1346
|
/// # Lifecycle
|
|
1330
1347
|
///
|
|
@@ -1358,26 +1375,6 @@ pub struct ZerobusArrowStream {
|
|
|
1358
1375
|
schema: Arc<RustArrowSchema>,
|
|
1359
1376
|
}
|
|
1360
1377
|
|
|
1361
|
-
/// Helper to parse Arrow IPC buffer to RecordBatch
|
|
1362
|
-
#[cfg(feature = "arrow-flight")]
|
|
1363
|
-
fn parse_arrow_ipc_to_batch(ipc_buffer: &[u8], _expected_schema: &RustArrowSchema) -> Result<RustRecordBatch> {
|
|
1364
|
-
let cursor = Cursor::new(ipc_buffer);
|
|
1365
|
-
let reader = StreamReader::try_new(cursor, None)
|
|
1366
|
-
.map_err(|e| Error::from_reason(format!("Failed to parse Arrow IPC: {}", e)))?;
|
|
1367
|
-
|
|
1368
|
-
// Collect all batches (typically just one)
|
|
1369
|
-
let batches: Vec<RustRecordBatch> = reader
|
|
1370
|
-
.filter_map(|r| r.ok())
|
|
1371
|
-
.collect();
|
|
1372
|
-
|
|
1373
|
-
if batches.is_empty() {
|
|
1374
|
-
return Err(Error::from_reason("Arrow IPC buffer contains no record batches"));
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
// Return the first batch (or could concatenate if multiple)
|
|
1378
|
-
Ok(batches.into_iter().next().unwrap())
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
1378
|
/// Helper to serialize RecordBatch to Arrow IPC buffer
|
|
1382
1379
|
#[cfg(feature = "arrow-flight")]
|
|
1383
1380
|
fn serialize_batch_to_ipc(batch: &RustRecordBatch) -> Result<Vec<u8>> {
|
|
@@ -1385,9 +1382,11 @@ fn serialize_batch_to_ipc(batch: &RustRecordBatch) -> Result<Vec<u8>> {
|
|
|
1385
1382
|
{
|
|
1386
1383
|
let mut writer = StreamWriter::try_new(&mut buffer, batch.schema().as_ref())
|
|
1387
1384
|
.map_err(|e| Error::from_reason(format!("Failed to create Arrow IPC writer: {}", e)))?;
|
|
1388
|
-
writer
|
|
1385
|
+
writer
|
|
1386
|
+
.write(batch)
|
|
1389
1387
|
.map_err(|e| Error::from_reason(format!("Failed to write batch to IPC: {}", e)))?;
|
|
1390
|
-
writer
|
|
1388
|
+
writer
|
|
1389
|
+
.finish()
|
|
1391
1390
|
.map_err(|e| Error::from_reason(format!("Failed to finish IPC stream: {}", e)))?;
|
|
1392
1391
|
}
|
|
1393
1392
|
Ok(buffer)
|
|
@@ -1423,29 +1422,30 @@ impl ZerobusArrowStream {
|
|
|
1423
1422
|
/// ```
|
|
1424
1423
|
#[napi(ts_return_type = "Promise<bigint>")]
|
|
1425
1424
|
pub fn ingest_batch(&self, env: Env, ipc_buffer: Buffer) -> Result<JsObject> {
|
|
1426
|
-
|
|
1425
|
+
// The Rust SDK's `ingest_ipc_batch` materialises the bytes into a
|
|
1426
|
+
// `RecordBatch`, rejects multi-batch streams, and validates the schema
|
|
1427
|
+
// against the stream's schema. No need to duplicate any of that here.
|
|
1427
1428
|
let stream = self.inner.clone();
|
|
1428
1429
|
let buffer_vec = ipc_buffer.to_vec();
|
|
1429
1430
|
|
|
1430
1431
|
env.execute_tokio_future(
|
|
1431
1432
|
async move {
|
|
1432
|
-
let batch = parse_arrow_ipc_to_batch(&buffer_vec, &schema)?;
|
|
1433
|
-
|
|
1434
1433
|
let mut guard = stream.lock().await;
|
|
1435
1434
|
let stream_ref = guard
|
|
1436
1435
|
.as_mut()
|
|
1437
1436
|
.ok_or_else(|| napi::Error::from_reason("Arrow stream has been closed"))?;
|
|
1438
1437
|
|
|
1439
1438
|
stream_ref
|
|
1440
|
-
.
|
|
1439
|
+
.ingest_ipc_batch(Bytes::from(buffer_vec))
|
|
1441
1440
|
.await
|
|
1442
|
-
.map_err(|e|
|
|
1441
|
+
.map_err(|e| {
|
|
1442
|
+
napi::Error::from_reason(format!("Failed to ingest batch: {}", e))
|
|
1443
|
+
})
|
|
1443
1444
|
},
|
|
1444
1445
|
|env, offset_id| {
|
|
1445
|
-
let offset_str = offset_id.to_string();
|
|
1446
1446
|
let global: JsGlobal = env.get_global()?;
|
|
1447
1447
|
let bigint_ctor: JsFunction = global.get_named_property("BigInt")?;
|
|
1448
|
-
let js_str = env.create_string(&
|
|
1448
|
+
let js_str = env.create_string(&offset_id.to_string())?;
|
|
1449
1449
|
bigint_ctor.call(None, &[js_str.into_unknown()])
|
|
1450
1450
|
},
|
|
1451
1451
|
)
|
|
@@ -1458,12 +1458,9 @@ impl ZerobusArrowStream {
|
|
|
1458
1458
|
/// # Arguments
|
|
1459
1459
|
///
|
|
1460
1460
|
/// * `offset_id` - The offset ID to wait for (returned by ingestBatch)
|
|
1461
|
-
#[napi(
|
|
1462
|
-
pub fn wait_for_offset(&self, env: Env, offset_id:
|
|
1463
|
-
let
|
|
1464
|
-
let number_ctor: JsFunction = global.get_named_property("Number")?;
|
|
1465
|
-
let num_result: JsUnknown = number_ctor.call(None, &[offset_id])?;
|
|
1466
|
-
let offset: i64 = num_result.coerce_to_number()?.get_int64()?;
|
|
1461
|
+
#[napi(ts_return_type = "Promise<void>")]
|
|
1462
|
+
pub fn wait_for_offset(&self, env: Env, offset_id: BigInt) -> Result<JsObject> {
|
|
1463
|
+
let offset = bigint_to_i64(offset_id)?;
|
|
1467
1464
|
|
|
1468
1465
|
let stream = self.inner.clone();
|
|
1469
1466
|
|
|
@@ -1474,10 +1471,9 @@ impl ZerobusArrowStream {
|
|
|
1474
1471
|
.as_ref()
|
|
1475
1472
|
.ok_or_else(|| napi::Error::from_reason("Arrow stream has been closed"))?;
|
|
1476
1473
|
|
|
1477
|
-
stream_ref
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
.map_err(|e| napi::Error::from_reason(format!("Failed to wait for offset: {}", e)))
|
|
1474
|
+
stream_ref.wait_for_offset(offset).await.map_err(|e| {
|
|
1475
|
+
napi::Error::from_reason(format!("Failed to wait for offset: {}", e))
|
|
1476
|
+
})
|
|
1481
1477
|
},
|
|
1482
1478
|
|_env, _| Ok(()),
|
|
1483
1479
|
)
|
|
@@ -1523,7 +1519,9 @@ impl ZerobusArrowStream {
|
|
|
1523
1519
|
/// Returns the table name for this stream.
|
|
1524
1520
|
#[napi(getter)]
|
|
1525
1521
|
pub fn table_name(&self) -> Result<String> {
|
|
1526
|
-
let guard = self
|
|
1522
|
+
let guard = self
|
|
1523
|
+
.inner
|
|
1524
|
+
.try_lock()
|
|
1527
1525
|
.map_err(|_| Error::from_reason("Stream is busy"))?;
|
|
1528
1526
|
let stream = guard
|
|
1529
1527
|
.as_ref()
|
|
@@ -1568,8 +1566,8 @@ impl ZerobusArrowStream {
|
|
|
1568
1566
|
impl ZerobusSdk {
|
|
1569
1567
|
/// Creates a new Arrow Flight stream to a Delta table.
|
|
1570
1568
|
///
|
|
1571
|
-
/// **
|
|
1572
|
-
///
|
|
1569
|
+
/// **Beta**: Arrow Flight support is in Beta. The API is stabilising
|
|
1570
|
+
/// but may still change before reaching GA.
|
|
1573
1571
|
///
|
|
1574
1572
|
/// This method establishes an Arrow Flight connection to the Zerobus service
|
|
1575
1573
|
/// for high-performance columnar data ingestion.
|
|
@@ -1613,22 +1611,69 @@ impl ZerobusSdk {
|
|
|
1613
1611
|
client_secret: String,
|
|
1614
1612
|
options: Option<ArrowStreamConfigurationOptions>,
|
|
1615
1613
|
) -> Result<JsObject> {
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1614
|
+
// Rust SDK 2.0 removed the convenience `create_arrow_stream` method;
|
|
1615
|
+
// open via `sdk.stream_builder().table(...).oauth(...).arrow(schema)
|
|
1616
|
+
// .build_arrow()` and apply options via setters. `ArrowTableProperties`
|
|
1617
|
+
// is also private now, so we build just the schema here.
|
|
1618
|
+
let schema = build_arrow_schema(&table_properties.schema_fields);
|
|
1619
|
+
let table_name = table_properties.table_name.clone();
|
|
1620
|
+
let opts = options.unwrap_or(ArrowStreamConfigurationOptions {
|
|
1621
|
+
max_inflight_batches: None,
|
|
1622
|
+
recovery: None,
|
|
1623
|
+
recovery_timeout_ms: None,
|
|
1624
|
+
recovery_backoff_ms: None,
|
|
1625
|
+
recovery_retries: None,
|
|
1626
|
+
server_lack_of_ack_timeout_ms: None,
|
|
1627
|
+
flush_timeout_ms: None,
|
|
1628
|
+
connection_timeout_ms: None,
|
|
1629
|
+
ipc_compression: None,
|
|
1630
|
+
});
|
|
1631
|
+
let ipc_compression = map_ipc_compression(opts.ipc_compression);
|
|
1632
|
+
let schema_for_stream = schema.clone();
|
|
1619
1633
|
|
|
1620
1634
|
let sdk = self.inner.clone();
|
|
1621
1635
|
|
|
1622
1636
|
env.execute_tokio_future(
|
|
1623
1637
|
async move {
|
|
1624
|
-
let
|
|
1625
|
-
.
|
|
1626
|
-
.
|
|
1627
|
-
.
|
|
1638
|
+
let mut builder = sdk
|
|
1639
|
+
.stream_builder()
|
|
1640
|
+
.table(table_name)
|
|
1641
|
+
.oauth(client_id, client_secret)
|
|
1642
|
+
.arrow(schema.clone());
|
|
1643
|
+
|
|
1644
|
+
if let Some(v) = opts.max_inflight_batches {
|
|
1645
|
+
builder = builder.max_inflight_batches(v as usize);
|
|
1646
|
+
}
|
|
1647
|
+
if let Some(v) = opts.recovery {
|
|
1648
|
+
builder = builder.recovery(v);
|
|
1649
|
+
}
|
|
1650
|
+
if let Some(v) = opts.recovery_timeout_ms {
|
|
1651
|
+
builder = builder.recovery_timeout_ms(v as u64);
|
|
1652
|
+
}
|
|
1653
|
+
if let Some(v) = opts.recovery_backoff_ms {
|
|
1654
|
+
builder = builder.recovery_backoff_ms(v as u64);
|
|
1655
|
+
}
|
|
1656
|
+
if let Some(v) = opts.recovery_retries {
|
|
1657
|
+
builder = builder.recovery_retries(v);
|
|
1658
|
+
}
|
|
1659
|
+
if let Some(v) = opts.server_lack_of_ack_timeout_ms {
|
|
1660
|
+
builder = builder.server_lack_of_ack_timeout_ms(v as u64);
|
|
1661
|
+
}
|
|
1662
|
+
if let Some(v) = opts.flush_timeout_ms {
|
|
1663
|
+
builder = builder.flush_timeout_ms(v as u64);
|
|
1664
|
+
}
|
|
1665
|
+
if let Some(v) = opts.connection_timeout_ms {
|
|
1666
|
+
builder = builder.connection_timeout_ms(v as u64);
|
|
1667
|
+
}
|
|
1668
|
+
builder = builder.ipc_compression(ipc_compression);
|
|
1669
|
+
|
|
1670
|
+
let stream = builder.build_arrow().await.map_err(|e| {
|
|
1671
|
+
napi::Error::from_reason(format!("Failed to create arrow stream: {}", e))
|
|
1672
|
+
})?;
|
|
1628
1673
|
|
|
1629
1674
|
Ok(ZerobusArrowStream {
|
|
1630
1675
|
inner: Arc::new(Mutex::new(Some(stream))),
|
|
1631
|
-
schema,
|
|
1676
|
+
schema: schema_for_stream,
|
|
1632
1677
|
})
|
|
1633
1678
|
},
|
|
1634
1679
|
|_env, stream| Ok(stream),
|
|
@@ -1637,7 +1682,7 @@ impl ZerobusSdk {
|
|
|
1637
1682
|
|
|
1638
1683
|
/// Recreates an Arrow stream with the same configuration and re-ingests unacknowledged batches.
|
|
1639
1684
|
///
|
|
1640
|
-
/// **
|
|
1685
|
+
/// **Beta**: Arrow Flight support is in Beta.
|
|
1641
1686
|
///
|
|
1642
1687
|
/// # Arguments
|
|
1643
1688
|
///
|
|
@@ -1647,7 +1692,10 @@ impl ZerobusSdk {
|
|
|
1647
1692
|
///
|
|
1648
1693
|
/// A Promise that resolves to a new ZerobusArrowStream with all unacknowledged batches re-ingested.
|
|
1649
1694
|
#[napi]
|
|
1650
|
-
pub async fn recreate_arrow_stream(
|
|
1695
|
+
pub async fn recreate_arrow_stream(
|
|
1696
|
+
&self,
|
|
1697
|
+
stream: &ZerobusArrowStream,
|
|
1698
|
+
) -> Result<ZerobusArrowStream> {
|
|
1651
1699
|
let inner_guard = stream.inner.lock().await;
|
|
1652
1700
|
let rust_stream = inner_guard
|
|
1653
1701
|
.as_ref()
|