LCOV - code coverage report
Current view: top level - dashql-app/src/connections/hyper/platform - native_hyperdb_grpc_client.ts Coverage Total Hit
Test: DashQL 915828b Lines: 88.3 % 154 136
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as arrow from 'apache-arrow';
       2              : import * as buf from "@bufbuild/protobuf";
       3              : import * as pb from "../../../proto.js";
       4              : import * as connection from '@ankoh/dashql-jsonschema/connection.js';
       5              : 
       6              : import {
       7              :     HyperDatabaseChannel,
       8              :     HyperDatabaseClient,
       9              :     HyperDatabaseConnectionContext,
      10              :     HyperQueryExecutionOptions,
      11              :     HyperQueryResultStream,
      12              : } from '../hyperdb_grpc_client.js';
      13              : import {
      14              :     NativeGrpcChannel,
      15              :     NativeGrpcClient,
      16              :     NativeGrpcProxyConfig,
      17              :     NativeGrpcServerStream,
      18              :     NativeGrpcServerStreamMessageIterator,
      19              : } from '../../../platform/grpc/native_grpc_client.js';
      20              : import {
      21              :     createQueryResponseStreamMetrics,
      22              :     QueryExecutionProgress,
      23              :     QueryExecutionResponseStream, QueryExecutionMetrics,
      24              :     QueryExecutionStatus,
      25              : } from '../../../query/query_execution_state.js';
      26              : import { ChannelArgs, ChannelError } from '../../../platform/channel_common.js';
      27              : import { decodeGrpcErrorHeaders, HyperQueryError } from '../hyper_error_info.js';
      28              : import { Logger } from '../../../platform/logger/logger.js';
      29              : import { AsyncConsumer } from '../../../utils/async_consumer.js';
      30              : import { AsyncValue } from '../../../utils/async_value.js';
      31              : import { yieldToBrowser } from '../../../utils/yield_to_browser.js';
      32              : 
      33            1 : const LOG_CTX = "native_hyperdb_grpc_client";
      34            1 : const STREAM_YIELD_INTERVAL = 8;
      35              : 
      36            1 : export class QueryResultReader implements AsyncIterator<Uint8Array>, AsyncIterable<Uint8Array> {
      37              :     /// The gRPC stream
      38            1 :     grpcStream: NativeGrpcServerStream;
      39              :     /// The logger
      40            1 :     logger: Logger;
      41              :     /// The message iterator
      42            1 :     messageIterator: NativeGrpcServerStreamMessageIterator;
      43              :     /// The current status
      44            1 :     currentStatus: QueryExecutionStatus;
      45              :     /// The metrics
      46            1 :     metrics: QueryExecutionMetrics;
      47              : 
      48            1 :     constructor(stream: NativeGrpcServerStream, logger: Logger) {
      49            1 :         this.grpcStream = stream;
      50            1 :         this.logger = logger;
      51            1 :         this.messageIterator = new NativeGrpcServerStreamMessageIterator(this.grpcStream, logger);
      52            1 :         this.currentStatus = QueryExecutionStatus.RUNNING;
      53            1 :         this.metrics = createQueryResponseStreamMetrics();
      54            1 :     }
      55              : 
      56              :     /// Get the result metadata (if any)
      57            0 :     get metadata() { return this.messageIterator.metadata; }
      58              :     /// Get the next binary result chunk
      59            1 :     async next(): Promise<IteratorResult<Uint8Array>> {
      60            1 :         while (true) {
      61            1 :             const next = await this.messageIterator.next();
      62            1 :             if (next.value == null) {
      63            1 :                 return { done: true, value: null };
      64              :             }
      65            1 :             const resultMessage = buf.fromBinary(pb.salesforce_hyperdb_grpc_v1.pb.QueryResultSchema$, next.value);
      66            1 :             switch (resultMessage.result.case) {
      67              :                 // We skip any dedicated header prefix
      68            1 :                 case "header":
      69            1 :                     continue;
      70              :                 // Skip qsv1 chunks
      71            1 :                 case "qsv1Chunk":
      72            1 :                     throw new Error("invalid result data message. expected arrowChunk, received qsv1Chunk");
      73              :                 // Unpack an arrow chunk
      74            1 :                 case "arrowChunk": {
      75            1 :                     const buffer = resultMessage.result.value.data;
      76            1 :                     this.metrics.totalDataBytesReceived += buffer.byteLength;
      77            1 :                     return { done: false, value: buffer };
      78            1 :                 }
      79            1 :             }
      80            1 :         }
      81            1 :     }
      82              : 
      83              :     /// Get the async iterator
      84            1 :     [Symbol.asyncIterator]() {
      85            1 :         return this;
      86            1 :     }
      87            1 : }
      88              : 
      89              : /// A native Hyper query result stream
      90            1 : export class NativeHyperQueryResultStream implements QueryExecutionResponseStream {
      91              :     /// The query result iterator
      92            1 :     resultReader: QueryResultReader;
      93              :     /// The schema Promise
      94            1 :     resultSchema: AsyncValue<arrow.Schema<any>, Error>;
      95              : 
      96            1 :     constructor(stream: NativeGrpcServerStream, _connection: HyperDatabaseConnectionContext, logger: Logger) {
      97            1 :         this.resultReader = new QueryResultReader(stream, logger);
      98            1 :         this.resultSchema = new AsyncValue();
      99            1 :     }
     100            0 :     async cancel(): Promise<void> {
     101            0 :         await this.resultReader.grpcStream.destroy();
     102            0 :     }
     103              : 
     104              :     /// Get the metadata
     105            0 :     getMetadata(): Map<string, string> {
     106            0 :         return this.resultReader.metadata;
     107            0 :     }
     108              :     /// Get the metrics
     109            1 :     getMetrics(): QueryExecutionMetrics {
     110            1 :         return this.resultReader.metrics;
     111            1 :     }
     112              :     /// Get the current query status
     113            0 :     getStatus(): QueryExecutionStatus {
     114            0 :         return this.resultReader.currentStatus;
     115            0 :     }
     116              :     /// Await the Arrow schema
     117            1 :     getSchema(): Promise<arrow.Schema> {
     118            1 :         return this.resultSchema.getValue();
     119            1 :     }
     120              :     /// Produce the result batches
     121            1 :     async produce(batches: AsyncConsumer<QueryExecutionResponseStream, arrow.RecordBatch>, _progress: AsyncConsumer<QueryExecutionResponseStream, QueryExecutionProgress>, abort?: AbortSignal): Promise<void> {
     122            1 :         const cancel = () => { void this.cancel(); };
     123            1 :         abort?.addEventListener('abort', cancel, { once: true });
     124            1 :         try {
     125            1 :             const arrowReader = await arrow.AsyncRecordBatchStreamReader.from(this.resultReader);
     126            1 :             abort?.throwIfAborted();
     127              : 
     128            1 :             await arrowReader.open();
     129            1 :             abort?.throwIfAborted();
     130            1 :             this.resultSchema.resolve(arrowReader.schema);
     131              : 
     132            1 :             let batchesRead = 0;
     133            1 :             while (true) {
     134            1 :                 const iter = await arrowReader!.next();
     135            1 :                 abort?.throwIfAborted();
     136            1 :                 if (iter.done) {
     137            1 :                     if (iter.value !== undefined) {
     138            0 :                         batches.resolve(this, iter.value);
     139              :                     }
     140            1 :                     return;
     141            1 :                 }
     142            1 :                 batches.resolve(this, iter.value);
     143            1 :                 if (++batchesRead % STREAM_YIELD_INTERVAL === 0) await yieldToBrowser();
     144            1 :             }
     145            1 :         } catch (e: unknown) {
     146              :             // A failed gRPC stream surfaces as a ChannelError whose headers carry
     147              :             // the forwarded gRPC richer error model. Decode it into a structured
     148              :             // HyperQueryError so the rest of the app sees Hyper's ErrorInfo.
     149            1 :             if (e instanceof ChannelError) {
     150            0 :                 const info = decodeGrpcErrorHeaders(e.headers);
     151            0 :                 if (info) {
     152            0 :                     throw new HyperQueryError(info, LOG_CTX);
     153              :                 }
     154            0 :             }
     155            1 :             throw e;
     156            1 :         } finally {
     157            1 :             abort?.removeEventListener('abort', cancel);
     158            1 :         }
     159            1 :     }
     160            1 : }
     161              : 
     162              : /// A native Hyper database connection
     163            1 : class NativeHyperDatabaseChannel implements HyperDatabaseChannel {
     164              :     /// A gRPC channel
     165            1 :     grpcChannel: NativeGrpcChannel;
     166              :     /// The connection context
     167            1 :     connection: HyperDatabaseConnectionContext;
     168              :     /// A logger
     169            1 :     logger: Logger;
     170              : 
     171            1 :     constructor(channel: NativeGrpcChannel, connection: HyperDatabaseConnectionContext, logger: Logger) {
     172            1 :         this.grpcChannel = channel;
     173            1 :         this.connection = connection;
     174            1 :         this.logger = logger;
     175            1 :     }
     176              : 
     177              :     /// Execute a query against Hyper
     178            1 :     public async executeQuery(params: pb.salesforce_hyperdb_grpc_v1.pb.QueryParam, abort?: AbortSignal, options?: HyperQueryExecutionOptions): Promise<HyperQueryResultStream> {
     179            1 :         abort?.throwIfAborted();
     180            1 :         params.outputFormat = pb.salesforce_hyperdb_grpc_v1.pb.QueryParam_OutputFormat.ARROW_STREAM;
     181            1 :         for (const db of this.connection.getAttachedDatabases()) {
     182            1 :             params.database.push(buf.create(pb.salesforce_hyperdb_grpc_v1.pb.AttachedDatabaseSchema, db));
     183              :         }
     184            1 :         for (const [key, value] of Object.entries(this.connection.getQueryParameters())) {
     185            1 :             params.params[key] = value;
     186              :         }
     187            1 :         const clientContext = this.connection.getClientContext?.();
     188            1 :         if (clientContext != null) params.params.external_client_context = JSON.stringify(clientContext);
     189            1 :         const stream = await this.grpcChannel.startServerStream({
     190            1 :             path: "/salesforce.hyperdb.grpc.v1.HyperService/ExecuteQuery",
     191            1 :             body: buf.toBinary(pb.salesforce_hyperdb_grpc_v1.pb.QueryParamSchema, params),
     192            1 :             readTimeoutMs: options?.readTimeoutMs,
     193            1 :         });
     194            1 :         if (abort?.aborted) {
     195            0 :             await stream.destroy();
     196            0 :             abort.throwIfAborted();
     197            0 :         }
     198            1 :         return new NativeHyperQueryResultStream(stream, this.connection, this.logger);
     199            1 :     }
     200              : 
     201              :     /// Close the connection
     202            1 :     public async close(): Promise<void> {
     203            1 :         await this.grpcChannel.close();
     204            1 :     }
     205            1 : }
     206              : 
     207              : /// A native Hyper database client
     208            1 : export class NativeHyperDatabaseClient implements HyperDatabaseClient {
     209              :     /// A logger
     210            1 :     logger: Logger;
     211              :     /// A native Hyper client
     212            1 :     client: NativeGrpcClient;
     213              : 
     214            1 :     constructor(config: NativeGrpcProxyConfig, logger: Logger) {
     215            1 :         this.logger = logger;
     216            1 :         this.client = new NativeGrpcClient(config, logger);
     217            1 :     }
     218              : 
     219              :     /// Create a database connection
     220            1 :     public async connect(hyperArgs: connection.HyperConnectionParams, connection: HyperDatabaseConnectionContext): Promise<NativeHyperDatabaseChannel> {
     221            1 :         const tls = hyperArgs.tls;
     222            1 :         const isHttps = new URL(hyperArgs.endpoint).protocol === "https:";
     223            1 :         const useTls = isHttps || Boolean(
     224            1 :             tls?.clientKeyPath || tls?.clientCertPath || tls?.caCertsPath,
     225            1 :         );
     226            1 :         if (useTls && !isHttps) {
     227            1 :             throw new Error("TLS certificate paths require an https:// endpoint");
     228              :         }
     229            1 :         const args: ChannelArgs = {
     230            1 :             endpoint: hyperArgs.endpoint,
     231            1 :             tls: useTls ? {
     232            1 :                 keyPath: tls?.clientKeyPath,
     233            1 :                 pubPath: tls?.clientCertPath,
     234            1 :                 caPath: tls?.caCertsPath,
     235            1 :             } : undefined
     236            1 :         };
     237            1 :         const channel = await this.client.connect(args, connection);
     238            1 :         return new NativeHyperDatabaseChannel(channel, connection, this.logger);
     239            1 :     }
     240            1 : }
        

Generated by: LCOV version 2.0-1