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

            Line data    Source code
       1              : import * as buffers from '@ankoh/dashql-core/buffers';
       2              : import type { DashQL } from '@ankoh/dashql-core';
       3              : import type { Logger } from '../platform/logger/logger.js';
       4              : import { stringifyError } from '../platform/logger/logger.js';
       5              : import type { ConnectionState } from './connection_state.js';
       6              : import { decodeConnectionFromProto, restoreConnectionState } from './connection_import.js';
       7              : import { connectionCatalogIsDurable } from './connection_params.js';
       8              : import { SALESFORCE_DATA_CLOUD_CONNECTOR } from './connector_info.js';
       9              : import { beginCatalogBatch, invalidateCatalogBatch, publishCatalogBatch, type BatchProcessor, type CatalogBatchEntry } from './catalog/catalog_batch.js';
      10              : import type { SessionConnection, SessionRegistryBackend } from '../storage/storage_backend.js';
      11              : import { STORAGE_CATALOG_CHANGES, STORAGE_CATALOG_FUNCTIONS, STORAGE_CATALOG_RELATIONS, STORAGE_CATALOG_STATE } from '../storage/storage_backend.js';
      12              : import { packRelationName, parseCatalogStateFile, readRelationName } from './catalog/catalog_sql_generator.js';
      13              : import { mergeRelationNames } from './catalog/catalog_update_state.js';
      14              : 
      15            1 : const LOG_CTX = "connection_restore";
      16              : 
      17              : /// The stored params of a connection, as the app manifest or a session file holds them
      18              : export type StoredConnection = Pick<SessionConnection, 'params' | 'paramsDraft'> & {
      19              :     /// The name of the connection, the app manifest keeps it, an imported session brings it along
      20              :     name?: string;
      21              :     /// Does the connection cache query results? The app manifest keeps it, absent for the default of the connector.
      22              :     cacheQueryResults?: boolean;
      23              : };
      24              : 
      25              : /// Create the state of a connection from its stored params.
      26              : /// The connection is neither registered nor written, see `destroyUnregisteredConnection`.
      27            1 : export function connectionStateFromStorage(core: DashQL, connectionId: string, stored: StoredConnection): ConnectionState {
      28            1 :     const [connectorInfo, details] = decodeConnectionFromProto(stored.params as any, connectionId);
      29            1 :     const state = restoreConnectionState(core, connectionId, connectorInfo, details, stored.paramsDraft);
      30            1 :     return {
      31            1 :         ...state,
      32            1 :         ...(stored.name == null ? {} : { name: stored.name }),
      33            1 :         ...(stored.cacheQueryResults == null ? {} : { cacheQueryResults: stored.cacheQueryResults }),
      34            1 :     };
      35            1 : }
      36              : 
      37              : /// Destroy a connection that was created but never registered
      38            1 : export function destroyUnregisteredConnection(connection: ConnectionState): void {
      39            1 :     invalidateCatalogBatch(connection.catalog);
      40            1 :     connection.catalog.dropDescriptor(connection.catalogRelationEntryId);
      41            1 :     connection.catalog.dropDescriptor(connection.catalogFunctionScript.catalog_entry_id);
      42            1 :     connection.catalogFunctionScript.destroy();
      43            1 :     connection.catalog.destroy();
      44            1 : }
      45              : 
      46              : /// What restoring a catalog needs
      47              : export interface CatalogRestoreContext {
      48              :     /// The core instance
      49              :     core: DashQL;
      50              :     /// The logger
      51              :     logger: Logger;
      52              :     /// The processor of catalog batches, the core processes them if absent
      53              :     processor?: BatchProcessor;
      54              : }
      55              : 
      56              : /// The outcome of restoring the catalog of a connection from its files
      57              : export interface ConnectionCatalogRestore {
      58              :     /// False if the restoration failed, the connection refreshes its catalog on connect
      59              :     succeeded: boolean;
      60              :     /// The files to write: those whose relations or changes were dropped are cleared
      61              :     files: Array<{ fileName: string; text: string }>;
      62              : }
      63              : 
      64              : /// Restore the catalog of a connection from the files of the connection.
      65              : ///
      66              : /// The relations of a connection whose database doesn't outlive the app describe a database that is
      67              : /// gone, they are dropped and their file is reported as stale. Such a catalog is described by a refresh
      68              : /// on connect. A restored catalog is kept until an explicit refresh.
      69              : ///
      70              : /// The relations the connection didn't answer about yet are asked about again once it is online, and those it reported as
      71              : /// gone stay gone. Older versions of the app kept the changes of executed statements in a file of their own. The relations
      72              : /// of that file count as not answered, the file is cleared.
      73            1 : export async function restoreConnectionCatalog(
      74            1 :     ctx: CatalogRestoreContext,
      75            1 :     registry: SessionRegistryBackend,
      76            1 :     connection: ConnectionState,
      77            1 :     logContext: Record<string, string>,
      78            1 : ): Promise<ConnectionCatalogRestore> {
      79            1 :     const { connectionId, catalog, catalogRelationEntryId, catalogFunctionScript } = connection;
      80            1 :     const durable = connectionCatalogIsDurable(connection.params);
      81            1 :     const files: Array<{ fileName: string; text: string }> = [];
      82            1 :     try {
      83            1 :         const validate = beginCatalogBatch(catalog);
      84            1 :         const [relations, functions, changes, state] = await Promise.all([
      85            1 :             registry.loadConnectionFile(connectionId, STORAGE_CATALOG_RELATIONS),
      86            1 :             registry.loadConnectionFile(connectionId, STORAGE_CATALOG_FUNCTIONS),
      87            1 :             registry.loadConnectionFile(connectionId, STORAGE_CATALOG_CHANGES),
      88            1 :             registry.loadConnectionFile(connectionId, STORAGE_CATALOG_STATE),
      89            1 :         ]);
      90            1 :         validate();
      91              :         // Clear the dropped relations that older versions of the app wrote
      92            1 :         if (!durable && relations) files.push({ fileName: STORAGE_CATALOG_RELATIONS, text: '' });
      93              : 
      94            1 :         const catalogScripts: CatalogBatchEntry[] = [];
      95            1 :         if (durable && relations && relations.trim().length > 0) {
      96            1 :             ctx.logger.info("Catalog schema loaded", { ...logContext, schemaLength: relations.length.toString() }, LOG_CTX);
      97              :             // The batch reads the file once, the catalog keeps the descriptor and not the text
      98            1 :             catalogScripts.push({ entryId: catalogRelationEntryId, text: relations });
      99            1 :         }
     100            1 :         if (functions && functions.trim().length > 0) {
     101            1 :             ctx.logger.info("Catalog functions loaded", { ...logContext, functionsLength: functions.length.toString() }, LOG_CTX);
     102            1 :             catalogScripts.push({ script: catalogFunctionScript, text: functions });
     103            1 :         }
     104            1 :         if (catalogScripts.length > 0) {
     105            1 :             await publishCatalogBatch(ctx.core, catalog, catalogScripts, { processor: ctx.processor }, validate);
     106              :             // Only restored relations make a catalog that is kept
     107            1 :             if (durable && relations && relations.trim().length > 0) {
     108            1 :                 connection.catalogUpdates.described = true;
     109              :             }
     110            1 :         }
     111            1 :         const { pending, gone } = parseCatalogStateFile(state);
     112            1 :         if (durable && gone.length > 0) {
     113            1 :             catalog.replaceTables(catalogRelationEntryId, buffers.catalog.CatalogLayer.CONNECTION, { relations: gone.map(packRelationName), tables: [] });
     114              :         }
     115              :         // Nothing that runs on Salesforce changes its relations, and it has no lookup for them
     116            1 :         if (durable && connection.details.type !== SALESFORCE_DATA_CLOUD_CONNECTOR) {
     117            1 :             const changed = changes?.trim() ? ctx.core.touchedRelations(changes).map(name => readRelationName(name)) : [];
     118            1 :             connection.catalogUpdates.touchedRelations = mergeRelationNames(pending, changed);
     119            1 :         }
     120              :         // The relations of the old file move to the new one before the old one is cleared
     121            1 :         if (changes) {
     122            1 :             files.push({ fileName: STORAGE_CATALOG_STATE, text: JSON.stringify({ pending: connection.catalogUpdates.touchedRelations, gone }) });
     123            1 :             files.push({ fileName: STORAGE_CATALOG_CHANGES, text: '' });
     124            1 :         }
     125            1 :         return { succeeded: true, files };
     126            1 :     } catch (error) {
     127            1 :         ctx.logger.warn("Failed to restore connection catalog, will refresh on connect", {
     128            1 :             ...logContext,
     129            1 :             error: stringifyError(error),
     130            1 :         }, LOG_CTX);
     131            1 :         return { succeeded: false, files };
     132            1 :     }
     133            1 : }
        

Generated by: LCOV version 2.0-1