LCOV - code coverage report
Current view: top level - dashql-app/src/connections - connection_state.ts Coverage Total Hit
Test: DashQL d805a4b Lines: 97.6 % 340 332
Test Date: 2026-11-09 20:22:13

            Line data    Source code
       1              : import * as core from '@ankoh/dashql-core';
       2              : import * as connection from '@ankoh/dashql-jsonschema/connection.js';
       3              : 
       4              : import type { HyperConnectorAction } from './hyper/hyper_connection_state.js';
       5              : import type { SalesforceConnectionStateAction } from './salesforce/salesforce_connection_state.js';
       6              : import { CatalogUpdateTaskState, reduceCatalogAction } from './catalog/catalog_update_state.js';
       7              : import { CatalogSource } from './catalog/catalog_sql_generator.js';
       8              : import { generateFunctionScriptHeader } from './catalog/catalog_function_sql_generator.js';
       9              : import { VariantKind } from '../utils/variant.js';
      10              : import {
      11              :     CONNECTOR_INFOS,
      12              :     ConnectorInfo,
      13              :     getConnectorInfoForParams,
      14              :     ConnectorType,
      15              : } from './connector_info.js';
      16              : import {
      17              :     createQueryExecutionMetrics,
      18              :     EXECUTE_QUERY,
      19              :     QUERY_CANCELLED,
      20              :     QUERY_FAILED,
      21              :     QUERY_PROGRESS_UPDATED,
      22              :     QUERY_STATEMENT_STARTED,
      23              :     QUERY_STATEMENT_SUCCEEDED,
      24              :     QUERY_RECEIVED_BATCH,
      25              :     QUERY_RUNNING,
      26              :     QUERY_RECEIVED_ALL_BATCHES,
      27              :     QueryExecutionAction,
      28              :     QUERY_SUCCEEDED,
      29              :     QUERY_PROCESSED_RESULTS,
      30              :     QUERY_PREPARING,
      31              :     QUERY_SENDING,
      32              :     QUERY_PROCESSING_RESULTS,
      33              :     QUERY_CACHE_RECORDED,
      34              :     QUERY_CACHE_DELETED,
      35              :     QueryExecutionState,
      36              : } from '../query/query_execution_state.js';
      37              : import { reduceQueryAction } from '../query/query_execution_state.js';
      38              : import type { TrinoConnectorAction } from './trino/trino_connection_state.js';
      39              : import { ConnectionStateDetailsVariant, createConnectionStateDetails, isSalesforceDetails } from './connection_state_details.js';
      40              : import { Logger, stringifyError } from '../platform/logger/logger.js';
      41              : import { invalidateCatalogBatch } from './catalog/catalog_batch.js';
      42              : import { mergeRelationNames } from './catalog/catalog_update_state.js';
      43              : import { readRelationName, type RelationName } from './catalog/catalog_sql_generator.js';
      44              : import { connectionParamsEqual, connectionParamsWithoutDraftRows, getConnectionParamsFromStateDetails, type ConnectionParams } from './connection_params.js';
      45              : import { HYPER_CHANNEL_SETUP_STARTED } from './hyper/hyper_connection_state.js';
      46              : import { OAUTH_STARTED, TRINO_CHANNEL_SETUP_STARTED } from './trino/trino_connection_state.js';
      47              : import { SETUP_STARTED } from './salesforce/salesforce_connection_state.js';
      48              : import { cancelConnectionSetup } from './connection_setup_cancellation.js';
      49              : import { dateToTimestamp } from './proto_helper.js';
      50              : import type { DetailedError } from '../utils/error.js';
      51              : import { catalogNameDefaults, connectionParamsOf, connectorOfDetails, connectorParams } from './connector_descriptor.js';
      52              : 
      53              : const LOG_CTX = 'connection_state';
      54              : 
      55              : export interface CatalogUpdates {
      56              :     /// The running tasks
      57              :     tasksRunning: Map<number, CatalogUpdateTaskState>;
      58              :     /// The finished tasks
      59              :     tasksFinished: Map<number, CatalogUpdateTaskState>;
      60              :     /// Does the catalog describe the connection, from a restored file or a full refresh that succeeded? Until then a
      61              :     /// connection is described when it comes online, and nothing is saved: a few relations aren't a catalog.
      62              :     described: boolean;
      63              :     /// The id of the running or most recently started full refresh, it tells that a refresh is running
      64              :     currentFullRefresh: number | null;
      65              :     /// The relations that executed statements created or dropped and that no update has answered yet
      66              :     touchedRelations: RelationName[];
      67              :     /// The updates in a row that failed to answer them
      68              :     touchedFailures: number;
      69              : }
      70              : 
      71              : export interface ConnectionState {
      72              :     /// Runtime identity of the connection.
      73              :     connectionId: string;
      74              : 
      75              :     /// The connection state contains many references into the Wasm heap.
      76              :     /// It therefore makes sense that connection state users resolve the "right" module through here.
      77              :     instance: core.DashQL;
      78              :     /// Whether the database belongs to a committed session or has connected at least once.
      79              :     /// Draft manifest writes are independent of activation; catalog writes require activation.
      80              :     active: boolean;
      81              : 
      82              :     /// The connection state
      83              :     connectionStatus: ConnectionStatus;
      84              :     /// The connection health
      85              :     connectionHealth: ConnectionHealth;
      86              :     /// The connection info
      87              :     connectorInfo: ConnectorInfo;
      88              :     /// The name the user gave the connection, the label falls back to the connector without it
      89              :     name?: string;
      90              :     /// Does the connection cache query results? Absent for the default of the connector.
      91              :     cacheQueryResults?: boolean;
      92              : 
      93              :     /// The connection details
      94              :     details: ConnectionStateDetailsVariant;
      95              :     /// Last successful parameters (initial defaults until the first successful setup).
      96              :     params: ConnectionParams;
      97              :     /// Local editable settings, independent of the runtime channel and OAuth state.
      98              :     paramsDraft?: ConnectionParams;
      99              :     /// Immutable input of the current setup attempt; never persisted.
     100              :     setupAttemptParams?: ConnectionParams;
     101              :     /// Draft at attempt start, including raw form rows, for exact successful-discard comparison.
     102              :     setupAttemptDraft?: ConnectionParams;
     103              :     /// The connection statistics
     104              :     metrics: connection.ConnectionMetrics;
     105              : 
     106              :     /// The catalog
     107              :     catalog: core.DashQLCatalog;
     108              :     /// The  catalog updates
     109              :     catalogUpdates: CatalogUpdates;
     110              :     /// The catalog entry of the descriptor with the relations of the connection
     111              :     catalogRelationEntryId: number;
     112              :     /// The catalog function script (consolidated SQL for all function declarations)
     113              :     catalogFunctionScript: core.DashQLScript;
     114              : 
     115              :     /// The queries that are currently running
     116              :     queriesActive: Map<number, QueryExecutionState>;
     117              :     /// The active queries ordered
     118              :     queriesActiveOrdered: number[];
     119              :     /// The queries that finished (succeeded, failed, cancelled)
     120              :     queriesFinished: Map<number, QueryExecutionState>;
     121              :     /// The finished queries ordered
     122              :     queriesFinishedOrdered: number[];
     123              : 
     124              :     /// The snapshot of query ids that are active or finished
     125              :     snapshotQueriesActiveFinished: number;
     126              : }
     127              : 
     128            1 : export enum ConnectionStatus {
     129            1 :     NOT_STARTED,
     130              : 
     131              :     // Generate setup status
     132            1 :     HEALTH_CHECK_STARTED,
     133            1 :     HEALTH_CHECK_FAILED,
     134            1 :     HEALTH_CHECK_CANCELLED,
     135            1 :     HEALTH_CHECK_SUCCEEDED,
     136              : 
     137              :     // Channel setup
     138            1 :     CHANNEL_SETUP_STARTED,
     139            1 :     CHANNEL_SETUP_FAILED,
     140            1 :     CHANNEL_SETUP_CANCELLED,
     141            1 :     CHANNEL_READY,
     142              : 
     143              :     // Generate OAuth
     144            1 :     AUTH_STARTED,
     145            1 :     AUTH_CANCELLED,
     146            1 :     AUTH_FAILED,
     147            1 :     PKCE_GENERATION_STARTED,
     148            1 :     PKCE_GENERATED,
     149            1 :     WAITING_FOR_OAUTH_CODE_VIA_WINDOW,
     150            1 :     WAITING_FOR_OAUTH_CODE_VIA_LINK,
     151            1 :     OAUTH_CODE_RECEIVED,
     152            1 :     ACCESS_TOKEN_REQUESTED,
     153            1 :     ACCESS_TOKEN_RECEIVED,
     154              : 
     155              :     // Salesforce OAuth
     156            1 :     DATA_CLOUD_TOKEN_REQUESTED,
     157            1 :     DATA_CLOUD_TOKEN_RECEIVED,
     158            1 :     CORE_ACCESS_TOKEN_REQUESTED,
     159            1 :     CORE_ACCESS_TOKEN_RECEIVED,
     160            1 : }
     161              : 
     162            1 : export enum ConnectionHealth {
     163            1 :     NOT_STARTED = 0,
     164            1 :     CONNECTING = 1,
     165            1 :     CANCELLED = 2,
     166            1 :     ONLINE = 3,
     167            1 :     FAILED = 4,
     168            1 : }
     169              : 
     170              : /// Is a connection connected or connecting? An open connection keeps its protocol and can't be merged.
     171            1 : export function isConnectionOpen(health: ConnectionHealth | undefined): boolean {
     172            1 :     return health === ConnectionHealth.CONNECTING || health === ConnectionHealth.ONLINE;
     173            1 : }
     174              : 
     175            1 : export function printConnectionHealth(health: ConnectionHealth) {
     176            1 :     switch (health) {
     177            1 :         case ConnectionHealth.NOT_STARTED:
     178            1 :             return "Not Started";
     179            0 :         case ConnectionHealth.CONNECTING:
     180            0 :             return "Connecting";
     181            0 :         case ConnectionHealth.CANCELLED:
     182            0 :             return "Cancelled";
     183            0 :         case ConnectionHealth.ONLINE:
     184            0 :             return "Online";
     185            0 :         case ConnectionHealth.FAILED:
     186            0 :             return "Failed";
     187            1 :     }
     188            1 : }
     189              : 
     190              : export type ConnectionStateWithoutId = Omit<ConnectionState, "connectionId">;
     191              : 
     192            1 : export const DELETE_CONNECTION = Symbol('DELETE_CONNECTION');
     193            1 : export const RESET_CONNECTION = Symbol('RESET_CONNECTION');
     194            1 : export const SWITCH_CONNECTOR_TYPE = Symbol('SWITCH_CONNECTOR_TYPE');
     195            1 : export const RELATIONS_TOUCHED = Symbol('RELATIONS_TOUCHED');
     196            1 : export const UPDATE_CATALOG = Symbol('UPDATE_CATALOG');
     197            1 : export const CATALOG_UPDATE_REGISTER_QUERY = Symbol('CATALOG_UPDATE_REGISTER_QUERY');
     198            1 : export const CATALOG_UPDATE_SUCCEEDED = Symbol('CATALOG_UPDATE_SUCCEEDED');
     199            1 : export const CATALOG_UPDATE_PARTIALLY_SUCCEEDED = Symbol('CATALOG_UPDATE_PARTIALLY_SUCCEEDED');
     200            1 : export const CATALOG_UPDATE_FAILED = Symbol('CATALOG_UPDATE_FAILED');
     201            1 : export const CATALOG_UPDATE_CANCELLED = Symbol('CATALOG_UPDATE_CANCELLED');
     202              : 
     203            1 : export const HEALTH_CHECK_STARTED = Symbol('HEALTH_CHECK_STARTED');
     204            1 : export const HEALTH_CHECK_CANCELLED = Symbol('HEALTH_CHECK_CANCELLED');
     205            1 : export const HEALTH_CHECK_SUCCEEDED = Symbol('HEALTH_CHECK_SUCCEEDED');
     206            1 : export const HEALTH_CHECK_FAILED = Symbol('HEALTH_CHECK_FAILED');
     207            1 : export const SET_CONNECTION_DRAFT = Symbol('SET_CONNECTION_DRAFT');
     208            1 : export const DISCARD_CONNECTION_DRAFT = Symbol('DISCARD_CONNECTION_DRAFT');
     209            1 : export const COMMIT_CONNECTION_PARAMS = Symbol('COMMIT_CONNECTION_PARAMS');
     210            1 : export const RENAME_CONNECTION = Symbol('RENAME_CONNECTION');
     211            1 : export const SET_CONNECTION_QUERY_RESULT_CACHING = Symbol('SET_CONNECTION_QUERY_RESULT_CACHING');
     212              : 
     213              : export type CatalogAction =
     214              :     | VariantKind<typeof RELATIONS_TOUCHED, core.buffers.analyzer.QualifiedTableNameT[]>
     215              :     | VariantKind<typeof UPDATE_CATALOG, [number, CatalogUpdateTaskState]>
     216              :     | VariantKind<typeof CATALOG_UPDATE_REGISTER_QUERY, [number, number]>
     217              :     | VariantKind<typeof CATALOG_UPDATE_CANCELLED, [number, Error]>
     218              :     | VariantKind<typeof CATALOG_UPDATE_FAILED, [number, Error]>
     219              :     | VariantKind<typeof CATALOG_UPDATE_SUCCEEDED, [number] | [number, boolean]>
     220              :     | VariantKind<typeof CATALOG_UPDATE_PARTIALLY_SUCCEEDED, [number, Error]>
     221              :     ;
     222              : 
     223              : /// The health check of a connection, every connector runs it the same way
     224              : export type HealthCheckAction =
     225              :     | VariantKind<typeof HEALTH_CHECK_STARTED, null>
     226              :     | VariantKind<typeof HEALTH_CHECK_CANCELLED, null>
     227              :     | VariantKind<typeof HEALTH_CHECK_FAILED, DetailedError>
     228              :     | VariantKind<typeof HEALTH_CHECK_SUCCEEDED, null>
     229              :     ;
     230              : 
     231              : export type ConnectionAction =
     232              :     | VariantKind<typeof SET_CONNECTION_DRAFT, ConnectionParams>
     233              :     | VariantKind<typeof DISCARD_CONNECTION_DRAFT, null>
     234              :     | VariantKind<typeof COMMIT_CONNECTION_PARAMS, ConnectionParams>
     235              :     | VariantKind<typeof RENAME_CONNECTION, string>
     236              :     | VariantKind<typeof SET_CONNECTION_QUERY_RESULT_CACHING, boolean>
     237              :     | VariantKind<typeof DELETE_CONNECTION, null>
     238              :     | VariantKind<typeof RESET_CONNECTION, null>
     239              :     | VariantKind<typeof SWITCH_CONNECTOR_TYPE, ConnectorType>
     240              :     | HealthCheckAction
     241              :     | CatalogAction
     242              :     | QueryExecutionAction
     243              :     | HyperConnectorAction
     244              :     | TrinoConnectorAction
     245              :     | SalesforceConnectionStateAction
     246              :     ;
     247              : 
     248              : /// Does a connection cache query results? The connector decides unless the user chose.
     249            1 : export function connectionCachesQueryResults(state: Pick<ConnectionState, 'cacheQueryResults' | 'connectorInfo'>): boolean {
     250            1 :     return state.cacheQueryResults ?? state.connectorInfo.features.queryResultCaching;
     251            1 : }
     252              : 
     253            1 : export function reduceConnectionState(state: ConnectionState, action: ConnectionAction, logger: Logger): ConnectionState {
     254            1 :     switch (action.type) {
     255            1 :         case SET_CONNECTION_DRAFT: {
     256            1 :             if (getConnectorInfoForParams(action.value)?.connectorType !== state.connectorInfo.connectorType) return state;
     257            1 :             const paramsDraft = structuredClone(action.value);
     258            1 :             if (connectionParamsEqual(state.paramsDraft, paramsDraft)) return state;
     259            1 :             return { ...state, paramsDraft };
     260            1 :         }
     261              :         // COMMIT_CONNECTION_PARAMS makes saved params the params of the connection.
     262              :         // The draft is cleared unless it changed while saving.
     263            1 :         case COMMIT_CONNECTION_PARAMS: {
     264            1 :             const params = connectionParamsWithoutDraftRows(structuredClone(action.value));
     265            1 :             const draftSaved = state.paramsDraft == null
     266            1 :                 || connectionParamsEqual(connectionParamsWithoutDraftRows(state.paramsDraft), params);
     267            1 :             if (connectionParamsEqual(state.params, params) && (state.paramsDraft == null || !draftSaved)) return state;
     268            1 :             return { ...state, params, paramsDraft: draftSaved ? undefined : state.paramsDraft };
     269            1 :         }
     270              :         // A blank name clears the name
     271            1 :         case RENAME_CONNECTION: {
     272            1 :             const name = action.value.trim() || undefined;
     273            1 :             if (name === state.name) return state;
     274            1 :             return { ...state, name };
     275            1 :         }
     276              :         // The connection keeps the choice, also when it matches the default of the connector
     277            1 :         case SET_CONNECTION_QUERY_RESULT_CACHING: {
     278            1 :             if (state.cacheQueryResults === action.value) return state;
     279            1 :             return { ...state, cacheQueryResults: action.value };
     280              :         }
     281            1 :         case DISCARD_CONNECTION_DRAFT: {
     282            1 :             if (state.connectionHealth === ConnectionHealth.CONNECTING) return state;
     283            1 :             const details = {
     284            1 :                 ...state.details,
     285            1 :                 value: {
     286            1 :                     ...state.details.value,
     287            1 :                     proto: {
     288            1 :                         ...state.details.value.proto,
     289            1 :                         setupError: undefined,
     290            1 :                         channelError: undefined,
     291            1 :                         schemaResolutionError: undefined,
     292            1 :                         healthCheckError: undefined,
     293            1 :                     },
     294            1 :                 },
     295            1 :             } as unknown as ConnectionStateDetailsVariant;
     296            1 :             return { ...state, paramsDraft: undefined, details };
     297            1 :         }
     298            1 :         case RELATIONS_TOUCHED:
     299            1 :             return recordTouchedRelations(state, action.value);
     300              : 
     301            1 :         case UPDATE_CATALOG:
     302            1 :         case CATALOG_UPDATE_REGISTER_QUERY:
     303            1 :         case CATALOG_UPDATE_CANCELLED:
     304            1 :         case CATALOG_UPDATE_SUCCEEDED:
     305            1 :         case CATALOG_UPDATE_PARTIALLY_SUCCEEDED:
     306            1 :         case CATALOG_UPDATE_FAILED:
     307            1 :             return reduceCatalogAction(state, action);
     308              : 
     309            1 :         case EXECUTE_QUERY:
     310            1 :         case QUERY_PREPARING:
     311            1 :         case QUERY_RUNNING:
     312            1 :         case QUERY_SENDING:
     313            1 :         case QUERY_PROGRESS_UPDATED:
     314            1 :         case QUERY_STATEMENT_STARTED:
     315            1 :         case QUERY_STATEMENT_SUCCEEDED:
     316            1 :         case QUERY_RECEIVED_BATCH:
     317            1 :         case QUERY_RECEIVED_ALL_BATCHES:
     318            1 :         case QUERY_PROCESSING_RESULTS:
     319            1 :         case QUERY_PROCESSED_RESULTS:
     320            1 :         case QUERY_SUCCEEDED:
     321            1 :         case QUERY_CANCELLED:
     322            1 :         case QUERY_FAILED:
     323            1 :         case QUERY_CACHE_RECORDED:
     324            1 :         case QUERY_CACHE_DELETED:
     325            1 :             return reduceQueryAction(state, action);
     326              : 
     327              :         // RESET_CONNECTION stops the connection and clears its catalog
     328            1 :         case RESET_CONNECTION: {
     329              :             // Pending catalog publishers are aborted before the catalog is cleared
     330            1 :             const newState = stopConnection(state, action, 'reset');
     331            1 :             state.catalog.clear();
     332            1 :             return newState;
     333            1 :         }
     334              : 
     335              :         // SWITCH_CONNECTOR_TYPE changes the connection's type in-place.
     336              :         // Only allowed when the connection hasn't been configured yet.
     337            1 :         case SWITCH_CONNECTOR_TYPE: {
     338            1 :             if (state.connectionStatus !== ConnectionStatus.NOT_STARTED || state.active || state.paramsDraft != null) {
     339            1 :                 return state;
     340              :             }
     341            1 :             const newType = action.value;
     342            1 :             if (state.connectorInfo.connectorType === newType) {
     343            1 :                 return state;
     344              :             }
     345            1 :             const newInfo = CONNECTOR_INFOS[newType as number];
     346            1 :             const newDetails = createConnectionStateDetails(newType);
     347            1 :             return {
     348            1 :                 ...state,
     349            1 :                 connectorInfo: newInfo,
     350            1 :                 details: newDetails,
     351            1 :                 params: getConnectionParamsFromStateDetails(newDetails)!,
     352            1 :                 paramsDraft: undefined,
     353            1 :                 setupAttemptParams: undefined,
     354            1 :                 setupAttemptDraft: undefined,
     355            1 :             };
     356            1 :         }
     357              : 
     358              : 
     359              :         /// DELETE_CONNECTION stops the connection and destroys its catalog.
     360              :         /// The registry refuses to delete connections that sessions reference.
     361            1 :         case DELETE_CONNECTION: {
     362            1 :             const newState = stopConnection(state, action, 'deleted');
     363              : 
     364              :             // Text-only source scripts do not own their published descriptors.
     365            1 :             try {
     366            1 :                 state.catalog.dropDescriptor(state.catalogRelationEntryId);
     367            1 :             } catch (e) {
     368              :                 // Descriptor may have already been dropped - ignore error
     369            1 :             }
     370            1 :             try {
     371            1 :                 state.catalog.dropDescriptor(state.catalogFunctionScript.catalog_entry_id);
     372            1 :             } catch (e) {
     373              :                 // Descriptor may have already been dropped - ignore error
     374            1 :             }
     375            1 :             state.catalogFunctionScript.destroy();
     376              : 
     377              :             // Delete the conneciton catalog
     378            1 :             state.catalog.destroy();
     379              : 
     380            1 :             return newState;
     381            1 :         }
     382              : 
     383            1 :         default: {
     384              :             // Capture setup input before connector reducers replace runtime setupParams.
     385            1 :             let setupAttemptParams = state.setupAttemptParams;
     386            1 :             let setupAttemptDraft = state.setupAttemptDraft;
     387            1 :             switch (action.type) {
     388            1 :                 case HYPER_CHANNEL_SETUP_STARTED:
     389            1 :                     setupAttemptParams = structuredClone({ hyper: action.value });
     390            1 :                     setupAttemptDraft = state.paramsDraft;
     391            1 :                     break;
     392            1 :                 case OAUTH_STARTED:
     393            1 :                 case TRINO_CHANNEL_SETUP_STARTED:
     394            1 :                     setupAttemptParams = structuredClone({ trino: action.value });
     395            1 :                     setupAttemptDraft = state.setupAttemptParams == null ? state.paramsDraft : state.setupAttemptDraft;
     396            1 :                     break;
     397            1 :                 case SETUP_STARTED:
     398            1 :                     setupAttemptParams = structuredClone(connectionParamsOf(connectorOfDetails(state.details), action.value));
     399            1 :                     setupAttemptDraft = state.paramsDraft;
     400              :                     break;
     401            1 :             }
     402            1 :             let newState = reduceHealthCheck(state, action) ?? reduceConnectorDetails(state, action);
     403            1 :             if (newState == null) {
     404            1 :                 throw new Error(`failed to apply state action: ${String(action.type)}`);
     405              :             }
     406            1 :             newState = { ...newState, setupAttemptParams, setupAttemptDraft };
     407            1 :             if (newState.connectionHealth === ConnectionHealth.ONLINE
     408            1 :                 && state.connectionHealth !== ConnectionHealth.ONLINE && setupAttemptParams != null) {
     409            1 :                 newState = {
     410            1 :                     ...newState,
     411            1 :                     params: connectionParamsWithoutDraftRows(setupAttemptParams),
     412            1 :                     paramsDraft: connectionParamsEqual(state.paramsDraft, setupAttemptDraft)
     413            1 :                         && (setupAttemptDraft == null || connectionParamsEqual(connectionParamsWithoutDraftRows(setupAttemptDraft), connectionParamsWithoutDraftRows(setupAttemptParams)))
     414            1 :                         ? undefined : state.paramsDraft,
     415            1 :                     setupAttemptParams: undefined,
     416            1 :                     setupAttemptDraft: undefined,
     417            1 :                 };
     418            1 :                 const connector = connectorOfDetails(newState.details);
     419            1 :                 if (isSalesforceDetails(newState.details) && connector.paramsKey in setupAttemptParams) {
     420            1 :                     const salesforceParams = connectorParams(connector, setupAttemptParams) as connection.SalesforceConnectionParams;
     421            1 :                     newState.params = connectionParamsOf(connector, {
     422            1 :                         ...salesforceParams,
     423            1 :                         login: newState.details.value.proto.setupParams?.login ?? salesforceParams.login,
     424            1 :                     });
     425            1 :                 }
     426            1 :             } else if (newState.connectionHealth === ConnectionHealth.FAILED || newState.connectionHealth === ConnectionHealth.CANCELLED) {
     427            1 :                 newState.setupAttemptParams = undefined;
     428            1 :                 newState.setupAttemptDraft = undefined;
     429            1 :             }
     430              : 
     431              :             // Embedded connectors become online as soon as their local channel is ready, without a
     432              :             // health-check action. Activate every database on its first transition to online.
     433            1 :             if (newState.connectionHealth === ConnectionHealth.ONLINE && !newState.active) {
     434            1 :                 newState = { ...newState, active: true };
     435              :             }
     436              : 
     437            1 :             return newState;
     438            1 :         }
     439            1 :     }
     440            1 : }
     441              : 
     442              : /// Apply a health check action, null for other actions
     443            1 : function reduceHealthCheck(state: ConnectionState, action: ConnectionAction): ConnectionState | null {
     444            1 :     const now = dateToTimestamp(new Date());
     445            1 :     switch (action.type) {
     446            1 :         case HEALTH_CHECK_STARTED:
     447            1 :             return withConnectorProto({ ...state, connectionStatus: ConnectionStatus.HEALTH_CHECK_STARTED, connectionHealth: ConnectionHealth.CONNECTING },
     448            1 :                 proto => ({ ...proto, setupTimings: { ...proto.setupTimings, healthCheckStartedAt: now } }));
     449            1 :         case HEALTH_CHECK_FAILED:
     450            1 :             return withConnectorProto({ ...state, connectionStatus: ConnectionStatus.HEALTH_CHECK_FAILED, connectionHealth: ConnectionHealth.FAILED },
     451            1 :                 proto => ({ ...proto, setupTimings: { ...proto.setupTimings, healthCheckFailedAt: now }, healthCheckError: action.value }));
     452            1 :         case HEALTH_CHECK_CANCELLED:
     453            1 :             return withConnectorProto({ ...state, connectionStatus: ConnectionStatus.HEALTH_CHECK_CANCELLED, connectionHealth: ConnectionHealth.CANCELLED },
     454            1 :                 proto => ({ ...proto, setupTimings: { ...proto.setupTimings, healthCheckCancelledAt: now } }));
     455            1 :         case HEALTH_CHECK_SUCCEEDED:
     456            1 :             return withConnectorProto({ ...state, connectionStatus: ConnectionStatus.HEALTH_CHECK_SUCCEEDED, connectionHealth: ConnectionHealth.ONLINE },
     457            1 :                 proto => ({ ...proto, setupTimings: { ...proto.setupTimings, healthCheckSucceededAt: now } }));
     458            1 :         default:
     459            1 :             return null;
     460            1 :     }
     461            1 : }
     462              : 
     463              : /// The setup timings and errors that the details of every connector keep
     464              : type ConnectorProto = Pick<ConnectionStateDetailsVariant['value']['proto'], 'setupTimings' | 'healthCheckError'>;
     465              : 
     466              : /// Update the setup timings and errors of the connector details
     467            1 : function withConnectorProto(state: ConnectionState, patch: (proto: ConnectorProto) => ConnectorProto): ConnectionState {
     468            1 :     const details = state.details.value;
     469            1 :     return {
     470            1 :         ...state,
     471            1 :         details: { ...state.details, value: { ...details, proto: { ...details.proto, ...patch(details.proto) } } } as ConnectionStateDetailsVariant,
     472            1 :     };
     473            1 : }
     474              : 
     475              : /// Let the reducer of the connector apply an action to the connection details
     476            1 : function reduceConnectorDetails(state: ConnectionState, action: ConnectionAction): ConnectionState | null {
     477            1 :     return connectorOfDetails(state.details).reduce(state, action);
     478            1 : }
     479              : 
     480              : /// Stop a connection that is reset or deleted.
     481              : /// Cancels the setup and the running catalog updates and drops the query executions.
     482            1 : function stopConnection(state: ConnectionState, action: ConnectionAction, reason: 'reset' | 'deleted'): ConnectionState {
     483            1 :     cancelConnectionSetup(state.connectionId);
     484            1 :     invalidateCatalogBatch(state.catalog);
     485            1 :     for (const task of state.catalogUpdates.tasksRunning.values()) {
     486            1 :         task.cancellation.abort(new DOMException(`Connection was ${reason}`, 'AbortError'));
     487              :     }
     488              :     // The queries run on the channel of the stopped connection, the executor stops them at the backend
     489            1 :     for (const query of state.queriesActive.values()) {
     490            1 :         query.cancellation.abort(new DOMException(`Connection was ${reason}`, 'AbortError'));
     491              :     }
     492            1 :     const cleaned: ConnectionState = {
     493            1 :         ...state,
     494            1 :         connectionStatus: ConnectionStatus.NOT_STARTED,
     495            1 :         connectionHealth: ConnectionHealth.NOT_STARTED,
     496            1 :         setupAttemptParams: undefined,
     497            1 :         setupAttemptDraft: undefined,
     498            1 :         metrics: createConnectionMetrics(),
     499            1 :         catalogUpdates: {
     500            1 :             tasksRunning: new Map(),
     501            1 :             tasksFinished: new Map(),
     502            1 :             currentFullRefresh: null,
     503            1 :             described: false,
     504            1 :             touchedRelations: [],
     505            1 :             touchedFailures: 0,
     506            1 :         },
     507            1 :         queriesActive: new Map(),
     508            1 :         queriesActiveOrdered: [],
     509            1 :         queriesFinished: new Map(),
     510            1 :         queriesFinishedOrdered: [],
     511            1 :         snapshotQueriesActiveFinished: state.snapshotQueriesActiveFinished + 1,
     512            1 :     };
     513              :     // Cleaning up the details is best-effort, the connector may ignore the action
     514            1 :     return reduceConnectorDetails(cleaned, action) ?? cleaned;
     515            1 : }
     516              : 
     517              : /// Record the relations that an executed statement touched, the catalog loader asks the connection about them. The
     518              : /// defaults of the connection name the database, a statement without a schema leaves that to the connection.
     519            1 : function recordTouchedRelations(state: ConnectionState, touched: core.buffers.analyzer.QualifiedTableNameT[]): ConnectionState {
     520            1 :     const defaults = catalogNameDefaults(state.details);
     521              :     // A name without a database needs one, without a default the statement can't have touched anything
     522            1 :     const relations = touched.map(name => readRelationName(name, defaults.databaseName)).filter(relation => relation.databaseName !== '');
     523            1 :     return {
     524            1 :         ...state,
     525            1 :         catalogUpdates: {
     526            1 :             ...state.catalogUpdates,
     527            1 :             touchedRelations: mergeRelationNames(state.catalogUpdates.touchedRelations, relations),
     528            1 :             touchedFailures: 0,
     529            1 :         },
     530            1 :     };
     531            1 : }
     532              : 
     533            1 : export function createConnectionMetrics(): connection.ConnectionMetrics {
     534            1 :     return createQueryExecutionMetrics();
     535            1 : }
     536              : 
     537              : /// The optional properties of a new connection state
     538              : export interface ConnectionStateOptions {
     539              :     /// Does the connection belong to a committed session or was it connected once?
     540              :     active?: boolean;
     541              :     /// The params, derived from the details by default
     542              :     params?: ConnectionParams;
     543              :     /// The draft of the params
     544              :     paramsDraft?: ConnectionParams;
     545              : }
     546              : 
     547              : /// Create the state of a connection with an empty catalog
     548            1 : export function createConnectionState(dql: core.DashQL, info: ConnectorInfo, details: ConnectionStateDetailsVariant, options: ConnectionStateOptions = {}): ConnectionStateWithoutId {
     549            1 :     let catalog: core.DashQLCatalog | null = null;
     550            1 :     let catalogRelationEntryId = 0;
     551            1 :     let catalogFunctionScript: core.DashQLScript | null = null;
     552            1 :     try {
     553            1 :         catalog = dql.createCatalog();
     554            1 :         catalogRelationEntryId = catalog.allocateEntryId();
     555            1 :         catalogFunctionScript = dql.createScript(catalog);
     556            1 :         catalogFunctionScript.replaceText(generateFunctionScriptHeader(CatalogSource.Unknown));
     557            1 :     } catch (error) {
     558            1 :         catalogFunctionScript?.destroy();
     559            1 :         catalog?.destroy();
     560            1 :         throw error;
     561            1 :     }
     562            1 :     return {
     563            1 :         instance: dql,
     564            1 :         active: options.active ?? false,
     565            1 :         connectionStatus: ConnectionStatus.NOT_STARTED,
     566            1 :         connectionHealth: ConnectionHealth.NOT_STARTED,
     567            1 :         connectorInfo: info,
     568            1 :         metrics: createConnectionMetrics(),
     569            1 :         details,
     570            1 :         params: options.params == null ? getConnectionParamsFromStateDetails(details)! : structuredClone(options.params),
     571            1 :         paramsDraft: options.paramsDraft == null ? undefined : structuredClone(options.paramsDraft),
     572            1 :         catalog,
     573            1 :         catalogUpdates: {
     574            1 :             tasksRunning: new Map(),
     575            1 :             tasksFinished: new Map(),
     576            1 :             currentFullRefresh: null,
     577            1 :             described: false,
     578            1 :             touchedRelations: [],
     579            1 :             touchedFailures: 0,
     580            1 :         },
     581            1 :         catalogRelationEntryId,
     582            1 :         catalogFunctionScript,
     583            1 :         snapshotQueriesActiveFinished: 1,
     584            1 :         queriesActive: new Map(),
     585            1 :         queriesActiveOrdered: [],
     586            1 :         queriesFinished: new Map(),
     587            1 :         queriesFinishedOrdered: [],
     588            1 :     };
     589            1 : }
        

Generated by: LCOV version 2.0-1