@qualithm/arrow-flight-sql-js 0.1.0 → 0.2.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/dist/client.js CHANGED
@@ -4,13 +4,12 @@
4
4
  * A TypeScript client for communicating with Arrow Flight SQL servers.
5
5
  * Modeled after the official Arrow Flight SQL clients (Java, C++, Go).
6
6
  */
7
- import * as grpc from "@grpc/grpc-js";
8
7
  import { RecordBatchReader } from "apache-arrow";
9
- import { collectToTable, parseFlightData, tryParseSchema } from "./arrow";
10
- import { AuthenticationError, ConnectionError, FlightSqlError } from "./errors";
11
- import { getFlightServiceDefinition } from "./generated";
12
- import { encodeActionClosePreparedStatementRequest, encodeActionCreatePreparedStatementRequest, encodeCommandGetCatalogs, encodeCommandGetDbSchemas, encodeCommandGetExportedKeys, encodeCommandGetImportedKeys, encodeCommandGetPrimaryKeys, encodeCommandGetTables, encodeCommandGetTableTypes, encodeCommandPreparedStatementQuery, encodeCommandStatementQuery, encodeCommandStatementUpdate, getBytesField, parseProtoFields } from "./proto";
13
- import { SubscriptionMessageType, SubscriptionMode } from "./types";
8
+ import { collectToTable, parseFlightData, tryParseSchema } from "./arrow.js";
9
+ import { AuthenticationError, ConnectionError, FlightSqlError } from "./errors.js";
10
+ import { encodeActionClosePreparedStatementRequest, encodeActionCreatePreparedStatementRequest, encodeCommandGetCatalogs, encodeCommandGetDbSchemas, encodeCommandGetExportedKeys, encodeCommandGetImportedKeys, encodeCommandGetPrimaryKeys, encodeCommandGetTables, encodeCommandGetTableTypes, encodeCommandPreparedStatementQuery, encodeCommandStatementQuery, encodeCommandStatementUpdate, getBytesField, parseProtoFields, unwrapAny } from "./proto.js";
11
+ import { getTransportForRuntime } from "./transport-grpc-js.js";
12
+ import { SubscriptionMessageType, SubscriptionMode } from "./types.js";
14
13
  // Default configuration values
15
14
  const defaultConnectTimeoutMs = 30_000;
16
15
  const defaultRequestTimeoutMs = 60_000;
@@ -38,8 +37,7 @@ const defaultRequestTimeoutMs = 60_000;
38
37
  */
39
38
  export class FlightSqlClient {
40
39
  options;
41
- grpcClient = null;
42
- flightService = null;
40
+ transport = null;
43
41
  authToken = null;
44
42
  connected = false;
45
43
  constructor(options) {
@@ -50,6 +48,10 @@ export class FlightSqlClient {
50
48
  connectTimeoutMs: options.connectTimeoutMs ?? defaultConnectTimeoutMs,
51
49
  requestTimeoutMs: options.requestTimeoutMs ?? defaultRequestTimeoutMs
52
50
  };
51
+ // Use provided transport or create one for the runtime
52
+ if (options.transport !== undefined) {
53
+ this.transport = options.transport;
54
+ }
53
55
  }
54
56
  // ===========================================================================
55
57
  // Connection Lifecycle
@@ -65,25 +67,17 @@ export class FlightSqlClient {
65
67
  return;
66
68
  }
67
69
  try {
68
- // Load the Flight service definition
69
- const packageDef = await getFlightServiceDefinition();
70
- // Navigate to arrow.flight.protocol.FlightService
71
- const arrowPackage = packageDef.arrow;
72
- const flightPackage = arrowPackage.flight;
73
- const protocolPackage = flightPackage.protocol;
74
- this.flightService = protocolPackage.FlightService;
75
- // Create channel credentials
76
- const credentials = this.options.credentials ?? this.createCredentials();
77
- // Create the gRPC client
78
- const address = `${this.options.host}:${String(this.options.port)}`;
79
- this.grpcClient = new this.flightService(address, credentials, {
80
- "grpc.max_receive_message_length": -1, // Unlimited
81
- "grpc.max_send_message_length": -1,
82
- "grpc.keepalive_time_ms": 30_000,
83
- "grpc.keepalive_timeout_ms": 10_000
70
+ // Create transport if not provided
71
+ this.transport ??= getTransportForRuntime({
72
+ host: this.options.host,
73
+ port: this.options.port,
74
+ tls: this.options.tls,
75
+ credentials: this.options.credentials,
76
+ connectTimeoutMs: this.options.connectTimeoutMs,
77
+ requestTimeoutMs: this.options.requestTimeoutMs
84
78
  });
85
- // Wait for the channel to be ready
86
- await this.waitForReady();
79
+ // Connect the transport
80
+ await this.transport.connect();
87
81
  // Perform authentication handshake if configured
88
82
  if (this.options.auth && this.options.auth.type !== "none") {
89
83
  await this.authenticate(this.options.auth);
@@ -215,12 +209,19 @@ export class FlightSqlClient {
215
209
  let datasetSchema = null;
216
210
  let parameterSchema = null;
217
211
  for await (const result of this.doAction(action)) {
212
+ // The result body may be wrapped in a protobuf Any envelope
213
+ // Try to unwrap it first, otherwise parse directly
214
+ let messageBytes = result.body;
215
+ const anyWrapper = unwrapAny(result.body);
216
+ if (anyWrapper) {
217
+ messageBytes = anyWrapper.value;
218
+ }
218
219
  // Parse ActionCreatePreparedStatementResult
219
220
  // Fields:
220
221
  // 1: prepared_statement_handle (bytes)
221
222
  // 2: dataset_schema (bytes) - Arrow IPC schema
222
223
  // 3: parameter_schema (bytes) - Arrow IPC schema
223
- const fields = parseProtoFields(result.body);
224
+ const fields = parseProtoFields(messageBytes);
224
225
  handle = getBytesField(fields, 1);
225
226
  const datasetSchemaBytes = getBytesField(fields, 2);
226
227
  const parameterSchemaBytes = getBytesField(fields, 3);
@@ -519,19 +520,19 @@ export class FlightSqlClient {
519
520
  * @returns FlightInfo with schema and endpoints
520
521
  */
521
522
  async getFlightInfo(descriptor) {
522
- this.ensureConnected();
523
- return new Promise((resolve, reject) => {
524
- const client = this.grpcClient;
523
+ const transport = this.getConnectedTransport();
524
+ try {
525
525
  const metadata = this.createRequestMetadata();
526
- const request = this.serializeFlightDescriptor(descriptor);
527
- client.getFlightInfo(request, metadata, (error, response) => {
528
- if (error) {
529
- reject(this.wrapGrpcError(error));
530
- return;
531
- }
532
- resolve(this.parseFlightInfo(response));
533
- });
534
- });
526
+ const rawInfo = await transport.getFlightInfo({
527
+ type: descriptor.type,
528
+ cmd: descriptor.cmd,
529
+ path: descriptor.path
530
+ }, metadata);
531
+ return this.parseFlightInfo(rawInfo);
532
+ }
533
+ catch (error) {
534
+ throw this.wrapTransportError(error);
535
+ }
535
536
  }
536
537
  /**
537
538
  * Get the schema for a flight descriptor without fetching data.
@@ -540,19 +541,19 @@ export class FlightSqlClient {
540
541
  * @returns Schema result
541
542
  */
542
543
  async getSchema(descriptor) {
543
- this.ensureConnected();
544
- return new Promise((resolve, reject) => {
545
- const client = this.grpcClient;
544
+ const transport = this.getConnectedTransport();
545
+ try {
546
546
  const metadata = this.createRequestMetadata();
547
- const request = this.serializeFlightDescriptor(descriptor);
548
- client.getSchema(request, metadata, (error, response) => {
549
- if (error) {
550
- reject(this.wrapGrpcError(error));
551
- return;
552
- }
553
- resolve(this.parseSchemaResult(response));
554
- });
555
- });
547
+ const rawSchema = await transport.getSchema({
548
+ type: descriptor.type,
549
+ cmd: descriptor.cmd,
550
+ path: descriptor.path
551
+ }, metadata);
552
+ return this.parseSchemaResult(rawSchema);
553
+ }
554
+ catch (error) {
555
+ throw this.wrapTransportError(error);
556
+ }
556
557
  }
557
558
  /**
558
559
  * Retrieve data for a ticket as an async iterator of FlightData.
@@ -561,17 +562,20 @@ export class FlightSqlClient {
561
562
  * @yields FlightData chunks containing dataHeader and dataBody
562
563
  */
563
564
  async *doGet(ticket) {
564
- this.ensureConnected();
565
- const client = this.grpcClient;
565
+ const transport = this.getConnectedTransport();
566
566
  const metadata = this.createRequestMetadata();
567
- const request = { ticket: ticket.ticket };
568
- const stream = client.doGet(request, metadata);
567
+ const stream = transport.doGet({ ticket: ticket.ticket }, metadata);
569
568
  try {
570
- for await (const data of this.wrapStream(stream)) {
571
- const flightData = data;
572
- yield flightData;
569
+ for await (const data of stream) {
570
+ yield {
571
+ dataHeader: data.dataHeader,
572
+ dataBody: data.dataBody
573
+ };
573
574
  }
574
575
  }
576
+ catch (error) {
577
+ throw this.wrapTransportError(error);
578
+ }
575
579
  finally {
576
580
  stream.cancel();
577
581
  }
@@ -584,16 +588,10 @@ export class FlightSqlClient {
584
588
  * @returns Async iterator of PutResult messages
585
589
  */
586
590
  async *doPut(descriptor, dataStream) {
587
- this.ensureConnected();
588
- const client = this.grpcClient;
591
+ const transport = this.getConnectedTransport();
589
592
  const requestMetadata = this.createRequestMetadata();
590
593
  // Create bidirectional stream
591
- const stream = client.doPut(requestMetadata);
592
- // Use object wrapper to track errors (allows TypeScript to understand mutation)
593
- const errorState = { error: null };
594
- stream.on("error", (err) => {
595
- errorState.error = err;
596
- });
594
+ const stream = transport.doPut(requestMetadata);
597
595
  // Send the first message with the descriptor
598
596
  const firstData = await this.getFirstFromIterable(dataStream);
599
597
  if (firstData) {
@@ -606,9 +604,6 @@ export class FlightSqlClient {
606
604
  }
607
605
  // Send remaining data
608
606
  for await (const data of dataStream) {
609
- if (errorState.error) {
610
- throw this.wrapGrpcError(errorState.error);
611
- }
612
607
  stream.write({
613
608
  dataHeader: data.dataHeader,
614
609
  dataBody: data.dataBody,
@@ -619,16 +614,12 @@ export class FlightSqlClient {
619
614
  stream.end();
620
615
  // Read responses
621
616
  try {
622
- for await (const result of this.wrapStream(stream)) {
623
- const putResult = result;
624
- yield { appMetadata: putResult.appMetadata };
617
+ for await (const result of stream) {
618
+ yield { appMetadata: result.appMetadata };
625
619
  }
626
620
  }
627
621
  catch (error) {
628
- if (errorState.error) {
629
- throw this.wrapGrpcError(errorState.error);
630
- }
631
- throw error;
622
+ throw this.wrapTransportError(error);
632
623
  }
633
624
  }
634
625
  /**
@@ -662,25 +653,15 @@ export class FlightSqlClient {
662
653
  * ```
663
654
  */
664
655
  doExchange(descriptor) {
665
- this.ensureConnected();
666
- const client = this.grpcClient;
656
+ const transport = this.getConnectedTransport();
667
657
  const metadata = this.createRequestMetadata();
668
- const stream = client.doExchange(metadata);
669
- // Use object wrapper to track errors
670
- const errorState = { error: null };
671
- stream.on("error", (err) => {
672
- errorState.error = err;
673
- });
658
+ const stream = transport.doExchange(metadata);
674
659
  // Capture references for closure
675
- const wrapGrpcError = this.wrapGrpcError.bind(this);
660
+ const wrapTransportError = this.wrapTransportError.bind(this);
676
661
  const serializeFlightDescriptor = this.serializeFlightDescriptor.bind(this);
677
- const wrapStream = this.wrapStream.bind(this);
678
662
  // Create the exchange handle
679
663
  const exchange = {
680
664
  async send(data) {
681
- if (errorState.error) {
682
- throw wrapGrpcError(errorState.error);
683
- }
684
665
  stream.write({
685
666
  flightDescriptor: data.flightDescriptor
686
667
  ? serializeFlightDescriptor(data.flightDescriptor)
@@ -700,27 +681,23 @@ export class FlightSqlClient {
700
681
  },
701
682
  async *[Symbol.asyncIterator]() {
702
683
  try {
703
- for await (const data of wrapStream(stream)) {
704
- const flightData = data;
684
+ for await (const data of stream) {
705
685
  yield {
706
- flightDescriptor: flightData.flightDescriptor
686
+ flightDescriptor: data.flightDescriptor
707
687
  ? {
708
- type: flightData.flightDescriptor.type,
709
- cmd: flightData.flightDescriptor.cmd,
710
- path: flightData.flightDescriptor.path
688
+ type: data.flightDescriptor.type,
689
+ cmd: data.flightDescriptor.cmd,
690
+ path: data.flightDescriptor.path
711
691
  }
712
692
  : undefined,
713
- dataHeader: flightData.dataHeader ?? new Uint8Array(),
714
- dataBody: flightData.dataBody ?? new Uint8Array(),
715
- appMetadata: flightData.appMetadata
693
+ dataHeader: data.dataHeader ?? new Uint8Array(),
694
+ dataBody: data.dataBody ?? new Uint8Array(),
695
+ appMetadata: data.appMetadata
716
696
  };
717
697
  }
718
698
  }
719
699
  catch (error) {
720
- if (errorState.error) {
721
- throw wrapGrpcError(errorState.error);
722
- }
723
- throw error;
700
+ throw wrapTransportError(error);
724
701
  }
725
702
  }
726
703
  };
@@ -779,16 +756,17 @@ export class FlightSqlClient {
779
756
  * @returns Async iterator of results
780
757
  */
781
758
  async *doAction(action) {
782
- this.ensureConnected();
783
- const client = this.grpcClient;
759
+ const transport = this.getConnectedTransport();
784
760
  const metadata = this.createRequestMetadata();
785
- const request = { type: action.type, body: action.body };
786
- const stream = client.doAction(request, metadata);
761
+ const stream = transport.doAction({ type: action.type, body: action.body }, metadata);
787
762
  try {
788
- for await (const result of this.wrapStream(stream)) {
789
- yield result;
763
+ for await (const result of stream) {
764
+ yield { body: result.body ?? new Uint8Array() };
790
765
  }
791
766
  }
767
+ catch (error) {
768
+ throw this.wrapTransportError(error);
769
+ }
792
770
  finally {
793
771
  stream.cancel();
794
772
  }
@@ -799,13 +777,20 @@ export class FlightSqlClient {
799
777
  * @returns Array of available action types
800
778
  */
801
779
  async listActions() {
802
- this.ensureConnected();
803
- const client = this.grpcClient;
780
+ const transport = this.getConnectedTransport();
804
781
  const metadata = this.createRequestMetadata();
805
- const stream = client.listActions({}, metadata);
782
+ const stream = transport.listActions(metadata);
806
783
  const actions = [];
807
- for await (const action of this.wrapStream(stream)) {
808
- actions.push(action);
784
+ try {
785
+ for await (const action of stream) {
786
+ actions.push({
787
+ type: action.type,
788
+ description: action.description ?? ""
789
+ });
790
+ }
791
+ }
792
+ catch (error) {
793
+ throw this.wrapTransportError(error);
809
794
  }
810
795
  return actions;
811
796
  }
@@ -833,30 +818,35 @@ export class FlightSqlClient {
833
818
  }
834
819
  }
835
820
  async handshake(username, password) {
836
- return new Promise((resolve, reject) => {
837
- const client = this.grpcClient;
838
- const stream = client.handshake();
839
- // Build BasicAuth payload
840
- const authPayload = this.encodeBasicAuth(username, password);
841
- stream.on("data", (response) => {
842
- resolve({
843
- protocolVersion: BigInt(response.protocolVersion ?? "0"),
844
- payload: response.payload ?? new Uint8Array()
845
- });
846
- });
847
- stream.on("error", (error) => {
848
- reject(new AuthenticationError("Handshake failed", { cause: error }));
849
- });
850
- stream.on("end", () => {
851
- // Stream ended without response
852
- });
853
- // Send handshake request
854
- stream.write({
855
- protocolVersion: "1",
856
- payload: authPayload
857
- });
858
- stream.end();
821
+ // Note: Called during connect(), so transport exists but connected flag isn't set yet
822
+ if (this.transport === null) {
823
+ throw new ConnectionError("Transport not initialized");
824
+ }
825
+ const stream = this.transport.handshake();
826
+ // Build BasicAuth payload
827
+ const authPayload = this.encodeBasicAuth(username, password);
828
+ // Send handshake request
829
+ stream.write({
830
+ protocolVersion: 1,
831
+ payload: authPayload
859
832
  });
833
+ stream.end();
834
+ // Read response
835
+ try {
836
+ for await (const response of stream) {
837
+ return {
838
+ protocolVersion: BigInt(response.protocolVersion ?? 0),
839
+ payload: response.payload ?? new Uint8Array()
840
+ };
841
+ }
842
+ throw new AuthenticationError("Handshake failed: no response received");
843
+ }
844
+ catch (error) {
845
+ if (error instanceof AuthenticationError) {
846
+ throw error;
847
+ }
848
+ throw new AuthenticationError("Handshake failed", { cause: error });
849
+ }
860
850
  }
861
851
  encodeBasicAuth(username, password) {
862
852
  // Simple encoding: "username:password" as UTF-8 bytes
@@ -865,101 +855,62 @@ export class FlightSqlClient {
865
855
  return encoder.encode(`${username}:${password}`);
866
856
  }
867
857
  // ===========================================================================
868
- // Private: gRPC Helpers
858
+ // Private: Transport Helpers
869
859
  // ===========================================================================
870
- createCredentials() {
871
- if (this.options.tls) {
872
- return grpc.credentials.createSsl();
873
- }
874
- return grpc.credentials.createInsecure();
875
- }
876
- async waitForReady() {
877
- return new Promise((resolve, reject) => {
878
- const deadline = Date.now() + this.options.connectTimeoutMs;
879
- const client = this.grpcClient;
880
- if (!client) {
881
- reject(new ConnectionError("gRPC client not initialized"));
882
- return;
883
- }
884
- client.waitForReady(deadline, (error) => {
885
- if (error) {
886
- reject(new ConnectionError("Connection timeout", { cause: error }));
887
- }
888
- else {
889
- resolve();
890
- }
891
- });
892
- });
893
- }
894
860
  createRequestMetadata() {
895
- const metadata = new grpc.Metadata();
861
+ const metadata = {};
896
862
  // Add auth token if present
897
863
  if (this.authToken !== null && this.authToken !== "") {
898
- metadata.set("authorization", `Bearer ${this.authToken}`);
864
+ metadata.authorization = `Bearer ${this.authToken}`;
899
865
  }
900
866
  // Add custom metadata from options
901
867
  if (this.options.metadata) {
902
868
  for (const [key, value] of Object.entries(this.options.metadata)) {
903
- metadata.set(key, value);
869
+ metadata[key] = value;
904
870
  }
905
871
  }
906
872
  return metadata;
907
873
  }
908
874
  cleanup() {
909
- if (this.grpcClient) {
910
- this.grpcClient.close();
911
- this.grpcClient = null;
875
+ if (this.transport) {
876
+ this.transport.close();
877
+ this.transport = null;
912
878
  }
913
- this.flightService = null;
914
879
  this.authToken = null;
915
880
  this.connected = false;
916
881
  }
917
882
  ensureConnected() {
918
- if (!this.connected) {
883
+ if (!this.connected || this.transport === null) {
919
884
  throw new ConnectionError("Client is not connected. Call connect() first.");
920
885
  }
921
886
  }
922
- wrapGrpcError(error) {
923
- const message = error.details || error.message;
924
- switch (error.code) {
925
- case grpc.status.UNAUTHENTICATED:
926
- return new AuthenticationError(message, { cause: error });
927
- case grpc.status.UNAVAILABLE:
928
- case grpc.status.DEADLINE_EXCEEDED:
929
- return new ConnectionError(message, { cause: error });
930
- case grpc.status.OK:
931
- case grpc.status.CANCELLED:
932
- case grpc.status.UNKNOWN:
933
- case grpc.status.INVALID_ARGUMENT:
934
- case grpc.status.NOT_FOUND:
935
- case grpc.status.ALREADY_EXISTS:
936
- case grpc.status.PERMISSION_DENIED:
937
- case grpc.status.RESOURCE_EXHAUSTED:
938
- case grpc.status.FAILED_PRECONDITION:
939
- case grpc.status.ABORTED:
940
- case grpc.status.OUT_OF_RANGE:
941
- case grpc.status.UNIMPLEMENTED:
942
- case grpc.status.INTERNAL:
943
- case grpc.status.DATA_LOSS:
944
- return new FlightSqlError(message, { cause: error });
945
- }
946
- }
947
- // ===========================================================================
948
- // Private: Streaming Helpers
949
- // ===========================================================================
950
887
  /**
951
- * Wraps a gRPC stream to convert errors to FlightSqlError types.
952
- * gRPC streams are already async iterable, so we just need error handling.
888
+ * Get the transport, throwing if not connected.
889
+ * This is a helper to avoid non-null assertions after ensureConnected().
953
890
  */
954
- async *wrapStream(stream) {
955
- try {
956
- for await (const data of stream) {
957
- yield data;
958
- }
891
+ getConnectedTransport() {
892
+ if (!this.connected || this.transport === null) {
893
+ throw new ConnectionError("Client is not connected. Call connect() first.");
959
894
  }
960
- catch (error) {
961
- throw this.wrapGrpcError(error);
895
+ return this.transport;
896
+ }
897
+ wrapTransportError(error) {
898
+ // Handle TransportError with gRPC status codes
899
+ const transportError = error;
900
+ const message = transportError.details ?? error.message;
901
+ if (transportError.code !== undefined) {
902
+ // gRPC status codes
903
+ switch (transportError.code) {
904
+ case 16: // UNAUTHENTICATED
905
+ return new AuthenticationError(message, { cause: error });
906
+ case 14: // UNAVAILABLE
907
+ case 4: // DEADLINE_EXCEEDED
908
+ return new ConnectionError(message, { cause: error });
909
+ default:
910
+ return new FlightSqlError(message, { cause: error });
911
+ }
962
912
  }
913
+ return new FlightSqlError(message, { cause: error });
963
914
  }
964
915
  // ===========================================================================
965
916
  // Private: Serialization Helpers