@databricks/zerobus-ingest-sdk 1.1.0 → 1.3.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 +82 -97
- package/Cargo.toml +6 -13
- package/README.md +150 -205
- package/index.d.ts +355 -41
- package/index.js +4 -1
- package/package.json +28 -13
- package/src/headers_provider.ts +24 -36
- package/src/lib.rs +119 -82
- package/utils/descriptor.d.ts +32 -0
- package/utils/descriptor.js +55 -0
- package/utils/descriptor.ts +2 -2
package/src/headers_provider.ts
CHANGED
|
@@ -1,69 +1,57 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Custom headers provider accepted by `createStream()`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* The native adapter invokes `getHeadersCallback` synchronously once during
|
|
5
|
+
* stream creation and stores the returned tuples. Returning a Promise, or
|
|
6
|
+
* passing a class with async `getHeaders()`, is not supported and can terminate
|
|
7
|
+
* the process. Token refresh is not currently wired through this callback.
|
|
6
8
|
*/
|
|
7
9
|
export interface HeadersProvider {
|
|
8
10
|
/**
|
|
9
|
-
* Returns headers as array of [name, value] tuples.
|
|
11
|
+
* Returns headers as an array of [name, value] tuples.
|
|
10
12
|
*
|
|
11
13
|
* Required headers:
|
|
12
14
|
* - ["authorization", "Bearer <token>"]
|
|
13
15
|
* - ["x-databricks-zerobus-table-name", "<table_name>"]
|
|
14
|
-
*
|
|
15
|
-
* @returns Promise resolving to array of header name-value pairs
|
|
16
16
|
*/
|
|
17
|
-
|
|
17
|
+
getHeadersCallback: () => Array<[string, string]>;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* OAuth 2.0 Client Credentials headers provider.
|
|
22
22
|
*
|
|
23
|
-
*
|
|
23
|
+
* Do not instantiate this class or pass it to `createStream()`.
|
|
24
24
|
*
|
|
25
25
|
* OAuth authentication is handled automatically by the Rust SDK when you call
|
|
26
|
-
* `createStream()` with clientId and clientSecret
|
|
27
|
-
* a headers_provider).
|
|
28
|
-
*
|
|
29
|
-
* This class exists for:
|
|
30
|
-
* 1. Documentation purposes - showing the HeadersProvider pattern
|
|
31
|
-
* 2. API consistency with other Zerobus SDKs (Python, Java, Rust)
|
|
26
|
+
* `createStream()` with clientId and clientSecret and omit the headers provider.
|
|
32
27
|
*
|
|
33
|
-
*
|
|
28
|
+
* How to use OAuth authentication:
|
|
34
29
|
* ```typescript
|
|
35
|
-
* // OAuth is the default - just pass clientId and clientSecret
|
|
36
30
|
* const stream = await sdk.createStream(
|
|
37
31
|
* tableProperties,
|
|
38
|
-
* clientId,
|
|
39
|
-
* clientSecret,
|
|
32
|
+
* clientId,
|
|
33
|
+
* clientSecret,
|
|
40
34
|
* options
|
|
41
|
-
* // No headers_provider parameter = OAuth authentication
|
|
42
35
|
* );
|
|
43
36
|
* ```
|
|
44
37
|
*
|
|
45
|
-
*
|
|
38
|
+
* How to use custom authentication (PAT or a static token):
|
|
46
39
|
* ```typescript
|
|
47
|
-
* class CustomHeadersProvider implements HeadersProvider {
|
|
48
|
-
* async getHeaders() {
|
|
49
|
-
* return [
|
|
50
|
-
* ["authorization", `Bearer ${myToken}`],
|
|
51
|
-
* ["x-databricks-zerobus-table-name", tableName]
|
|
52
|
-
* ];
|
|
53
|
-
* }
|
|
54
|
-
* }
|
|
55
|
-
*
|
|
56
|
-
* const provider = new CustomHeadersProvider();
|
|
57
40
|
* const stream = await sdk.createStream(
|
|
58
41
|
* tableProperties,
|
|
59
|
-
* '',
|
|
60
|
-
* '',
|
|
42
|
+
* '',
|
|
43
|
+
* '',
|
|
61
44
|
* options,
|
|
62
|
-
* {
|
|
45
|
+
* {
|
|
46
|
+
* getHeadersCallback: () => [
|
|
47
|
+
* ["authorization", `Bearer ${myToken}`],
|
|
48
|
+
* ["x-databricks-zerobus-table-name", tableName]
|
|
49
|
+
* ]
|
|
50
|
+
* }
|
|
63
51
|
* );
|
|
64
52
|
* ```
|
|
65
53
|
*/
|
|
66
|
-
export class OAuthHeadersProvider
|
|
54
|
+
export class OAuthHeadersProvider {
|
|
67
55
|
constructor(
|
|
68
56
|
private clientId: string,
|
|
69
57
|
private clientSecret: string,
|
|
@@ -75,8 +63,8 @@ export class OAuthHeadersProvider implements HeadersProvider {
|
|
|
75
63
|
throw new Error(
|
|
76
64
|
'OAuthHeadersProvider should not be instantiated directly. ' +
|
|
77
65
|
'OAuth authentication is handled internally by the Rust SDK. ' +
|
|
78
|
-
'To use OAuth: call createStream(tableProperties, clientId, clientSecret, options) without
|
|
79
|
-
'To use custom authentication:
|
|
66
|
+
'To use OAuth: call createStream(tableProperties, clientId, clientSecret, options) without a headers provider. ' +
|
|
67
|
+
'To use custom authentication: pass { getHeadersCallback: () => [...] } as the headers provider.'
|
|
80
68
|
);
|
|
81
69
|
}
|
|
82
70
|
}
|
package/src/lib.rs
CHANGED
|
@@ -50,7 +50,7 @@ pub enum RecordType {
|
|
|
50
50
|
#[napi(object)]
|
|
51
51
|
pub struct StreamConfigurationOptions {
|
|
52
52
|
/// Maximum number of unacknowledged requests that can be in flight.
|
|
53
|
-
/// Default:
|
|
53
|
+
/// Default: 1,000,000
|
|
54
54
|
pub max_inflight_requests: Option<u32>,
|
|
55
55
|
|
|
56
56
|
/// Enable automatic stream recovery on transient failures.
|
|
@@ -103,7 +103,8 @@ pub struct TableProperties {
|
|
|
103
103
|
pub table_name: String,
|
|
104
104
|
|
|
105
105
|
/// Optional Protocol Buffer descriptor as a base64-encoded string.
|
|
106
|
-
///
|
|
106
|
+
/// Omitting this does not select JSON. The stream defaults to Protocol Buffers
|
|
107
|
+
/// unless `record_type` is set to JSON.
|
|
107
108
|
pub descriptor_proto: Option<String>,
|
|
108
109
|
}
|
|
109
110
|
|
|
@@ -213,15 +214,16 @@ fn convert_js_to_record_payload(env: &Env, payload: Unknown) -> Result<RustRecor
|
|
|
213
214
|
|
|
214
215
|
/// A stream for ingesting data into a Databricks Delta table.
|
|
215
216
|
///
|
|
216
|
-
/// The stream manages
|
|
217
|
-
/// and provides automatic recovery
|
|
217
|
+
/// The stream manages JSON or Protocol Buffer ingestion over a bidirectional
|
|
218
|
+
/// gRPC connection, handles acknowledgments, and provides automatic recovery
|
|
219
|
+
/// on transient failures.
|
|
218
220
|
///
|
|
219
221
|
/// # Example
|
|
220
222
|
///
|
|
221
223
|
/// ```typescript
|
|
222
224
|
/// const stream = await sdk.createStream(tableProps, clientId, clientSecret, options);
|
|
223
|
-
/// const
|
|
224
|
-
///
|
|
225
|
+
/// const offset = await stream.ingestRecordOffset(Buffer.from([1, 2, 3]));
|
|
226
|
+
/// await stream.flush();
|
|
225
227
|
/// await stream.close();
|
|
226
228
|
/// ```
|
|
227
229
|
#[napi]
|
|
@@ -400,6 +402,13 @@ impl ZerobusStream {
|
|
|
400
402
|
/// This is the recommended API for high-throughput scenarios where you want to
|
|
401
403
|
/// decouple record ingestion from acknowledgment tracking.
|
|
402
404
|
///
|
|
405
|
+
/// **Acknowledgments:** the idiomatic flow is to ingest in a loop and then `flush()`
|
|
406
|
+
/// once to confirm everything queued so far. The returned offset, together with
|
|
407
|
+
/// `waitForOffset()`, lets you confirm a specific record when you need it (acks are
|
|
408
|
+
/// ordered, so the last offset confirms the whole run) — prefer `flush()` for bulk.
|
|
409
|
+
/// Avoid calling `waitForOffset()` after every record in a tight loop, since that
|
|
410
|
+
/// limits throughput to one record per round-trip.
|
|
411
|
+
///
|
|
403
412
|
/// # Arguments
|
|
404
413
|
///
|
|
405
414
|
/// * `payload` - The record data (Buffer, string, protobuf message, or plain object)
|
|
@@ -412,11 +421,14 @@ impl ZerobusStream {
|
|
|
412
421
|
/// # Example
|
|
413
422
|
///
|
|
414
423
|
/// ```typescript
|
|
415
|
-
/// //
|
|
416
|
-
///
|
|
417
|
-
/// const
|
|
418
|
-
/// //
|
|
419
|
-
///
|
|
424
|
+
/// // High-throughput pattern: ingest in a loop, wait once at the end.
|
|
425
|
+
/// let lastOffset: bigint | null = null;
|
|
426
|
+
/// for (const record of records) {
|
|
427
|
+
/// lastOffset = await stream.ingestRecordOffset(record); // resolves on queue, no round-trip
|
|
428
|
+
/// }
|
|
429
|
+
/// // The ack watermark is monotonic: waiting on the last offset confirms all prior records.
|
|
430
|
+
/// if (lastOffset !== null) await stream.waitForOffset(lastOffset);
|
|
431
|
+
/// // Or simply: await stream.flush();
|
|
420
432
|
/// ```
|
|
421
433
|
#[napi(ts_return_type = "Promise<bigint>")]
|
|
422
434
|
pub fn ingest_record_offset(&self, env: Env, payload: Unknown) -> Result<JsObject> {
|
|
@@ -454,6 +466,12 @@ impl ZerobusStream {
|
|
|
454
466
|
/// the batch is queued, without waiting for server acknowledgment. Use
|
|
455
467
|
/// `waitForOffset()` to wait for acknowledgment when needed.
|
|
456
468
|
///
|
|
469
|
+
/// **Acknowledgments:** the idiomatic flow is to ingest your batches in a loop and
|
|
470
|
+
/// then `flush()` once to confirm. The returned offset, together with `waitForOffset()`,
|
|
471
|
+
/// confirms a specific batch when you need it (acks are ordered, so the last offset
|
|
472
|
+
/// confirms the whole run) — prefer `flush()` for bulk. Avoid calling `waitForOffset()`
|
|
473
|
+
/// after every batch in a tight loop, since that limits throughput to one round-trip per batch.
|
|
474
|
+
///
|
|
457
475
|
/// # Arguments
|
|
458
476
|
///
|
|
459
477
|
/// * `records` - Array of record data
|
|
@@ -466,11 +484,14 @@ impl ZerobusStream {
|
|
|
466
484
|
/// # Example
|
|
467
485
|
///
|
|
468
486
|
/// ```typescript
|
|
469
|
-
/// //
|
|
470
|
-
///
|
|
471
|
-
///
|
|
472
|
-
/// await stream.
|
|
487
|
+
/// // Ingest many batches without waiting, then flush once.
|
|
488
|
+
/// let lastOffset = null;
|
|
489
|
+
/// for (const batch of batches) {
|
|
490
|
+
/// const offset = await stream.ingestRecordsOffset(batch); // resolves on queue
|
|
491
|
+
/// if (offset !== null) lastOffset = offset;
|
|
473
492
|
/// }
|
|
493
|
+
/// if (lastOffset !== null) await stream.waitForOffset(lastOffset);
|
|
494
|
+
/// // Or simply: await stream.flush();
|
|
474
495
|
/// ```
|
|
475
496
|
#[napi(ts_return_type = "Promise<bigint | null>")]
|
|
476
497
|
pub fn ingest_records_offset(&self, env: Env, records: Vec<Unknown>) -> Result<JsObject> {
|
|
@@ -511,9 +532,12 @@ impl ZerobusStream {
|
|
|
511
532
|
|
|
512
533
|
/// Waits for a specific offset to be acknowledged by the server.
|
|
513
534
|
///
|
|
514
|
-
/// Use this method with `ingestRecordOffset()` and `ingestRecordsOffset()` to
|
|
515
|
-
///
|
|
516
|
-
///
|
|
535
|
+
/// Use this method with `ingestRecordOffset()` and `ingestRecordsOffset()` to confirm
|
|
536
|
+
/// a specific record before continuing. Acks are ordered, so waiting on the LAST offset
|
|
537
|
+
/// confirms every prior record too — you never need to wait on intermediate offsets.
|
|
538
|
+
/// For confirming a bulk run, `flush()` is usually simpler; reach for `waitForOffset()`
|
|
539
|
+
/// when one particular record must be confirmed. Avoid calling it after every record in
|
|
540
|
+
/// a tight loop, since that limits throughput to one record per round-trip.
|
|
517
541
|
///
|
|
518
542
|
/// # Arguments
|
|
519
543
|
///
|
|
@@ -527,12 +551,12 @@ impl ZerobusStream {
|
|
|
527
551
|
/// # Example
|
|
528
552
|
///
|
|
529
553
|
/// ```typescript
|
|
530
|
-
///
|
|
554
|
+
/// let lastOffset: bigint | null = null;
|
|
531
555
|
/// for (const record of records) {
|
|
532
|
-
///
|
|
556
|
+
/// lastOffset = await stream.ingestRecordOffset(record); // no per-record wait
|
|
533
557
|
/// }
|
|
534
|
-
/// // Wait for the last offset (implies all previous are also acknowledged)
|
|
535
|
-
/// await stream.waitForOffset(
|
|
558
|
+
/// // Wait for the last offset only (implies all previous are also acknowledged).
|
|
559
|
+
/// if (lastOffset !== null) await stream.waitForOffset(lastOffset);
|
|
536
560
|
/// ```
|
|
537
561
|
#[napi(ts_return_type = "Promise<void>")]
|
|
538
562
|
pub fn wait_for_offset(&self, env: Env, offset_id: BigInt) -> Result<JsObject> {
|
|
@@ -560,6 +584,11 @@ impl ZerobusStream {
|
|
|
560
584
|
/// This method ensures all previously ingested records have been sent to the server
|
|
561
585
|
/// and acknowledged. It's useful for checkpointing or ensuring data durability.
|
|
562
586
|
///
|
|
587
|
+
/// This is the idiomatic way to confirm records ingested via `ingestRecordOffset()` /
|
|
588
|
+
/// `ingestRecordsOffset()`: ingest in a loop, then `flush()` once (for a bounded batch,
|
|
589
|
+
/// or periodically for a long-running stream). It resolves once everything queued so
|
|
590
|
+
/// far is acknowledged.
|
|
591
|
+
///
|
|
563
592
|
/// # Errors
|
|
564
593
|
///
|
|
565
594
|
/// - Timeout errors if flush takes longer than configured timeout
|
|
@@ -646,15 +675,12 @@ impl ZerobusStream {
|
|
|
646
675
|
///
|
|
647
676
|
/// ```typescript
|
|
648
677
|
/// try {
|
|
649
|
-
/// await stream.
|
|
650
|
-
/// await stream.
|
|
678
|
+
/// await stream.ingestRecordsOffset(batch1);
|
|
679
|
+
/// await stream.ingestRecordsOffset(batch2);
|
|
680
|
+
/// await stream.flush();
|
|
651
681
|
/// } catch (error) {
|
|
652
682
|
/// const unackedBatches = await stream.getUnackedBatches();
|
|
653
|
-
///
|
|
654
|
-
/// // Re-ingest with new stream
|
|
655
|
-
/// for (const batch of unackedBatches) {
|
|
656
|
-
/// await newStream.ingestRecords(batch);
|
|
657
|
-
/// }
|
|
683
|
+
/// console.log(`Batches available for recovery: ${unackedBatches.length}`);
|
|
658
684
|
/// }
|
|
659
685
|
/// ```
|
|
660
686
|
#[napi]
|
|
@@ -687,10 +713,10 @@ impl ZerobusStream {
|
|
|
687
713
|
/// JavaScript headers provider callback wrapper.
|
|
688
714
|
///
|
|
689
715
|
/// Allows TypeScript code to provide custom authentication headers
|
|
690
|
-
/// by implementing a
|
|
716
|
+
/// by implementing a getHeadersCallback() function.
|
|
691
717
|
#[napi(object)]
|
|
692
718
|
pub struct JsHeadersProvider {
|
|
693
|
-
/// JavaScript function: () =>
|
|
719
|
+
/// JavaScript function: () => Array<[string, string]>
|
|
694
720
|
pub get_headers_callback: JsFunction,
|
|
695
721
|
}
|
|
696
722
|
|
|
@@ -721,11 +747,6 @@ impl StaticHeadersProvider {
|
|
|
721
747
|
));
|
|
722
748
|
}
|
|
723
749
|
|
|
724
|
-
// Add TS user agent if not provided
|
|
725
|
-
if !map.contains_key("user-agent") {
|
|
726
|
-
map.insert("user-agent", TS_SDK_USER_AGENT.to_string());
|
|
727
|
-
}
|
|
728
|
-
|
|
729
750
|
Ok(Self { headers: map })
|
|
730
751
|
}
|
|
731
752
|
}
|
|
@@ -810,11 +831,17 @@ impl RustHeadersProvider for TsOAuthHeadersProvider {
|
|
|
810
831
|
let mut headers = HashMap::new();
|
|
811
832
|
headers.insert("authorization", format!("Bearer {}", token));
|
|
812
833
|
headers.insert("x-databricks-zerobus-table-name", self.table_name.clone());
|
|
813
|
-
headers.insert("user-agent", TS_SDK_USER_AGENT.to_string());
|
|
814
834
|
Ok(headers)
|
|
815
835
|
}
|
|
816
836
|
}
|
|
817
837
|
|
|
838
|
+
#[napi(object)]
|
|
839
|
+
#[derive(Default)]
|
|
840
|
+
pub struct ZerobusSdkOptions {
|
|
841
|
+
/// Identifier appended to the `user-agent` header
|
|
842
|
+
pub application_name: Option<String>,
|
|
843
|
+
}
|
|
844
|
+
|
|
818
845
|
/// The main SDK for interacting with the Databricks Zerobus service.
|
|
819
846
|
///
|
|
820
847
|
/// This is the entry point for creating ingestion streams to Delta tables.
|
|
@@ -824,7 +851,8 @@ impl RustHeadersProvider for TsOAuthHeadersProvider {
|
|
|
824
851
|
/// ```typescript
|
|
825
852
|
/// const sdk = new ZerobusSdk(
|
|
826
853
|
/// "https://workspace-id.zerobus.region.cloud.databricks.com",
|
|
827
|
-
/// "https://workspace.cloud.databricks.com"
|
|
854
|
+
/// "https://workspace.cloud.databricks.com",
|
|
855
|
+
/// { applicationName: "my-app/1.0" }
|
|
828
856
|
/// );
|
|
829
857
|
///
|
|
830
858
|
/// const stream = await sdk.createStream(
|
|
@@ -852,13 +880,19 @@ impl ZerobusSdk {
|
|
|
852
880
|
/// (e.g., "https://workspace-id.zerobus.region.cloud.databricks.com")
|
|
853
881
|
/// * `unity_catalog_url` - The Unity Catalog endpoint URL
|
|
854
882
|
/// (e.g., "https://workspace.cloud.databricks.com")
|
|
883
|
+
/// * `options` - Optional SDK configuration (see `ZerobusSdkOptions`),
|
|
884
|
+
/// including `applicationName` for server-side attribution.
|
|
855
885
|
///
|
|
856
886
|
/// # Errors
|
|
857
887
|
///
|
|
858
888
|
/// - Invalid endpoint URLs
|
|
859
889
|
/// - Failed to extract workspace ID from the endpoint
|
|
860
890
|
#[napi(constructor)]
|
|
861
|
-
pub fn new(
|
|
891
|
+
pub fn new(
|
|
892
|
+
zerobus_endpoint: String,
|
|
893
|
+
unity_catalog_url: String,
|
|
894
|
+
options: Option<ZerobusSdkOptions>,
|
|
895
|
+
) -> Result<Self> {
|
|
862
896
|
let workspace_id = zerobus_endpoint
|
|
863
897
|
.strip_prefix("https://")
|
|
864
898
|
.or_else(|| zerobus_endpoint.strip_prefix("http://"))
|
|
@@ -870,9 +904,17 @@ impl ZerobusSdk {
|
|
|
870
904
|
)
|
|
871
905
|
})?;
|
|
872
906
|
|
|
873
|
-
let
|
|
907
|
+
let options = options.unwrap_or_default();
|
|
908
|
+
|
|
909
|
+
let builder = RustZerobusSdk::builder()
|
|
874
910
|
.endpoint(&zerobus_endpoint)
|
|
875
911
|
.unity_catalog_url(&unity_catalog_url)
|
|
912
|
+
.sdk_identifier(TS_SDK_USER_AGENT);
|
|
913
|
+
let builder = match options.application_name {
|
|
914
|
+
Some(name) => builder.application_name(name),
|
|
915
|
+
None => builder,
|
|
916
|
+
};
|
|
917
|
+
let inner = builder
|
|
876
918
|
.build()
|
|
877
919
|
.map_err(|e| Error::from_reason(format!("Failed to create SDK: {}", e)))?;
|
|
878
920
|
|
|
@@ -885,7 +927,7 @@ impl ZerobusSdk {
|
|
|
885
927
|
|
|
886
928
|
/// Creates a new ingestion stream to a Delta table.
|
|
887
929
|
///
|
|
888
|
-
/// This method
|
|
930
|
+
/// This method opens a JSON or Protocol Buffer stream to the Zerobus service
|
|
889
931
|
/// and prepares it for data ingestion. By default, it uses OAuth 2.0 Client Credentials
|
|
890
932
|
/// authentication. For custom authentication (e.g., Personal Access Tokens), provide
|
|
891
933
|
/// a custom headers_provider.
|
|
@@ -927,7 +969,7 @@ impl ZerobusSdk {
|
|
|
927
969
|
/// "", // ignored
|
|
928
970
|
/// undefined,
|
|
929
971
|
/// {
|
|
930
|
-
/// getHeadersCallback:
|
|
972
|
+
/// getHeadersCallback: () => [
|
|
931
973
|
/// ["authorization", `Bearer ${myToken}`],
|
|
932
974
|
/// ["x-databricks-zerobus-table-name", tableName]
|
|
933
975
|
/// ]
|
|
@@ -947,19 +989,19 @@ impl ZerobusSdk {
|
|
|
947
989
|
// Decode the optional protobuf descriptor up-front so we can hand it
|
|
948
990
|
// to the builder's `.compiled_proto()` setter; the builder constructs
|
|
949
991
|
// the (now-private) `TableProperties` itself.
|
|
950
|
-
let descriptor_proto: Option<prost_types::DescriptorProto> =
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
992
|
+
let descriptor_proto: Option<prost_types::DescriptorProto> = if let Some(ref desc_str) =
|
|
993
|
+
table_properties.descriptor_proto
|
|
994
|
+
{
|
|
995
|
+
let bytes = base64_decode(desc_str)
|
|
996
|
+
.map_err(|e| Error::from_reason(format!("Failed to decode descriptor: {}", e)))?;
|
|
997
|
+
let dp: prost_types::DescriptorProto =
|
|
998
|
+
prost::Message::decode(&bytes[..]).map_err(|e| {
|
|
999
|
+
Error::from_reason(format!("Failed to parse descriptor proto: {}", e))
|
|
954
1000
|
})?;
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
Some(dp)
|
|
960
|
-
} else {
|
|
961
|
-
None
|
|
962
|
-
};
|
|
1001
|
+
Some(dp)
|
|
1002
|
+
} else {
|
|
1003
|
+
None
|
|
1004
|
+
};
|
|
963
1005
|
|
|
964
1006
|
let opts = options.unwrap_or(StreamConfigurationOptions {
|
|
965
1007
|
max_inflight_requests: None,
|
|
@@ -1076,7 +1118,8 @@ impl ZerobusSdk {
|
|
|
1076
1118
|
///
|
|
1077
1119
|
/// # Arguments
|
|
1078
1120
|
///
|
|
1079
|
-
/// * `stream` - The failed
|
|
1121
|
+
/// * `stream` - The terminally failed stream to recreate. The TypeScript wrapper
|
|
1122
|
+
/// must not have been closed because `close()` releases its native handle.
|
|
1080
1123
|
///
|
|
1081
1124
|
/// # Returns
|
|
1082
1125
|
///
|
|
@@ -1092,12 +1135,23 @@ impl ZerobusSdk {
|
|
|
1092
1135
|
///
|
|
1093
1136
|
/// ```typescript
|
|
1094
1137
|
/// try {
|
|
1095
|
-
/// await stream.
|
|
1138
|
+
/// await stream.ingestRecordsOffset(batch);
|
|
1139
|
+
/// await stream.flush();
|
|
1096
1140
|
/// } catch (error) {
|
|
1097
|
-
///
|
|
1098
|
-
///
|
|
1099
|
-
///
|
|
1100
|
-
///
|
|
1141
|
+
/// try {
|
|
1142
|
+
/// const newStream = await sdk.recreateStream(stream);
|
|
1143
|
+
/// try {
|
|
1144
|
+
/// await newStream.flush();
|
|
1145
|
+
/// } finally {
|
|
1146
|
+
/// await newStream.close();
|
|
1147
|
+
/// }
|
|
1148
|
+
/// } finally {
|
|
1149
|
+
/// try {
|
|
1150
|
+
/// await stream.close();
|
|
1151
|
+
/// } catch (closeError) {
|
|
1152
|
+
/// console.error("Failed stream released:", closeError);
|
|
1153
|
+
/// }
|
|
1154
|
+
/// }
|
|
1101
1155
|
/// }
|
|
1102
1156
|
/// ```
|
|
1103
1157
|
#[napi]
|
|
@@ -1128,8 +1182,8 @@ fn base64_decode(input: &str) -> std::result::Result<Vec<u8>, String> {
|
|
|
1128
1182
|
}
|
|
1129
1183
|
|
|
1130
1184
|
// =============================================================================
|
|
1131
|
-
// Arrow Flight
|
|
1132
|
-
//
|
|
1185
|
+
// Arrow Flight support
|
|
1186
|
+
// Behind the arrow-flight feature. Enable with: npm run build:arrow
|
|
1133
1187
|
// =============================================================================
|
|
1134
1188
|
|
|
1135
1189
|
#[cfg(feature = "arrow-flight")]
|
|
@@ -1144,8 +1198,6 @@ use databricks_zerobus_ingest_sdk::{
|
|
|
1144
1198
|
|
|
1145
1199
|
/// IPC compression type for Arrow Flight streams.
|
|
1146
1200
|
///
|
|
1147
|
-
/// **Beta**: Arrow Flight support is in Beta. The API is stabilising but
|
|
1148
|
-
/// may still change before reaching GA.
|
|
1149
1201
|
#[cfg(feature = "arrow-flight")]
|
|
1150
1202
|
#[napi]
|
|
1151
1203
|
pub enum IpcCompressionType {
|
|
@@ -1157,8 +1209,6 @@ pub enum IpcCompressionType {
|
|
|
1157
1209
|
|
|
1158
1210
|
/// Configuration options for Arrow Flight streams.
|
|
1159
1211
|
///
|
|
1160
|
-
/// **Beta**: Arrow Flight support is in Beta. The API is stabilising but
|
|
1161
|
-
/// may still change before reaching GA.
|
|
1162
1212
|
#[cfg(feature = "arrow-flight")]
|
|
1163
1213
|
#[napi(object)]
|
|
1164
1214
|
#[derive(Debug, Clone)]
|
|
@@ -1215,7 +1265,6 @@ fn map_ipc_compression(value: Option<i32>) -> Option<arrow_ipc::CompressionType>
|
|
|
1215
1265
|
|
|
1216
1266
|
/// Arrow data type enum for schema definition.
|
|
1217
1267
|
///
|
|
1218
|
-
/// **Beta**: Arrow Flight support is in Beta.
|
|
1219
1268
|
#[cfg(feature = "arrow-flight")]
|
|
1220
1269
|
#[napi]
|
|
1221
1270
|
pub enum ArrowDataType {
|
|
@@ -1287,7 +1336,6 @@ fn convert_arrow_data_type(dt: i32) -> RustDataType {
|
|
|
1287
1336
|
|
|
1288
1337
|
/// Arrow field definition for schema.
|
|
1289
1338
|
///
|
|
1290
|
-
/// **Beta**: Arrow Flight support is in Beta.
|
|
1291
1339
|
#[cfg(feature = "arrow-flight")]
|
|
1292
1340
|
#[napi(object)]
|
|
1293
1341
|
#[derive(Debug, Clone)]
|
|
@@ -1305,8 +1353,6 @@ pub struct ArrowField {
|
|
|
1305
1353
|
/// Unlike `TableProperties` which uses Protocol Buffers, Arrow Flight streams
|
|
1306
1354
|
/// require an Arrow schema definition.
|
|
1307
1355
|
///
|
|
1308
|
-
/// **Beta**: Arrow Flight support is in Beta. The API is stabilising but
|
|
1309
|
-
/// may still change before reaching GA.
|
|
1310
1356
|
#[cfg(feature = "arrow-flight")]
|
|
1311
1357
|
#[napi(object)]
|
|
1312
1358
|
#[derive(Debug, Clone)]
|
|
@@ -1340,9 +1386,6 @@ fn build_arrow_schema(fields: &[ArrowField]) -> Arc<RustArrowSchema> {
|
|
|
1340
1386
|
/// This stream provides a high-performance interface for streaming Arrow data
|
|
1341
1387
|
/// to Databricks Delta tables using the Arrow Flight protocol.
|
|
1342
1388
|
///
|
|
1343
|
-
/// **Beta**: Arrow Flight support is in Beta. The API is stabilising but
|
|
1344
|
-
/// may still change before reaching GA.
|
|
1345
|
-
///
|
|
1346
1389
|
/// # Lifecycle
|
|
1347
1390
|
///
|
|
1348
1391
|
/// 1. Create a stream via `sdk.createArrowStream()`
|
|
@@ -1438,9 +1481,7 @@ impl ZerobusArrowStream {
|
|
|
1438
1481
|
stream_ref
|
|
1439
1482
|
.ingest_ipc_batch(Bytes::from(buffer_vec))
|
|
1440
1483
|
.await
|
|
1441
|
-
.map_err(|e| {
|
|
1442
|
-
napi::Error::from_reason(format!("Failed to ingest batch: {}", e))
|
|
1443
|
-
})
|
|
1484
|
+
.map_err(|e| napi::Error::from_reason(format!("Failed to ingest batch: {}", e)))
|
|
1444
1485
|
},
|
|
1445
1486
|
|env, offset_id| {
|
|
1446
1487
|
let global: JsGlobal = env.get_global()?;
|
|
@@ -1566,9 +1607,6 @@ impl ZerobusArrowStream {
|
|
|
1566
1607
|
impl ZerobusSdk {
|
|
1567
1608
|
/// Creates a new Arrow Flight stream to a Delta table.
|
|
1568
1609
|
///
|
|
1569
|
-
/// **Beta**: Arrow Flight support is in Beta. The API is stabilising
|
|
1570
|
-
/// but may still change before reaching GA.
|
|
1571
|
-
///
|
|
1572
1610
|
/// This method establishes an Arrow Flight connection to the Zerobus service
|
|
1573
1611
|
/// for high-performance columnar data ingestion.
|
|
1574
1612
|
///
|
|
@@ -1682,11 +1720,10 @@ impl ZerobusSdk {
|
|
|
1682
1720
|
|
|
1683
1721
|
/// Recreates an Arrow stream with the same configuration and re-ingests unacknowledged batches.
|
|
1684
1722
|
///
|
|
1685
|
-
/// **Beta**: Arrow Flight support is in Beta.
|
|
1686
|
-
///
|
|
1687
1723
|
/// # Arguments
|
|
1688
1724
|
///
|
|
1689
|
-
/// * `stream` - The failed
|
|
1725
|
+
/// * `stream` - The terminally failed Arrow stream to recreate. The TypeScript wrapper
|
|
1726
|
+
/// must not have been closed because `close()` releases its native handle.
|
|
1690
1727
|
///
|
|
1691
1728
|
/// # Returns
|
|
1692
1729
|
///
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for loading a descriptor from a FileDescriptorSet.
|
|
3
|
+
*/
|
|
4
|
+
export interface LoadDescriptorOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Path to the descriptor file generated by protoc.
|
|
7
|
+
* Example: 'schemas/my_schema_descriptor.pb'
|
|
8
|
+
*/
|
|
9
|
+
descriptorPath: string;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Name of the proto file within the FileDescriptorSet.
|
|
13
|
+
* This should match the filename in your .proto file.
|
|
14
|
+
* Example: 'schemas/air_quality.proto' or 'air_quality.proto'
|
|
15
|
+
*/
|
|
16
|
+
protoFileName: string;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Name of the message type to extract.
|
|
20
|
+
* Example: 'AirQuality'
|
|
21
|
+
*/
|
|
22
|
+
messageName: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Loads a specific message's DescriptorProto from a FileDescriptorSet.
|
|
27
|
+
*
|
|
28
|
+
* @param options - Configuration specifying which message to extract
|
|
29
|
+
* @returns Base64-encoded DescriptorProto string for the specified message
|
|
30
|
+
* @throws Error if the descriptor file, proto file, or message cannot be found
|
|
31
|
+
*/
|
|
32
|
+
export function loadDescriptorProto(options: LoadDescriptorOptions): string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Utility functions for working with Protocol Buffer descriptors.
|
|
5
|
+
*
|
|
6
|
+
* These utilities help extract message descriptors from FileDescriptorSets
|
|
7
|
+
* generated by protoc, providing flexibility in choosing which message to use.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
const descriptor = require("protobufjs/ext/descriptor");
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Loads a specific message's DescriptorProto from a FileDescriptorSet.
|
|
15
|
+
*
|
|
16
|
+
* @param {object} options
|
|
17
|
+
* @param {string} options.descriptorPath Path to the descriptor file generated by protoc.
|
|
18
|
+
* @param {string} options.protoFileName Name of the proto file within the FileDescriptorSet.
|
|
19
|
+
* @param {string} options.messageName Name of the message type to extract.
|
|
20
|
+
* @returns {string} Base64-encoded DescriptorProto string for the specified message.
|
|
21
|
+
*/
|
|
22
|
+
function loadDescriptorProto(options) {
|
|
23
|
+
const { descriptorPath, protoFileName, messageName } = options;
|
|
24
|
+
|
|
25
|
+
const descriptorBytes = fs.readFileSync(descriptorPath);
|
|
26
|
+
const fileDescriptorSet = descriptor.FileDescriptorSet.decode(descriptorBytes);
|
|
27
|
+
const fileDescriptor = fileDescriptorSet.file.find((file) =>
|
|
28
|
+
file.name === protoFileName ||
|
|
29
|
+
file.name.endsWith("/" + protoFileName)
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
if (!fileDescriptor) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`Proto file '${protoFileName}' not found in descriptor. Available files: ${
|
|
35
|
+
fileDescriptorSet.file.map((file) => file.name).join(", ")
|
|
36
|
+
}`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const messageType = fileDescriptor.messageType?.find((message) => message.name === messageName);
|
|
41
|
+
|
|
42
|
+
if (!messageType) {
|
|
43
|
+
const availableMessages = fileDescriptor.messageType?.map((message) => message.name).join(", ") || "none";
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Message '${messageName}' not found in ${protoFileName}. Available messages: ${availableMessages}`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const descriptorProtoBytes = descriptor.DescriptorProto.encode(messageType).finish();
|
|
50
|
+
return Buffer.from(descriptorProtoBytes).toString("base64");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = {
|
|
54
|
+
loadDescriptorProto,
|
|
55
|
+
};
|
package/utils/descriptor.ts
CHANGED
|
@@ -49,7 +49,7 @@ export interface LoadDescriptorOptions {
|
|
|
49
49
|
*
|
|
50
50
|
* @example
|
|
51
51
|
* ```typescript
|
|
52
|
-
* import { loadDescriptorProto } from '@databricks/zerobus-sdk/utils/descriptor';
|
|
52
|
+
* import { loadDescriptorProto } from '@databricks/zerobus-ingest-sdk/utils/descriptor.js';
|
|
53
53
|
*
|
|
54
54
|
* const descriptorBase64 = loadDescriptorProto({
|
|
55
55
|
* descriptorPath: 'schemas/air_quality_descriptor.pb',
|
|
@@ -74,7 +74,7 @@ export function loadDescriptorProto(options: LoadDescriptorOptions): string {
|
|
|
74
74
|
|
|
75
75
|
// Find the file descriptor matching the proto file name
|
|
76
76
|
const fileDescriptor = fileDescriptorSet.file.find((f: any) =>
|
|
77
|
-
f.name === protoFileName || f.name.endsWith('/' + protoFileName)
|
|
77
|
+
f.name === protoFileName || f.name.endsWith('/' + protoFileName)
|
|
78
78
|
);
|
|
79
79
|
|
|
80
80
|
if (!fileDescriptor) {
|