LCOV - code coverage report
Current view: top level - dashql-app/src/connections/embedded - embedded_database_channel.ts Coverage Total Hit
Test: DashQL 915828b Lines: 97.4 % 115 112
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import type * as arrow from 'apache-arrow';
       2              : import type * as pb from '../../proto.js';
       3              : 
       4              : import type { EmbeddedConnection, EmbeddedResultStream } from '../../platform/database/embedded_database.js';
       5              : import type { AsyncConsumer } from '../../utils/async_consumer.js';
       6              : import {
       7              :     createQueryResponseStreamMetrics,
       8              :     QueryExecutionProgress,
       9              :     QueryExecutionResponseStream,
      10              :     QueryExecutionStatus,
      11              :     type QueryExecutionMetrics,
      12              : } from '../../query/query_execution_state.js';
      13              : import type { HyperDatabaseChannel } from '../hyper/hyperdb_grpc_client.js';
      14              : import { yieldToBrowser } from '../../utils/yield_to_browser.js';
      15              : 
      16            1 : const STREAM_YIELD_INTERVAL = 8;
      17              : 
      18              : /// A query result that streams the record batches as the embedded database produces them.
      19            1 : class EmbeddedQueryResultStream implements QueryExecutionResponseStream {
      20            1 :     private readonly metrics: QueryExecutionMetrics;
      21            1 :     private status = QueryExecutionStatus.RUNNING;
      22              :     /// Resolves once the result is read to the end or cancelled, the connection is free again then
      23            1 :     readonly finished: Promise<void>;
      24            1 :     private resolveFinished!: () => void;
      25              : 
      26            1 :     constructor(
      27              :         /// The result batches
      28            1 :         private readonly reader: EmbeddedResultStream,
      29              :         /// The time at which the query was started
      30            1 :         private readonly startedAt: number,
      31            1 :     ) {
      32            1 :         this.metrics = createQueryResponseStreamMetrics();
      33            1 :         this.metrics.totalQueryRequestsStarted = 1;
      34            1 :         this.finished = new Promise(resolve => { this.resolveFinished = resolve; });
      35            1 :     }
      36              : 
      37            1 :     async cancel(): Promise<void> {
      38            1 :         try {
      39            1 :             await this.reader.cancel();
      40            1 :         } finally {
      41            1 :             if (this.status === QueryExecutionStatus.RUNNING) this.status = QueryExecutionStatus.CANCELLED;
      42            1 :             this.resolveFinished();
      43            1 :         }
      44            1 :     }
      45              : 
      46            1 :     getMetadata(): Map<string, string> {
      47            1 :         return new Map();
      48            1 :     }
      49              : 
      50            1 :     getMetrics(): QueryExecutionMetrics {
      51            1 :         return this.metrics;
      52            1 :     }
      53              : 
      54            1 :     getStatus(): QueryExecutionStatus {
      55            1 :         return this.status;
      56            1 :     }
      57              : 
      58            1 :     async getSchema(): Promise<arrow.Schema> {
      59            1 :         return this.reader.schema;
      60            1 :     }
      61              : 
      62              :     /// Produce the batches, the abort signal of the query cancels it in the database
      63            1 :     async produce(
      64            1 :         batches: AsyncConsumer<QueryExecutionResponseStream, arrow.RecordBatch>,
      65            1 :         _progress: AsyncConsumer<QueryExecutionResponseStream, QueryExecutionProgress>,
      66            1 :         abort?: AbortSignal,
      67            1 :     ): Promise<void> {
      68            1 :         try {
      69            1 :             for (;;) {
      70            1 :                 abort?.throwIfAborted();
      71            1 :                 const next = await this.reader.next();
      72            1 :                 if (next.done) break;
      73            1 :                 const batch = next.value;
      74            1 :                 this.metrics.durationUntilFirstBatchMs ??= performance.now() - this.startedAt;
      75            1 :                 this.metrics.totalDataBytesReceived += batch.data.byteLength;
      76            1 :                 this.metrics.totalBatchesReceived += 1;
      77            1 :                 this.metrics.totalRowsReceived += batch.numRows;
      78            1 :                 batches.resolve(this, batch);
      79            1 :                 if (this.metrics.totalBatchesReceived % STREAM_YIELD_INTERVAL === 0) await yieldToBrowser();
      80            1 :             }
      81            1 :             this.metrics.totalQueryRequestsSucceeded = 1;
      82            1 :             this.status = QueryExecutionStatus.SUCCEEDED;
      83            1 :         } catch (error) {
      84            1 :             this.metrics.totalQueryRequestsFailed = 1;
      85            1 :             this.status = abort?.aborted ? QueryExecutionStatus.CANCELLED : QueryExecutionStatus.FAILED;
      86              :             // Stop the query when the batches are not read to the end
      87            1 :             await this.reader.cancel().catch(() => {});
      88            1 :             throw error;
      89            1 :         } finally {
      90            1 :             this.metrics.totalQueryRequestDurationMs = performance.now() - this.startedAt;
      91            1 :             this.resolveFinished();
      92            1 :         }
      93            1 :     }
      94            1 : }
      95              : 
      96              : /// Wait for a promise, an abort rejects right away
      97            1 : function waitUnlessAborted(promise: Promise<void>, abort?: AbortSignal): Promise<void> {
      98            1 :     if (abort == null) return promise;
      99            1 :     return new Promise((resolve, reject) => {
     100            1 :         const onAbort = () => reject(abort.reason);
     101            1 :         abort.addEventListener('abort', onAbort, { once: true });
     102            1 :         void promise.then(() => {
     103            1 :             abort.removeEventListener('abort', onAbort);
     104            1 :             resolve();
     105            1 :         });
     106            1 :     });
     107            1 : }
     108              : 
     109            1 : export class EmbeddedDatabaseChannel {
     110              :     /// The connection runs one statement at a time, so queries wait until the result of the previous one is read.
     111              :     /// One connection keeps the session state of the scripts, such as settings and attached databases.
     112            1 :     private previousQuery: Promise<void> = Promise.resolve();
     113              : 
     114            1 :     constructor(private readonly connection: EmbeddedConnection) {}
     115              : 
     116            1 :     async executeQuery(query: string, abort?: AbortSignal): Promise<QueryExecutionResponseStream> {
     117            1 :         abort?.throwIfAborted();
     118            1 :         const previous = this.previousQuery;
     119            1 :         let release!: () => void;
     120            1 :         const finished = new Promise<void>(resolve => { release = resolve; });
     121              :         // The next query waits for the previous one as well, this query may be cancelled while it waits
     122            1 :         this.previousQuery = previous.then(() => finished);
     123            1 :         try {
     124            1 :             await waitUnlessAborted(previous, abort);
     125            1 :             abort?.throwIfAborted();
     126            1 :             const startedAt = performance.now();
     127            1 :             const stream = new EmbeddedQueryResultStream(await this.connection.queryStream(query, abort), startedAt);
     128            1 :             void stream.finished.then(release);
     129            1 :             return stream;
     130            1 :         } catch (error) {
     131            1 :             release();
     132            1 :             throw error;
     133            1 :         }
     134            1 :     }
     135              : 
     136            1 :     async close(): Promise<void> {
     137            1 :         await this.connection.close();
     138            1 :     }
     139            1 : }
     140              : 
     141            1 : export class EmbeddedHyperDatabaseChannel implements HyperDatabaseChannel {
     142            1 :     constructor(
     143            1 :         private readonly channel: EmbeddedDatabaseChannel,
     144            1 :     ) {}
     145              : 
     146            1 :     async executeQuery(
     147            1 :         params: pb.salesforce_hyperdb_grpc_v1.pb.QueryParam,
     148            1 :         abort?: AbortSignal,
     149            1 :     ): Promise<QueryExecutionResponseStream> {
     150            1 :         return await this.channel.executeQuery(params.query, abort);
     151            1 :     }
     152              : 
     153            0 :     async close(): Promise<void> {
     154            0 :         await this.channel.close();
     155            0 :     }
     156            1 : }
        

Generated by: LCOV version 2.0-1