LCOV - code coverage report
Current view: top level - dashql-app/src/query - query_execution_state.ts Coverage Total Hit
Test: DashQL 915828b Lines: 100.0 % 426 426
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as arrow from 'apache-arrow';
       2              : import * as connection from '@ankoh/dashql-jsonschema/connection.js';
       3              : import * as buf from '@bufbuild/protobuf';
       4              : 
       5              : import { AsyncConsumer } from '../utils/async_consumer.js';
       6              : import { removePrimitiveFromArray } from '../utils/array.js';
       7              : import { LoggableException } from '../platform/logger/logger.js';
       8              : import { VariantKind } from '../utils/variant.js';
       9              : import type { EngineProgressPayload, QueryProfileProgress } from './query_progress_model.js';
      10              : 
      11            1 : export enum QueryExecutionStatus {
      12            1 :     REQUESTED = 0,
      13            1 :     PREPARING = 1,
      14            1 :     SENDING = 2,
      15            1 :     QUEUED = 3,
      16            1 :     RUNNING = 4,
      17            1 :     RECEIVED_FIRST_BATCH = 5,
      18            1 :     RECEIVED_ALL_BATCHES = 6,
      19            1 :     PROCESSING_RESULTS = 7,
      20            1 :     PROCESSED_RESULTS = 8,
      21            1 :     SUCCEEDED = 9,
      22            1 :     FAILED = 10,
      23            1 :     CANCELLED = 11,
      24            1 : }
      25              : 
      26            1 : export function queryIsDone(status: QueryExecutionStatus) {
      27            1 :     switch (status) {
      28            1 :         case QueryExecutionStatus.SUCCEEDED:
      29            1 :         case QueryExecutionStatus.FAILED:
      30            1 :         case QueryExecutionStatus.CANCELLED:
      31            1 :             return true;
      32            1 :         default:
      33            1 :             return false;
      34            1 :     }
      35            1 : }
      36              : 
      37              : export interface QueryExecutionMetrics {
      38              :     /// The total data bytes
      39              :     totalDataBytesReceived: number;
      40              :     /// The total batches received
      41              :     totalBatchesReceived: number;
      42              :     /// The total batches received
      43              :     totalRowsReceived: number;
      44              : 
      45              :     /// The query requests that started
      46              :     totalQueryRequestsStarted: number;
      47              :     /// The query requests that finished
      48              :     totalQueryRequestsSucceeded: number;
      49              :     /// The query requests that failed
      50              :     totalQueryRequestsFailed: number
      51              :     /// The total request duration of all requests that failed or succeeded
      52              :     totalQueryRequestDurationMs: number;
      53              : 
      54              :     /// The duration until the first batch
      55              :     durationUntilFirstBatchMs: number | null;
      56              : }
      57              : 
      58              : export interface QueryExecutionProgress {
      59              :     /// Is queued?
      60              :     isQueued: boolean | null;
      61              :     /// The progress in percent, as reported by the server
      62              :     progressPercent: number | null;
      63              :     /// The number of result rows, as reported by the server
      64              :     resultRows: number | null;
      65              :     /// The number of rows the server processed
      66              :     rowsProcessed: number | null;
      67              :     /// The execution time on the server in milliseconds
      68              :     executionTimeMs: number | null;
      69              :     /// The query execution metrics
      70              :     metrics: QueryExecutionMetrics;
      71              :     /// The phase the engine reports, e.g. the Trino query state (QUEUED, PLANNING, RUNNING, FINISHING)
      72              :     phase?: string | null;
      73              :     /// The raw progress payload of the engine, the query executor feeds it into the progress model of the query
      74              :     engineProgress?: EngineProgressPayload | null;
      75              :     /// The structured progress (stages, pipelines, the plan), set by the query executor
      76              :     profileProgress?: QueryProfileProgress | null;
      77              : }
      78              : 
      79              : export interface QueryExecutionResponseStream {
      80              :     /// Ask the backend to stop the query, when the connector supports explicit cancellation.
      81              :     cancel?(): Promise<void>;
      82              :     /// Is the profile of the query shown? Connectors fetch the plan and its statistics only then (Trino).
      83              :     setProfileDetail?(wanted: boolean): void;
      84              :     /// Get the result metadata (after completion)
      85              :     getMetadata(): Map<string, string>;
      86              :     /// Get the stream metrics
      87              :     getMetrics(): QueryExecutionMetrics;
      88              :     /// Get the current query status
      89              :     getStatus(): QueryExecutionStatus;
      90              :     /// Await the schema message
      91              :     getSchema(): Promise<arrow.Schema | null>;
      92              :     /// Await the next record batch
      93              :     produce(batches: AsyncConsumer<QueryExecutionResponseStream, arrow.RecordBatch>, progress: AsyncConsumer<QueryExecutionResponseStream, QueryExecutionProgress>, abort?: AbortSignal): Promise<void>;
      94              : }
      95              : 
      96              : export interface QueryMetrics {
      97              :     /// The time at which we requested the query (if any)
      98              :     queryRequestedAt: Date | null;
      99              :     /// The time at which we prepared the query (if any)
     100              :     queryPreparingStartedAt: Date | null;
     101              :     /// The time at which the query was sent (if any)
     102              :     querySendingStartedAt: Date | null;
     103              :     /// The time at which the query was queued (if any)
     104              :     queryQueuedStartedAt: Date | null;
     105              :     /// The time at which we the query started running (if any)
     106              :     queryRunningStartedAt: Date | null;
     107              :     /// Received the frist batch at (if any)
     108              :     receivedFirstBatchAt: Date | null;
     109              :     /// Received all batches at (if any)
     110              :     receivedLastBatchAt: Date | null;
     111              :     /// Received all batches at (if any)
     112              :     receivedAllBatchesAt: Date | null;
     113              :     /// The time at which we started processed the results
     114              :     processingResultsStartedAt: Date | null;
     115              :     /// The time at which we processed the results
     116              :     processedResultsAt: Date | null;
     117              :     /// The time at which the query execution finished (if any)
     118              :     querySucceededAt: Date | null;
     119              :     /// The time at which the query execution failed (if any)
     120              :     queryFailedAt: Date | null;
     121              :     /// The time at which the query execution was cancelled (if any)
     122              :     queryCancelledAt: Date | null;
     123              :     /// The time at which the query execution was last updated
     124              :     lastUpdatedAt: Date | null;
     125              :     /// The number of query_status updates received
     126              :     progressUpdatesReceived: number;
     127              :     /// The total query duration
     128              :     queryDurationMs: number | null;
     129              :     /// The text length in characters
     130              :     textLength: number | null;
     131              :     /// The stream metrics
     132              :     streamMetrics: QueryExecutionMetrics;
     133              : }
     134              : 
     135              : /// A query hint
     136            1 : export enum QueryType {
     137            1 :     USER_PROVIDED,
     138            1 :     CATALOG_QUERY_INFORMATION_SCHEMA,
     139            1 :     CATALOG_QUERY_PG_ATTRIBUTE,
     140            1 :     CATALOG_QUERY_PG_PROC,
     141            1 :     CATALOG_QUERY_HYPER_CLOUD,
     142            1 :     HEALTH_CHECK,
     143            1 :     INTERNAL_SQLFRAME,
     144            1 : }
     145              : 
     146              : export interface QueryMetadata {
     147              :     /// The query type
     148              :     queryType: QueryType;
     149              :     /// The title of the query (if any)
     150              :     title: string | null;
     151              :     /// The description
     152              :     description: string | null;
     153              :     /// The issuer
     154              :     issuer: string | null;
     155              :     /// Authored by the user or the app?
     156              :     userProvided: boolean;
     157              :     /// The session that issued the query, if any.
     158              :     /// Several sessions may share a connection, the session scopes the query result cache and the
     159              :     /// cleanup of the session's queries.
     160              :     sessionId?: string;
     161              :     /// The owner of the result, e.g. a script of a session. A succeeded query releases the result table of the
     162              :     /// earlier queries of its owner, the owner shows only its latest result.
     163              :     resultOwner?: string;
     164              :     /// Where a statement of a plan of session scripts comes from
     165              :     planned?: PlannedStatement;
     166              : }
     167              : 
     168              : /// A statement of a plan of session scripts (docs/design/planner_dependencies.md)
     169              : export interface PlannedStatement {
     170              :     /// A run of the whole session or of a script
     171              :     run: 'session' | 'script';
     172              :     /// The script that a run of a script runs
     173              :     target: string | null;
     174              :     /// The node of the plan
     175              :     node: number;
     176              :     /// The script of the statement
     177              :     script: string;
     178              :     /// The statement within the statements of the script in the plan, from 1
     179              :     statement: number;
     180              :     statements: number;
     181              :     /// The type of the statement, e.g. SELECT
     182              :     statementType: string;
     183              :     /// Is the statement the result of its script?
     184              :     result: boolean;
     185              :     /// Is the statement the cached result of its script? The executor reads the file instead of running it.
     186              :     cached: boolean;
     187              :     /// The scripts whose queries the statement inlines as common table expressions
     188              :     inlined: string[];
     189              :     /// The materialized results of the scripts of other connections that the statement scans, cached if the host
     190              :     /// provides the file of an earlier run
     191              :     scans: Array<{ script: string; path: string; cached: boolean }>;
     192              :     /// The file that the result of the statement is written to, for statements of other connections
     193              :     materializes: string | null;
     194              :     /// The nodes the statement runs after and why
     195              :     after: Array<{ node: number; reason: string }>;
     196              : }
     197              : 
     198              : export interface QueryExecutionState {
     199              :     /// The query id
     200              :     queryId: number;
     201              :     /// The trace id for this query execution
     202              :     traceId: number;
     203              :     /// The query text
     204              :     queryText: string;
     205              :     /// The query metadata
     206              :     queryMetadata: QueryMetadata;
     207              :     /// The current status
     208              :     status: QueryExecutionStatus;
     209              :     /// The query metric counters
     210              :     metrics: QueryMetrics;
     211              :     /// The cancellation signal
     212              :     cancellation: AbortController;
     213              :     /// The response stream
     214              :     resultStream: QueryExecutionResponseStream | null;
     215              :     /// The loading error (if any)
     216              :     error: LoggableException | null;
     217              :     /// The latest update for the query execution
     218              :     latestProgressUpdate: QueryExecutionProgress | null;
     219              :     /// One-based active statement position for a core-steered script execution.
     220              :     statementIndex: number | null;
     221              :     /// Total statements in the active script execution.
     222              :     statementCount: number | null;
     223              :     /// Whether the latest script statement completed successfully.
     224              :     statementSucceeded: boolean;
     225              :     /// The number of record batches that are already buffered
     226              :     resultSchema: arrow.Schema | null;
     227              :     /// The number of record batches that are already buffered
     228              :     resultBatches: arrow.RecordBatch[];
     229              :     /// The result metadata
     230              :     resultMetadata: Map<string, string> | null;
     231              :     /// The result query_result iff the query succeeded
     232              :     resultTable: arrow.Table | null;
     233              :     /// The file-based query result cache key for this execution, or null when the query is not
     234              :     /// cacheable (or no key could be derived). Recorded so the UI can offer a "delete cached" action.
     235              :     cacheKey: string | null;
     236              :     /// Whether this result was served from the query result cache instead of the backend.
     237              :     servedFromCache: boolean;
     238              :     /// Whether the cached `.arrow` entry for this query has been deleted (via the UI). Used to
     239              :     /// disable the "delete cached" button once there is nothing left to delete.
     240              :     cacheDeleted: boolean;
     241              :     /// When the cache entry backing this result was written (`.arrow` file mtime), or null when the
     242              :     /// result did not come from the cache. Lets the UI show how old a cached result is. Note that a
     243              :     /// cache hit does not re-touch the file, so this is the write time, not a last-access time.
     244              :     cachedAt: Date | null;
     245              : }
     246              : 
     247              : export interface QueryExecutionTracker {
     248              :     dispatch(action: QueryExecutionAction): void;
     249              : }
     250              : 
     251            1 : export const EXECUTE_QUERY = Symbol('EXECUTE_QUERY');
     252            1 : export const QUERY_PREPARING = Symbol('QUERY_PREPARING');
     253            1 : export const QUERY_SENDING = Symbol('QUERY_SENDING');
     254            1 : export const QUERY_RUNNING = Symbol('QUERY_RUNNING');
     255            1 : export const QUERY_PROGRESS_UPDATED = Symbol('QUERY_PROGRESS_UPDATED');
     256            1 : export const QUERY_STATEMENT_STARTED = Symbol('QUERY_STATEMENT_STARTED');
     257            1 : export const QUERY_STATEMENT_SUCCEEDED = Symbol('QUERY_STATEMENT_SUCCEEDED');
     258            1 : export const QUERY_RECEIVED_BATCH = Symbol('QUERY_RECEIVED_BATCH');
     259            1 : export const QUERY_RECEIVED_ALL_BATCHES = Symbol('QUERY_RECEIVED_ALL_BATCHES');
     260            1 : export const QUERY_PROCESSING_RESULTS = Symbol('QUERY_PROCESSING_RESULTS');
     261            1 : export const QUERY_PROCESSED_RESULTS = Symbol('QUERY_PROCESSED_RESULTS');
     262            1 : export const QUERY_SUCCEEDED = Symbol('QUERY_SUCCEEDED');
     263            1 : export const QUERY_FAILED = Symbol('QUERY_FAILED');
     264            1 : export const QUERY_CANCELLED = Symbol('QUERY_CANCELLED');
     265            1 : export const QUERY_CACHE_RECORDED = Symbol('QUERY_CACHE_RECORDED');
     266            1 : export const QUERY_CACHE_DELETED = Symbol('QUERY_CACHE_DELETED');
     267              : 
     268              : export type QueryExecutionAction =
     269              :     | VariantKind<typeof EXECUTE_QUERY, [number, QueryExecutionState]>
     270              :     | VariantKind<typeof QUERY_PREPARING, [number]>
     271              :     | VariantKind<typeof QUERY_SENDING, [number]>
     272              :     | VariantKind<typeof QUERY_RUNNING, [number, QueryExecutionResponseStream | null]>
     273              :     | VariantKind<typeof QUERY_PROGRESS_UPDATED, [number, QueryExecutionProgress]>
     274              :     | VariantKind<typeof QUERY_STATEMENT_STARTED, [number, number, number]>
     275              :     | VariantKind<typeof QUERY_STATEMENT_SUCCEEDED, [number, number, number]>
     276              :     | VariantKind<typeof QUERY_RECEIVED_BATCH, [number, arrow.RecordBatch, QueryExecutionMetrics]>
     277              :     | VariantKind<typeof QUERY_RECEIVED_ALL_BATCHES, [number, arrow.Table, Map<string, string>, QueryExecutionMetrics]>
     278              :     | VariantKind<typeof QUERY_PROCESSING_RESULTS, [number]>
     279              :     | VariantKind<typeof QUERY_PROCESSED_RESULTS, [number]>
     280              :     | VariantKind<typeof QUERY_SUCCEEDED, [number]>
     281              :     | VariantKind<typeof QUERY_FAILED, [number, LoggableException, QueryExecutionMetrics | null]>
     282              :     | VariantKind<typeof QUERY_CANCELLED, [number, LoggableException, QueryExecutionMetrics | null]>
     283              :     | VariantKind<typeof QUERY_CACHE_RECORDED, [number, string, boolean, number | null]>
     284              :     | VariantKind<typeof QUERY_CACHE_DELETED, [number]>;
     285              : 
     286              : export interface QueryExecutionHistoryState {
     287              :     queriesActive: Map<number, QueryExecutionState>;
     288              :     queriesActiveOrdered: number[];
     289              :     queriesFinished: Map<number, QueryExecutionState>;
     290              :     queriesFinishedOrdered: number[];
     291              :     snapshotQueriesActiveFinished: number;
     292              :     metrics: connection.ConnectionMetrics;
     293              : }
     294              : 
     295            1 : export function createQueryExecutionState(
     296            1 :     queryId: number,
     297            1 :     traceId: number,
     298            1 :     queryText: string,
     299            1 :     queryMetadata: QueryMetadata,
     300            1 :     cancellation: AbortController,
     301            1 : ): QueryExecutionState {
     302            1 :     const requestedAt = new Date();
     303            1 :     return {
     304            1 :         queryId,
     305            1 :         traceId,
     306            1 :         queryText,
     307            1 :         queryMetadata,
     308            1 :         status: QueryExecutionStatus.REQUESTED,
     309            1 :         cancellation,
     310            1 :         resultStream: null,
     311            1 :         error: null,
     312            1 :         metrics: {
     313            1 :             textLength: queryText.length,
     314            1 :             queryRequestedAt: requestedAt,
     315            1 :             queryPreparingStartedAt: null,
     316            1 :             querySendingStartedAt: null,
     317            1 :             queryQueuedStartedAt: null,
     318            1 :             queryRunningStartedAt: null,
     319            1 :             receivedFirstBatchAt: null,
     320            1 :             receivedLastBatchAt: null,
     321            1 :             receivedAllBatchesAt: null,
     322            1 :             processingResultsStartedAt: null,
     323            1 :             processedResultsAt: null,
     324            1 :             querySucceededAt: null,
     325            1 :             queryFailedAt: null,
     326            1 :             queryCancelledAt: null,
     327            1 :             lastUpdatedAt: requestedAt,
     328            1 :             progressUpdatesReceived: 0,
     329            1 :             queryDurationMs: null,
     330            1 :             streamMetrics: createQueryResponseStreamMetrics(),
     331            1 :         },
     332            1 :         latestProgressUpdate: null,
     333            1 :         statementIndex: null,
     334            1 :         statementCount: null,
     335            1 :         statementSucceeded: false,
     336            1 :         resultMetadata: null,
     337            1 :         resultSchema: null,
     338            1 :         resultBatches: [],
     339            1 :         resultTable: null,
     340            1 :         cacheKey: null,
     341            1 :         servedFromCache: false,
     342            1 :         cacheDeleted: false,
     343            1 :         cachedAt: null,
     344            1 :     };
     345            1 : }
     346              : 
     347            1 : export function reduceQueryAction<T extends QueryExecutionHistoryState>(state: T, action: QueryExecutionAction): T {
     348            1 :     const now = new Date();
     349            1 :     const queryId = action.value[0];
     350              : 
     351              :     // Initial setup?
     352            1 :     if (action.type == EXECUTE_QUERY) {
     353            1 :         state.queriesActive.set(queryId, action.value[1]);
     354            1 :         state.queriesActiveOrdered.push(queryId);
     355            1 :         state.snapshotQueriesActiveFinished += 1;
     356            1 :         return { ...state };
     357            1 :     }
     358              : 
     359              :     // Cache bookkeeping actions can arrive after the query has already moved to `queriesFinished`
     360              :     // (the write path fires-and-forgets, and the UI delete happens on a finished result), so resolve
     361              :     // the query from whichever map holds it and write the update back to the same map.
     362            1 :     if (action.type == QUERY_CACHE_RECORDED || action.type == QUERY_CACHE_DELETED) {
     363            1 :         const isActive = state.queriesActive.has(queryId);
     364            1 :         const target = isActive ? state.queriesActive : state.queriesFinished;
     365            1 :         const q = target.get(queryId);
     366            1 :         if (!q) {
     367            1 :             return state;
     368              :         }
     369            1 :         let next: QueryExecutionState;
     370            1 :         if (action.type == QUERY_CACHE_RECORDED) {
     371            1 :             const [, cacheKey, servedFromCache, cachedAtMs] = action.value;
     372            1 :             next = {
     373            1 :                 ...q,
     374            1 :                 cacheKey,
     375            1 :                 servedFromCache,
     376            1 :                 cachedAt: cachedAtMs != null ? new Date(cachedAtMs) : q.cachedAt,
     377            1 :             };
     378            1 :         } else {
     379            1 :             next = { ...q, cacheDeleted: true };
     380              :         }
     381            1 :         target.set(queryId, next);
     382            1 :         return { ...state };
     383            1 :     }
     384              : 
     385            1 :     let query = state.queriesActive.get(queryId);
     386            1 :     if (!query) {
     387            1 :         return state;
     388              :     }
     389            1 :     switch (action.type) {
     390            1 :         case QUERY_PREPARING: {
     391            1 :             query = {
     392            1 :                 ...query,
     393            1 :                 status: QueryExecutionStatus.PREPARING,
     394            1 :                 metrics: {
     395            1 :                     ...query.metrics,
     396            1 :                     lastUpdatedAt: now,
     397            1 :                     queryPreparingStartedAt: now,
     398            1 :                 },
     399            1 :             };
     400            1 :             state.queriesActive.set(query.queryId, query);
     401            1 :             return { ...state };
     402              :         }
     403            1 :         case QUERY_SENDING: {
     404            1 :             query = {
     405            1 :                 ...query,
     406            1 :                 status: QueryExecutionStatus.SENDING,
     407            1 :                 metrics: {
     408            1 :                     ...query.metrics,
     409            1 :                     lastUpdatedAt: now,
     410            1 :                     querySendingStartedAt: now,
     411            1 :                 },
     412            1 :             };
     413            1 :             state.queriesActive.set(query.queryId, query);
     414            1 :             return { ...state };
     415              :         }
     416            1 :         case QUERY_RUNNING: {
     417            1 :             query = {
     418            1 :                 ...query,
     419            1 :                 status: QueryExecutionStatus.RUNNING,
     420            1 :                 resultStream: action.value[1],
     421            1 :                 metrics: {
     422            1 :                     ...query.metrics,
     423            1 :                     lastUpdatedAt: now,
     424            1 :                     queryRunningStartedAt: now,
     425            1 :                 },
     426            1 :             };
     427            1 :             state.queriesActive.set(query.queryId, query);
     428            1 :             return { ...state };
     429              :         }
     430            1 :         case QUERY_PROGRESS_UPDATED: {
     431              :             // An update without structured progress keeps the last one, e.g. while the core loads
     432            1 :             const progress = action.value[1];
     433            1 :             const profileProgress = progress.profileProgress ?? query.latestProgressUpdate?.profileProgress ?? null;
     434            1 :             const next = {
     435            1 :                 ...query,
     436            1 :                 latestProgressUpdate: { ...progress, profileProgress },
     437            1 :                 metrics: {
     438            1 :                     ...query.metrics,
     439            1 :                     lastUpdatedAt: now,
     440            1 :                     progressUpdatesReceived: ++query.metrics.progressUpdatesReceived
     441            1 :                 },
     442            1 :             };
     443              :             // Is the query queued for the first time?
     444            1 :             if (action.value[1].isQueued && next.status == QueryExecutionStatus.SENDING) {
     445            1 :                 next.status = QueryExecutionStatus.QUEUED;
     446            1 :                 next.metrics.queryQueuedStartedAt = now;
     447            1 :             }
     448            1 :             state.queriesActive.set(query.queryId, next);
     449            1 :             return { ...state };
     450            1 :         }
     451            1 :         case QUERY_STATEMENT_STARTED: {
     452            1 :             query = {
     453            1 :                 ...query,
     454            1 :                 statementIndex: action.value[1],
     455            1 :                 statementCount: action.value[2],
     456            1 :                 statementSucceeded: false,
     457            1 :                 status: QueryExecutionStatus.RUNNING,
     458            1 :                 metrics: {
     459            1 :                     ...query.metrics,
     460            1 :                     lastUpdatedAt: now,
     461            1 :                 },
     462            1 :             };
     463            1 :             state.queriesActive.set(query.queryId, query);
     464            1 :             return { ...state };
     465              :         }
     466            1 :         case QUERY_STATEMENT_SUCCEEDED: {
     467            1 :             query = {
     468            1 :                 ...query,
     469            1 :                 statementIndex: action.value[1],
     470            1 :                 statementCount: action.value[2],
     471            1 :                 statementSucceeded: true,
     472            1 :                 status: QueryExecutionStatus.RUNNING,
     473            1 :                 metrics: {
     474            1 :                     ...query.metrics,
     475            1 :                     lastUpdatedAt: now,
     476            1 :                 },
     477            1 :             };
     478            1 :             state.queriesActive.set(query.queryId, query);
     479            1 :             return { ...state };
     480              :         }
     481            1 :         case QUERY_RECEIVED_BATCH: {
     482            1 :             const [_queryId, batch, streamMetrics] = action.value;
     483            1 :             const metrics = { ...query.metrics };
     484            1 :             if (metrics.receivedFirstBatchAt == null) {
     485            1 :                 metrics.receivedFirstBatchAt = now;
     486              :             }
     487            1 :             metrics.receivedLastBatchAt = now;
     488            1 :             metrics.lastUpdatedAt = now;
     489            1 :             metrics.streamMetrics = streamMetrics;
     490            1 :             query.resultBatches.push(batch);
     491            1 :             query = {
     492            1 :                 ...query,
     493            1 :                 status: QueryExecutionStatus.RECEIVED_FIRST_BATCH,
     494            1 :                 metrics: metrics,
     495            1 :             };
     496            1 :             if (query.resultSchema == null) {
     497            1 :                 query.resultSchema = batch.schema;
     498              :             }
     499            1 :             state.queriesActive.set(query.queryId, query);
     500            1 :             return { ...state };
     501            1 :         }
     502            1 :         case QUERY_RECEIVED_ALL_BATCHES: {
     503            1 :             const metrics = { ...query.metrics };
     504            1 :             metrics.lastUpdatedAt = now;
     505            1 :             metrics.receivedAllBatchesAt = now;
     506            1 :             metrics.streamMetrics = action.value[3];
     507            1 :             query = {
     508            1 :                 ...query,
     509            1 :                 resultMetadata: action.value[2],
     510            1 :                 resultTable: action.value[1],
     511            1 :                 status: QueryExecutionStatus.RECEIVED_ALL_BATCHES,
     512            1 :                 metrics: metrics,
     513            1 :             };
     514            1 :             state.queriesActive.set(query.queryId, query);
     515            1 :             return { ...state, };
     516            1 :         }
     517            1 :         case QUERY_PROCESSING_RESULTS: {
     518            1 :             const metrics = { ...query.metrics };
     519            1 :             metrics.lastUpdatedAt = now;
     520            1 :             metrics.processingResultsStartedAt = now;
     521            1 :             query = {
     522            1 :                 ...query,
     523            1 :                 status: QueryExecutionStatus.PROCESSING_RESULTS,
     524            1 :                 metrics: metrics,
     525            1 :             };
     526            1 :             state.queriesActive.set(query.queryId, query);
     527            1 :             return { ...state, };
     528            1 :         }
     529            1 :         case QUERY_PROCESSED_RESULTS: {
     530            1 :             const metrics = { ...query.metrics };
     531            1 :             metrics.lastUpdatedAt = now;
     532            1 :             metrics.processedResultsAt = now;
     533            1 :             query = {
     534            1 :                 ...query,
     535            1 :                 status: QueryExecutionStatus.PROCESSED_RESULTS,
     536            1 :                 metrics: metrics,
     537            1 :             };
     538            1 :             state.queriesActive.set(query.queryId, query);
     539            1 :             return { ...state, };
     540            1 :         }
     541            1 :         case QUERY_SUCCEEDED: {
     542            1 :             const metrics = { ...query.metrics };
     543            1 :             const untilNow = now.getTime() - (query.metrics.queryRunningStartedAt ?? query.metrics.queryRequestedAt ?? now).getTime();
     544            1 :             metrics.lastUpdatedAt = now;
     545            1 :             metrics.querySucceededAt = now;
     546            1 :             metrics.queryDurationMs = untilNow;
     547            1 :             query = {
     548            1 :                 ...query,
     549            1 :                 status: QueryExecutionStatus.SUCCEEDED,
     550            1 :                 metrics: metrics,
     551            1 :             };
     552            1 :             finishQuery(state, query);
     553            1 :             return query.queryMetadata.queryType == QueryType.INTERNAL_SQLFRAME
     554            1 :                 ? { ...state }
     555            1 :                 : {
     556            1 :                     ...state,
     557            1 :                     metrics: {
     558            1 :                         ...state.metrics,
     559            1 :                         successfulQueries: mergeQueryMetrics(state.metrics.successfulQueries!, metrics)
     560            1 :                     },
     561            1 :                 };
     562            1 :         }
     563            1 :         case QUERY_CANCELLED: {
     564            1 :             const untilNow = now.getTime() - (query.metrics.queryRunningStartedAt ?? query.metrics.queryRequestedAt ?? now).getTime();
     565            1 :             const metrics = { ...query.metrics };
     566            1 :             metrics.lastUpdatedAt = now;
     567            1 :             metrics.queryCancelledAt = now;
     568            1 :             metrics.queryDurationMs = untilNow;
     569            1 :             metrics.streamMetrics = action.value[2] ?? metrics.streamMetrics;
     570            1 :             query = {
     571            1 :                 ...query,
     572            1 :                 status: QueryExecutionStatus.CANCELLED,
     573            1 :                 error: action.value[1],
     574            1 :                 metrics
     575            1 :             };
     576            1 :             finishQuery(state, query);
     577            1 :             return query.queryMetadata.queryType == QueryType.INTERNAL_SQLFRAME
     578            1 :                 ? { ...state }
     579            1 :                 : {
     580            1 :                     ...state,
     581            1 :                     metrics: {
     582            1 :                         ...state.metrics,
     583            1 :                         canceledQueries: mergeQueryMetrics(state.metrics.canceledQueries!, metrics)
     584            1 :                     },
     585            1 :                 };
     586            1 :         }
     587            1 :         case QUERY_FAILED: {
     588            1 :             const untilNow = now.getTime() - (query.metrics.queryRunningStartedAt ?? query.metrics.queryRequestedAt ?? now).getTime();
     589            1 :             const metrics = { ...query.metrics };
     590            1 :             metrics.lastUpdatedAt = now;
     591            1 :             metrics.queryFailedAt = now;
     592            1 :             metrics.queryDurationMs = untilNow;
     593            1 :             metrics.streamMetrics = action.value[2] ?? metrics.streamMetrics;
     594            1 :             query = {
     595            1 :                 ...query,
     596            1 :                 status: QueryExecutionStatus.FAILED,
     597            1 :                 error: action.value[1],
     598            1 :                 metrics
     599            1 :             };
     600            1 :             finishQuery(state, query);
     601            1 :             return query.queryMetadata.queryType == QueryType.INTERNAL_SQLFRAME
     602            1 :                 ? { ...state }
     603            1 :                 : {
     604            1 :                     ...state,
     605            1 :                     metrics: {
     606            1 :                         ...state.metrics,
     607            1 :                         failedQueries: mergeQueryMetrics(state.metrics.failedQueries!, metrics)
     608            1 :                     },
     609            1 :                 };
     610            1 :         }
     611            1 :     }
     612            1 : }
     613              : 
     614              : /// The finished internal queries that the history keeps, internal queries keep no result
     615              : export const INTERNAL_QUERY_HISTORY_LIMIT = 100;
     616              : 
     617              : /// Move a terminal query to the finished queries.
     618              : ///
     619              : /// The finished queries keep their result tables for the views of their results, but not their batches, the
     620              : /// table holds them. Internal queries return their results to their callers and keep only their metrics. A
     621              : /// succeeded query with a result owner releases the result tables of the other queries of its owner, except for
     622              : /// a later one, like the latest result of a script.
     623            1 : function finishQuery(state: QueryExecutionHistoryState, query: QueryExecutionState) {
     624            1 :     const userProvided = query.queryMetadata.queryType == QueryType.USER_PROVIDED;
     625            1 :     query = {
     626            1 :         ...query,
     627            1 :         resultBatches: [],
     628            1 :         resultTable: userProvided ? query.resultTable : null,
     629            1 :     };
     630            1 :     state.queriesActive.delete(query.queryId);
     631            1 :     removePrimitiveFromArray(state.queriesActiveOrdered, query.queryId);
     632            1 :     state.queriesFinished.set(query.queryId, query);
     633            1 :     state.queriesFinishedOrdered.push(query.queryId);
     634            1 :     state.snapshotQueriesActiveFinished += 1;
     635              : 
     636            1 :     const owner = query.queryMetadata.resultOwner;
     637            1 :     if (userProvided && owner != null && query.status == QueryExecutionStatus.SUCCEEDED && query.resultTable != null) {
     638            1 :         for (const [otherId, other] of state.queriesFinished) {
     639            1 :             if (otherId === query.queryId || other.resultTable == null || other.queryMetadata.resultOwner !== owner) continue;
     640              :             // A later result stays, the earlier query finished after it
     641            1 :             if (otherId > query.queryId) {
     642            1 :                 state.queriesFinished.set(query.queryId, { ...query, resultTable: null });
     643              :             } else {
     644            1 :                 state.queriesFinished.set(otherId, { ...other, resultTable: null });
     645              :             }
     646            1 :         }
     647              :     }
     648              : 
     649              :     // Internal queries leave only their metrics
     650            1 :     let internal = 0;
     651            1 :     for (const queryId of state.queriesFinishedOrdered) {
     652            1 :         if (state.queriesFinished.get(queryId)?.queryMetadata.queryType != QueryType.USER_PROVIDED) ++internal;
     653              :     }
     654            1 :     for (let i = 0; internal > INTERNAL_QUERY_HISTORY_LIMIT && i < state.queriesFinishedOrdered.length;) {
     655            1 :         const queryId = state.queriesFinishedOrdered[i];
     656            1 :         if (state.queriesFinished.get(queryId)?.queryMetadata.queryType != QueryType.USER_PROVIDED) {
     657            1 :             state.queriesFinished.delete(queryId);
     658            1 :             state.queriesFinishedOrdered.splice(i, 1);
     659            1 :             --internal;
     660            1 :         } else {
     661            1 :             ++i;
     662              :         }
     663            1 :     }
     664            1 : }
     665              : 
     666            1 : function mergeQueryMetrics(metrics: connection.ConnectionQueryMetrics, query: QueryMetrics): connection.ConnectionQueryMetrics {
     667            1 :     return {
     668            1 :         totalQueries: metrics.totalQueries + 1,
     669            1 :         totalBatchesReceived: metrics.totalBatchesReceived + query.streamMetrics.totalBatchesReceived,
     670            1 :         totalRowsReceived: metrics.totalRowsReceived + query.streamMetrics.totalRowsReceived,
     671            1 :         accumulatedTimeUntilFirstBatch: metrics.accumulatedTimeUntilFirstBatch + (query.streamMetrics.durationUntilFirstBatchMs ?? 0),
     672            1 :         accumulatedQueryDuration: metrics.accumulatedQueryDuration + (query.queryDurationMs ?? 0)
     673            1 :     };
     674            1 : }
     675              : 
     676            1 : export function createQueryResponseStreamMetrics(): QueryExecutionMetrics {
     677            1 :     return {
     678            1 :         totalDataBytesReceived: 0,
     679            1 :         totalBatchesReceived: 0,
     680            1 :         totalRowsReceived: 0,
     681              : 
     682            1 :         totalQueryRequestsStarted: 0,
     683            1 :         totalQueryRequestsSucceeded: 0,
     684            1 :         totalQueryRequestsFailed: 0,
     685            1 :         totalQueryRequestDurationMs: 0,
     686              : 
     687            1 :         durationUntilFirstBatchMs: null,
     688            1 :     };
     689            1 : }
     690              : 
     691            1 : export function createQueryExecutionMetrics(): connection.ConnectionMetrics {
     692            1 :     return {
     693            1 :         successfulQueries: {
     694            1 :             totalQueries: 0,
     695            1 :             totalBatchesReceived: 0,
     696            1 :             totalRowsReceived: 0,
     697            1 :             accumulatedTimeUntilFirstBatch: 0,
     698            1 :             accumulatedQueryDuration: 0,
     699            1 :         },
     700            1 :         canceledQueries: {
     701            1 :             totalQueries: 0,
     702            1 :             totalBatchesReceived: 0,
     703            1 :             totalRowsReceived: 0,
     704            1 :             accumulatedTimeUntilFirstBatch: 0,
     705            1 :             accumulatedQueryDuration: 0,
     706            1 :         },
     707            1 :         failedQueries: {
     708            1 :             totalQueries: 0,
     709            1 :             totalBatchesReceived: 0,
     710            1 :             totalRowsReceived: 0,
     711            1 :             accumulatedTimeUntilFirstBatch: 0,
     712            1 :             accumulatedQueryDuration: 0,
     713            1 :         },
     714            1 :     };
     715            1 : }
        

Generated by: LCOV version 2.0-1