LCOV - code coverage report
Current view: top level - dashql-app/src/connections - connection_state.ts Coverage Total Hit
Test: DashQL 915828b Lines: 97.8 % 359 351
Test Date: 2026-11-06 19:30:32

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

Generated by: LCOV version 2.0-1