LCOV - code coverage report
Current view: top level - dashql-app/src/connections/trino - trino_channel.ts Coverage Total Hit
Test: DashQL 915828b Lines: 96.1 % 363 349
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as arrow from 'apache-arrow';
       2              : import * as proto from "../../proto.js";
       3              : 
       4              : import { LoggableException, Logger } from '../../platform/logger/logger.js';
       5              : import { createQueryResponseStreamMetrics, QueryExecutionProgress, QueryExecutionResponseStream, QueryExecutionMetrics, QueryExecutionStatus } from "../../query/query_execution_state.js";
       6              : import { yieldToBrowser } from '../../utils/yield_to_browser.js';
       7              : import { TrinoApiClientInterface, TrinoApiEndpoint, TrinoQueryData, TrinoQueryInfo, TrinoQueryResult, TrinoQueryStage, TrinoQueryStatistics } from "./trino_api_client.js";
       8              : import { ChannelError, RawProxyError } from '../../platform/channel_common.js';
       9              : import { AsyncValue } from '../../utils/async_value.js';
      10              : import { AsyncConsumer } from '../../utils/async_consumer.js';
      11              : import { translateAnyRowsToArrowBatch } from './arrow_conversion.js';
      12              : import type { TrinoSessionDefaults } from './trino_catalogs.js';
      13              : 
      14            1 : const LOG_CTX = 'trino_channel';
      15              : /// Fetch QueryInfo again after this long while a stage whose tasks are done has no operator summaries yet
      16            1 : const QUERY_INFO_RETRY_MS = 1000;
      17              : /// Wait this long for the final QueryInfo of a finished query
      18            1 : const QUERY_INFO_FINAL_TIMEOUT_MS = 3000;
      19              : /// Fetch the QueryInfo of a finished query this often until it is final
      20            1 : const QUERY_INFO_FINAL_RETRY_MS = 200;
      21              : 
      22              : /// Are the tasks of a stage done? A stage waits for the query in PENDING and finishes with it.
      23            1 : function isStageDone(state: string | undefined): boolean {
      24            1 :     return state === 'PENDING' || state === 'FINISHED';
      25            1 : }
      26              : 
      27              : /// The stages of the statement stats whose tasks are done
      28            1 : function countDoneStages(stage: TrinoQueryStage | null | undefined): number {
      29            1 :     if (stage == null) return 0;
      30            1 :     let count = isStageDone(stage.state) ? 1 : 0;
      31            1 :     for (const child of stage.subStages ?? []) count += countDoneStages(child);
      32            1 :     return count;
      33            1 : }
      34              : 
      35              : /// Does a QueryInfo have a stage whose tasks are done but that reports no operator summaries yet? Right after a
      36              : /// stage turned PENDING, Trino may still wait for the final infos of its tasks.
      37            1 : function lacksOperatorSummaries(info: TrinoQueryInfo): boolean {
      38            1 :     return (info.stages?.stages ?? []).some(stage =>
      39            1 :         isStageDone(stage.state) && (stage.stageStats?.operatorSummaries?.length ?? 0) === 0);
      40            1 : }
      41              : 
      42              : /// Is a QueryInfo final? A finished query is once Trino collected the final infos of its tasks, a failed one may
      43              : /// never be.
      44            1 : function isFinalQueryInfo(info: TrinoQueryInfo): boolean {
      45            1 :     return info.state === 'FAILED' || (info.state === 'FINISHED' && info.finalQueryInfo === true);
      46            1 : }
      47              : 
      48              : export interface TrinoQueryExecutionProgress extends QueryExecutionProgress { }
      49              : 
      50            1 : export class TrinoQueryResultStream implements QueryExecutionResponseStream {
      51              :     /// The logger
      52            1 :     logger: Logger;
      53              :     /// The api client
      54            1 :     apiClient: TrinoApiClientInterface;
      55              :     /// The current execution status
      56            1 :     currentStatus: QueryExecutionStatus;
      57              :     /// The response metadata
      58            1 :     responseMetadata: Map<string, string>;
      59              :     /// The schema
      60            1 :     resultSchema: AsyncValue<arrow.Schema, Error>;
      61              :     /// The current result
      62            1 :     latestQueryResult: TrinoQueryResult | null;
      63              :     /// The latest statistics
      64            1 :     latestQueryStats: TrinoQueryStatistics | null;
      65              :     /// The latest query state
      66            1 :     latestQueryState: string | null;
      67              :     /// When the latest result arrived, in milliseconds since epoch
      68            1 :     latestQueryReceivedAt: number;
      69              :     /// Was the QueryInfo with the plans of the stages requested?
      70            1 :     queryInfoRequested: boolean = false;
      71              :     /// The request of the QueryInfo with the plans
      72            1 :     queryInfoPlan: Promise<void> | null = null;
      73              :     /// The pending request of a QueryInfo for the operator summaries
      74            1 :     queryInfoStats: Promise<TrinoQueryInfo | null> | null = null;
      75              :     /// Fetch QueryInfo again once the pending request is done?
      76            1 :     queryInfoStale: boolean = false;
      77              :     /// The stages with done tasks that the requested QueryInfos cover
      78            1 :     queryInfoDoneStages: number = 0;
      79              :     /// The timer of the next QueryInfo while a stage with done tasks has no operator summaries
      80            1 :     queryInfoRetry: ReturnType<typeof setTimeout> | null = null;
      81              :     /// Is the profile shown?
      82            1 :     profileDetail: boolean = false;
      83              :     /// The consumer of the progress while the stream produces
      84            1 :     progressConsumer: AsyncConsumer<QueryExecutionResponseStream, QueryExecutionProgress> | null = null;
      85              :     /// The abort signal of the stream while it produces
      86            1 :     produceAbort: AbortSignal | null = null;
      87              :     /// The progress updates
      88            1 :     latestQueryProgress: QueryExecutionProgress | null;
      89              :     /// The metrics
      90            1 :     queryMetrics: QueryExecutionMetrics;
      91            1 :     endpoint: TrinoApiEndpoint;
      92              : 
      93              :     /// The constructor
      94            1 :     constructor(logger: Logger, apiClient: TrinoApiClientInterface, endpoint: TrinoApiEndpoint, result: TrinoQueryResult, metrics: QueryExecutionMetrics) {
      95            1 :         this.logger = logger;
      96            1 :         this.apiClient = apiClient;
      97            1 :         this.currentStatus = QueryExecutionStatus.RUNNING;
      98            1 :         this.responseMetadata = new Map();
      99            1 :         this.resultSchema = new AsyncValue();
     100            1 :         this.latestQueryResult = result;
     101            1 :         this.latestQueryStats = result.stats ?? null;
     102            1 :         this.latestQueryState = result.stats?.state ?? null;
     103            1 :         this.latestQueryReceivedAt = Date.now();
     104            1 :         this.latestQueryProgress = null;
     105            1 :         this.queryMetrics = metrics;
     106            1 :         this.endpoint = endpoint;
     107            1 :         if (result.columns) {
     108            1 :             this.resultSchema.resolve(translateTrinoSchema(result, this.logger));
     109              :         }
     110            1 :     }
     111              : 
     112              :     /// Cancel the query, a query without a next URI is done already
     113            1 :     async cancel(): Promise<void> {
     114            1 :         const nextUri = this.latestQueryResult?.nextUri;
     115            1 :         if (nextUri != null) await this.apiClient.cancelQuery(this.endpoint, nextUri);
     116            1 :     }
     117              : 
     118              :     /// Is the profile of the query shown? The plan and the operator summaries are fetched only then.
     119            1 :     setProfileDetail(wanted: boolean): void {
     120            1 :         this.profileDetail = wanted;
     121            1 :         if (!wanted) this.clearQueryInfoRetry();
     122            1 :         this.requestProfile();
     123            1 :     }
     124              : 
     125              :     /// Request the QueryInfo with the plans of the stages once the stages exist and the profile is shown. Trino
     126              :     /// reports the operator summaries of a stage (the rows of the plan) once its tasks are done, so QueryInfo is
     127              :     /// fetched again whenever the statement stats show more such stages. The requests do not hold up the query,
     128              :     /// a failed request leaves the stages without plan or rows. Pruned QueryInfo is no use here: it keeps only the
     129              :     /// output stage.
     130            1 :     requestProfile(): void {
     131            1 :         const progress = this.progressConsumer;
     132            1 :         const abort = this.produceAbort ?? undefined;
     133            1 :         const queryId = this.latestQueryResult?.id;
     134            1 :         if (!this.profileDetail || progress == null || this.latestQueryStats?.rootStage == null || !queryId) return;
     135            1 :         const doneStages = countDoneStages(this.latestQueryStats.rootStage);
     136            1 :         if (!this.queryInfoRequested) {
     137            1 :             this.queryInfoRequested = true;
     138            1 :             this.queryInfoDoneStages = doneStages;
     139            1 :             this.queryInfoPlan = this.apiClient.getQueryInfo(this.endpoint, queryId, abort).then(
     140            1 :                 info => {
     141            1 :                     progress.resolve(this, {
     142            1 :                         ...deriveProgress(this.latestQueryStats, this.queryMetrics, this.latestQueryReceivedAt),
     143            1 :                         engineProgress: { kind: 'trino_query_info', info, receivedAt: Date.now() },
     144            1 :                     });
     145            1 :                     this.retryQueryInfoIfIncomplete(info);
     146            1 :                 },
     147            0 :                 (e: unknown) => this.logger.warn("Failed to fetch the query plan", { "error": String(e), ...(e instanceof LoggableException ? e.keyValues : {}) }, LOG_CTX),
     148            1 :             );
     149            1 :         } else if (doneStages > this.queryInfoDoneStages) {
     150            1 :             this.queryInfoDoneStages = doneStages;
     151            1 :             void this.fetchQueryInfoStats();
     152            1 :         }
     153            1 :     }
     154              : 
     155              :     /// Fetch QueryInfo for the operator summaries, after the plans. One request at a time, a request while one is
     156              :     /// pending fetches once more after it.
     157            1 :     fetchQueryInfoStats(): Promise<TrinoQueryInfo | null> {
     158            1 :         if (this.queryInfoStats != null) {
     159            1 :             this.queryInfoStale = true;
     160            1 :             return this.queryInfoStats;
     161            1 :         }
     162            1 :         const progress = this.progressConsumer;
     163            1 :         const queryId = this.latestQueryResult?.id;
     164            1 :         const abort = this.produceAbort ?? undefined;
     165            1 :         if (progress == null || !queryId) return Promise.resolve(null);
     166            1 :         this.clearQueryInfoRetry();
     167            1 :         const request = (this.queryInfoPlan ?? Promise.resolve())
     168            1 :             .then(() => this.apiClient.getQueryInfo(this.endpoint, queryId, abort))
     169            1 :             .then(
     170            1 :                 info => {
     171            1 :                     progress.resolve(this, {
     172            1 :                         ...deriveProgress(this.latestQueryStats, this.queryMetrics, this.latestQueryReceivedAt),
     173            1 :                         engineProgress: { kind: 'trino_query_info_stats', info, receivedAt: Date.now() },
     174            1 :                     });
     175            1 :                     return info;
     176            1 :                 },
     177            0 :                 (e: unknown) => {
     178            0 :                     this.logger.debug("Failed to fetch the query statistics", { "error": String(e), ...(e instanceof LoggableException ? e.keyValues : {}) }, LOG_CTX);
     179            0 :                     return null;
     180            0 :                 },
     181            1 :             )
     182            1 :             .finally(() => {
     183            1 :                 this.queryInfoStats = null;
     184            1 :             });
     185            1 :         this.queryInfoStats = request;
     186            1 :         void request.then(info => {
     187            1 :             if (this.queryInfoStale) {
     188            1 :                 this.queryInfoStale = false;
     189            1 :                 void this.fetchQueryInfoStats();
     190            1 :             } else if (info != null) {
     191            1 :                 this.retryQueryInfoIfIncomplete(info);
     192              :             }
     193            1 :         });
     194            1 :         return request;
     195            1 :     }
     196              : 
     197              :     /// Fetch QueryInfo again later while a stage with done tasks reports no operator summaries and the query runs
     198            1 :     retryQueryInfoIfIncomplete(info: TrinoQueryInfo): void {
     199            1 :         if (this.progressConsumer == null || !this.profileDetail || !lacksOperatorSummaries(info)) return;
     200            1 :         this.queryInfoRetry ??= setTimeout(() => {
     201            1 :             this.queryInfoRetry = null;
     202            1 :             void this.fetchQueryInfoStats();
     203            1 :         }, QUERY_INFO_RETRY_MS);
     204            1 :     }
     205              : 
     206              :     /// Clear the timer of the next QueryInfo
     207            1 :     clearQueryInfoRetry(): void {
     208            1 :         if (this.queryInfoRetry != null) clearTimeout(this.queryInfoRetry);
     209            1 :         this.queryInfoRetry = null;
     210            1 :     }
     211              : 
     212              :     /// Fetch the QueryInfo of a finished query until it is final: then the operator summaries of all stages are
     213              :     /// complete. The query waits for it, but not longer than a bound.
     214            1 :     async finishQueryInfo(finished: boolean): Promise<void> {
     215            1 :         this.clearQueryInfoRetry();
     216            1 :         if (finished && this.profileDetail && this.queryInfoRequested) {
     217            1 :             const deadline = Date.now() + QUERY_INFO_FINAL_TIMEOUT_MS;
     218            1 :             let timer: ReturnType<typeof setTimeout> | null = null;
     219            1 :             const bound = new Promise<void>(resolve => { timer = setTimeout(resolve, QUERY_INFO_FINAL_TIMEOUT_MS); });
     220            1 :             const final = (async () => {
     221              :                 // A pending request may predate the end of the query
     222            1 :                 await this.queryInfoStats;
     223            1 :                 while (true) {
     224            1 :                     const info = await this.fetchQueryInfoStats();
     225            1 :                     if (info == null || isFinalQueryInfo(info) || Date.now() + QUERY_INFO_FINAL_RETRY_MS >= deadline) break;
     226            1 :                     await new Promise(resolve => setTimeout(resolve, QUERY_INFO_FINAL_RETRY_MS));
     227            1 :                 }
     228            1 :             })();
     229            1 :             await Promise.race([final, bound]);
     230            1 :             if (timer != null) clearTimeout(timer);
     231            1 :         }
     232              :         // Progress after the end of the stream is dropped
     233            1 :         this.progressConsumer = null;
     234            1 :         this.produceAbort = null;
     235            1 :         this.clearQueryInfoRetry();
     236            1 :     }
     237              : 
     238              :     /// Fetch the next query result
     239            1 :     async fetchNextQueryResult(abort?: AbortSignal): Promise<TrinoQueryResult | null> {
     240              :         // Do we have a next URI?
     241            1 :         const nextUri = this.latestQueryResult?.nextUri;
     242            1 :         if (!nextUri) {
     243            1 :             return null;
     244              :         }
     245            1 :         this.logger.debug("Fetching next query results", { "nextUri": nextUri }, LOG_CTX);
     246              : 
     247              :         // Get the next query result
     248            1 :         this.queryMetrics.totalQueryRequestsStarted += 1;
     249            1 :         const timeBefore = (new Date()).getTime();
     250            1 :         const queryResult = await this.apiClient.getQueryResult(this.endpoint, nextUri, abort);
     251            1 :         const timeAfter = (new Date()).getTime();
     252            1 :         this.latestQueryResult = queryResult;
     253            1 :         this.latestQueryStats = queryResult.stats ?? null;
     254            1 :         this.latestQueryState = this.latestQueryStats?.state ?? null;
     255            1 :         this.latestQueryReceivedAt = Date.now();
     256              : 
     257              :         // Did the query fail?
     258            1 :         if (queryResult.error) {
     259            1 :             const errorCode = queryResult.error.errorCode;
     260            1 :             const errorName = queryResult.error.errorName;
     261            1 :             const errorType = queryResult.error.errorType;
     262            1 :             const errorMessage = queryResult.error.message;
     263              : 
     264            1 :             const rawError: RawProxyError = {
     265            1 :                 message: "Query returned an error",
     266            1 :                 data: {
     267            1 :                     "errorCode": errorCode.toString(),
     268            1 :                     "errorName": errorName,
     269            1 :                     "errorType": errorType,
     270            1 :                     "errorMessage": errorMessage,
     271            1 :                 },
     272            1 :             };
     273            1 :             const error = new ChannelError(rawError, errorCode, undefined, LOG_CTX);
     274            1 :             if (!this.resultSchema.isResolved()) {
     275            1 :                 this.resultSchema.reject(error);
     276              :             }
     277            1 :             this.queryMetrics.totalQueryRequestsFailed += 1;
     278            1 :             throw error;
     279            1 :         } else {
     280            1 :             this.queryMetrics.totalQueryRequestsSucceeded += 1;
     281              :         }
     282            1 :         this.queryMetrics.totalQueryRequestDurationMs += timeAfter - timeBefore;
     283              : 
     284              :         // Do we already have a schema?
     285            1 :         if (!this.resultSchema.isResolved() && queryResult.columns) {
     286              :             // Attempt to translate the schema
     287            1 :             const translated = translateTrinoSchema(queryResult, this.logger);
     288            1 :             this.resultSchema.resolve(translated);
     289            1 :         }
     290            1 :         return queryResult;
     291            1 :     }
     292              : 
     293              :     /// Get the result metadata (after completion)
     294            0 :     getMetadata(): Map<string, string> {
     295            0 :         return new Map();
     296            0 :     }
     297              :     /// Get the stream metrics
     298            1 :     getMetrics(): QueryExecutionMetrics {
     299            1 :         return this.queryMetrics;
     300            1 :     }
     301              :     /// Get the current query status
     302            0 :     getStatus(): QueryExecutionStatus {
     303            0 :         return this.currentStatus;
     304            0 :     }
     305              :     /// Await the schema message
     306            1 :     async getSchema(): Promise<arrow.Schema | null> {
     307            1 :         return this.resultSchema.getValue();
     308            1 :     }
     309              :     /// Await the next record batch.
     310              :     /// The stream follows the next URIs until Trino stops sending one, the query state does not end the stream:
     311              :     /// Trino reports states like PLANNING before the rows and FINISHED while the coordinator still buffers rows.
     312            1 :     async produce(batches: AsyncConsumer<QueryExecutionResponseStream, arrow.RecordBatch | null>, progress: AsyncConsumer<QueryExecutionResponseStream, QueryExecutionProgress>, abort?: AbortSignal): Promise<void> {
     313            1 :         this.progressConsumer = progress;
     314            1 :         this.produceAbort = abort ?? null;
     315              :         // The response to the query request carries the first progress (QUEUED)
     316            1 :         if (this.latestQueryStats != null) {
     317            1 :             this.latestQueryProgress = deriveProgress(this.latestQueryStats, this.queryMetrics, this.latestQueryReceivedAt);
     318            1 :             progress.resolve(this, this.latestQueryProgress);
     319            1 :             this.requestProfile();
     320            1 :         }
     321            1 :         let pending = this.fetchNextQueryResult(abort);
     322            1 :         try {
     323            1 :             while (true) {
     324            1 :                 const result = await pending;
     325            1 :                 abort?.throwIfAborted();
     326            1 :                 if (result == null) {
     327            1 :                     break;
     328              :                 }
     329              :                 // Request the next page before translating this one, Trino fills the page while we translate
     330            1 :                 pending = this.fetchNextQueryResult(abort);
     331              : 
     332              :                 // Has a data attribute?
     333            1 :                 if (Array.isArray(result.data)) {
     334              :                     // Have result data but not schema yet?
     335              :                     // This is unexpected.
     336            1 :                     if (!this.resultSchema.isResolved()) {
     337            1 :                         throw new Error("Result schema is missing");
     338              :                     }
     339              :                     // Translate the trino batch
     340            1 :                     const schema = this.resultSchema.getResolvedValue();
     341            1 :                     const resultBatch = translateAnyRowsToArrowBatch(schema!, result.data, this.logger);
     342            1 :                     this.queryMetrics.totalBatchesReceived += 1;
     343            1 :                     this.queryMetrics.totalRowsReceived += resultBatch.numRows;
     344              : 
     345              :                     // Publish a new progress update
     346            1 :                     this.latestQueryProgress = deriveProgress(result.stats ?? null, this.queryMetrics, this.latestQueryReceivedAt);
     347            1 :                     progress.resolve(this, this.latestQueryProgress);
     348            1 :                     this.requestProfile();
     349              : 
     350              :                     // Produce a batch
     351            1 :                     batches.resolve(this, resultBatch);
     352            1 :                     abort?.throwIfAborted();
     353            1 :                     await yieldToBrowser();
     354            1 :                 } else {
     355              :                     // Publish the progress update
     356            1 :                     this.latestQueryProgress = deriveProgress(result.stats ?? null, this.queryMetrics, this.latestQueryReceivedAt);
     357            1 :                     progress.resolve(this, this.latestQueryProgress);
     358            1 :                     this.requestProfile();
     359            1 :                 }
     360            1 :             }
     361            1 :             this.logger.debug("Reached end of query result stream", { "queryState": this.latestQueryState }, LOG_CTX);
     362            1 :             await this.finishQueryInfo(true);
     363            1 :         } catch (e: any) {
     364              :             // Don't leave the request of the next page as unhandled rejection
     365            1 :             pending.catch(() => { });
     366            1 :             await this.finishQueryInfo(false);
     367            1 :             throw e;
     368            1 :         }
     369            1 :     }
     370            1 : }
     371              : 
     372              : export interface TrinoChannelInterface {
     373              :     /// Execute Query
     374              :     executeQuery(param: proto.salesforce_hyperdb_grpc_v1.pb.QueryParam, abort?: AbortSignal): Promise<TrinoQueryResultStream>;
     375              :     /// Destroy the connection
     376              :     close(): Promise<void>;
     377              : }
     378              : 
     379            1 : export class TrinoChannel implements TrinoChannelInterface {
     380              :     /// The logger
     381            1 :     logger: Logger;
     382              :     /// The client
     383            1 :     apiClient: TrinoApiClientInterface;
     384              :     /// The trino api endpoint
     385            1 :     endpoint: TrinoApiEndpoint;
     386              :     /// The catalog and schema of the names that statements leave out
     387            1 :     defaults: TrinoSessionDefaults;
     388              : 
     389              :     /// Constructor
     390            1 :     constructor(logger: Logger, client: TrinoApiClientInterface, endpoint: TrinoApiEndpoint, defaults: TrinoSessionDefaults) {
     391            1 :         this.logger = logger;
     392            1 :         this.apiClient = client;
     393            1 :         this.endpoint = endpoint;
     394            1 :         this.defaults = defaults;
     395            1 :     }
     396              : 
     397              :     /// Execute Query
     398            1 :     async executeQuery(param: proto.salesforce_hyperdb_grpc_v1.pb.QueryParam, abort?: AbortSignal): Promise<TrinoQueryResultStream> {
     399            1 :         const metrics = createQueryResponseStreamMetrics();
     400            1 :         const timeBefore = (new Date()).getTime();
     401              : 
     402            1 :         try {
     403            1 :             this.logger.debug("Executing query", {}, LOG_CTX);
     404            1 :             metrics.totalQueryRequestsStarted += 1;
     405              : 
     406            1 :             const result = await this.apiClient.runQuery(this.endpoint, this.defaults, param.query, abort);
     407            1 :             const timeAfter = (new Date()).getTime();
     408              : 
     409            1 :             metrics.totalQueryRequestsSucceeded += 1;
     410            1 :             metrics.totalQueryRequestDurationMs += timeAfter - timeBefore;
     411              : 
     412            1 :             this.logger.debug("Opened query result stream", {}, LOG_CTX);
     413            1 :             const stream = new TrinoQueryResultStream(this.logger, this.apiClient, this.endpoint, result, metrics);
     414            1 :             return stream;
     415            1 :         } catch (e: any) {
     416            1 :             const timeAfter = (new Date()).getTime();
     417            1 :             metrics.totalQueryRequestsFailed += 1;
     418            1 :             metrics.totalQueryRequestDurationMs += timeAfter - timeBefore;
     419            1 :             throw e;
     420            1 :         }
     421            1 :     }
     422              : 
     423              :     /// Destroy the connection
     424            0 :     async close(): Promise<void> {
     425              :         return;
     426            0 :     }
     427            1 : }
     428              : 
     429              : /// Parse a Trino type string and return the corresponding Arrow DataType
     430            1 : function parseTrinoType(typeStr: string): arrow.DataType {
     431            1 :     const type = typeStr.toLowerCase().trim();
     432              : 
     433              :     // Boolean
     434            1 :     if (type === "boolean") {
     435            1 :         return new arrow.Bool();
     436              :     }
     437              :     // Integer types
     438            1 :     if (type === "tinyint") {
     439            1 :         return new arrow.Int8();
     440              :     }
     441            1 :     if (type === "smallint") {
     442            1 :         return new arrow.Int16();
     443              :     }
     444            1 :     if (type === "integer" || type === "int") {
     445            1 :         return new arrow.Int32();
     446              :     }
     447            1 :     if (type === "bigint") {
     448            1 :         return new arrow.Int64();
     449              :     }
     450              :     // Floating-point types
     451            1 :     if (type === "real") {
     452            1 :         return new arrow.Float32();
     453              :     }
     454            1 :     if (type === "double") {
     455            1 :         return new arrow.Float64();
     456              :     }
     457              : 
     458              :     // Decimal types: decimal(precision, scale)
     459            1 :     const decimalMatch = type.match(/^decimal\((\d+),\s*(\d+)\)$/);
     460            1 :     if (decimalMatch) {
     461            1 :         const precision = parseInt(decimalMatch[1], 10);
     462            1 :         const scale = parseInt(decimalMatch[2], 10);
     463            1 :         return new arrow.Decimal(scale, precision, 128);
     464            1 :     }
     465            1 :     if (type === "decimal") {
     466              :         // Default decimal without precision/scale
     467            1 :         return new arrow.Decimal(0, 38, 128);
     468              :     }
     469              : 
     470              :     // String types
     471            1 :     if (type === "varchar" || type.startsWith("varchar(") || type === "char" || type.startsWith("char(")) {
     472            1 :         return new arrow.Utf8();
     473              :     }
     474            1 :     if (type === "varbinary") {
     475            1 :         return new arrow.Binary();
     476              :     }
     477            1 :     if (type === "json") {
     478            1 :         return new arrow.Utf8(); // JSON is represented as string
     479              :     }
     480              :     // Date/Time types
     481            1 :     if (type === "date") {
     482            1 :         return new arrow.DateDay();
     483              :     }
     484            1 :     if (type === "time" || type.startsWith("time(")) {
     485            1 :         return new arrow.TimeMillisecond();
     486              :     }
     487            1 :     if (type === "time with time zone" || type.startsWith("time(") && type.includes("with time zone")) {
     488            0 :         return new arrow.TimeMillisecond();
     489              :     }
     490            1 :     if (type === "timestamp" || type.startsWith("timestamp(")) {
     491            1 :         if (type.includes("with time zone")) {
     492            1 :             return new arrow.TimestampMillisecond("UTC");
     493              :         }
     494            1 :         return new arrow.TimestampMillisecond();
     495            1 :     }
     496              :     // Interval types - represent as strings since Arrow has limited interval support
     497            1 :     if (type.startsWith("interval")) {
     498            1 :         return new arrow.Utf8();
     499              :     }
     500              :     // UUID - represent as string
     501            1 :     if (type === "uuid") {
     502            1 :         return new arrow.Utf8();
     503              :     }
     504              :     // IP address - represent as string
     505            1 :     if (type === "ipaddress") {
     506            1 :         return new arrow.Utf8();
     507              :     }
     508              :     // Complex types (array, map, row) - represent as JSON strings for simplicity
     509              :     // The Arrow JS API for complex types is cumbersome; JSON serialization is practical
     510            1 :     if (type.startsWith("array(") || type.startsWith("map(") || type.startsWith("row(")) {
     511            1 :         return new arrow.Utf8();
     512              :     }
     513              : 
     514              :     // Unknown type - fall back to string representation
     515            1 :     return new arrow.Utf8();
     516            1 : }
     517              : 
     518              : /// Translate the Trino schema
     519            1 : function translateTrinoSchema(result: TrinoQueryResult, logger: Logger): arrow.Schema {
     520            1 :     let fields: arrow.Field[] = [];
     521            1 :     for (const column of result.columns!) {
     522            1 :         const arrowType = parseTrinoType(column.type);
     523            1 :         fields.push(new arrow.Field(column.name, arrowType, true));
     524            1 :     }
     525            1 :     return new arrow.Schema(fields);
     526            1 : }
     527              : 
     528              : /// Derive the execution progress of the statistics of a response that arrived at a time (milliseconds since epoch)
     529            1 : export function deriveProgress(stats: TrinoQueryStatistics | null, metrics: QueryExecutionMetrics, receivedAt: number): QueryExecutionProgress {
     530            1 :     return {
     531            1 :         isQueued: stats?.queued ?? null,
     532            1 :         progressPercent: stats?.progressPercentage ?? null,
     533            1 :         resultRows: null,
     534            1 :         rowsProcessed: stats?.processedRows ?? null,
     535            1 :         executionTimeMs: stats?.elapsedTimeMillis ?? null,
     536            1 :         metrics: { ...metrics },
     537            1 :         phase: stats?.state ?? null,
     538            1 :         engineProgress: stats == null ? null : { kind: 'trino_statement_stats', stats, receivedAt },
     539            1 :     };
     540            1 : }
        

Generated by: LCOV version 2.0-1