LCOV - code coverage report
Current view: top level - dashql-app/src/session/inputs - session_inputs.ts Coverage Total Hit
Test: DashQL d805a4b Lines: 92.5 % 160 148
Test Date: 2026-11-09 20:22:13

            Line data    Source code
       1              : import * as core from '@ankoh/dashql-core';
       2              : 
       3              : import { scriptDisplayName } from '../scripts/script_types.js';
       4              : import { formattingDialectForConnector } from '../scripts/script_format.js';
       5              : import { attachmentPrefix, planAttachments } from '../attachments/session_attachments.js';
       6              : import { orderedSessionScripts, sessionAnalysisSignature, type ScriptData, type SessionScripts } from '../scripts/session_scripts.js';
       7              : 
       8            1 : const SqlTypeKind = core.buffers.analyzer.SqlTypeKind;
       9            1 : const EdgeKind = core.buffers.execution.DataflowEdgeKind;
      10              : 
      11              : /// The control that edits the value of an input, from its type
      12            1 : export enum SessionInputControl {
      13            1 :     Number = 'number',
      14            1 :     Integer = 'integer',
      15            1 :     Boolean = 'boolean',
      16            1 :     Date = 'date',
      17            1 :     Timestamp = 'timestamp',
      18            1 :     Interval = 'interval',
      19            1 :     Text = 'text',
      20            1 : }
      21              : 
      22              : /// The type of an input, of a cast of the parameter or of the literal of its value
      23              : export interface SessionInputType {
      24              :     kind: core.buffers.analyzer.SqlTypeKind;
      25              :     /// The SQL text of the type, e.g. `numeric(10,2)`
      26              :     sql: string;
      27              :     arrayDimensions: number;
      28              :     withTimeZone: boolean;
      29              :     /// Is the type the type of a cast? The type of a literal follows the value.
      30              :     cast: boolean;
      31              :     /// Is the type known? An input without a cast and without a value is text until it has one.
      32              :     known: boolean;
      33              : }
      34              : 
      35              : /// A statement that uses an input
      36              : export interface SessionInputConsumer {
      37              :     scriptKey: number;
      38              :     statementId: number;
      39              : }
      40              : 
      41              : /// An input of a session: a named scalar parameter of its scripts, see docs/design/session_inputs.md
      42              : export interface SessionInput {
      43              :     name: string;
      44              :     type: SessionInputType;
      45              :     /// The value as a control shows it, null if no script has a value: the number, the content of a string, the text of
      46              :     /// a typed literal
      47              :     value: string | null;
      48              :     /// The value as SQL literal in the Hyper dialect, the value of the `@param` tags
      49              :     literal: string | null;
      50              :     /// The statements that use the input, in session order
      51              :     consumers: SessionInputConsumer[];
      52              :     /// The problems of the input, e.g. casts to different types
      53              :     warnings: string[];
      54              : }
      55              : 
      56            1 : const NO_INPUTS: SessionInput[] = [];
      57              : 
      58              : /// The request of the core for the scripts of a session, connections in the order of their first script. A
      59              : /// connection speaks the dialect of its connector, the planner prints the values of inputs in it. With the session,
      60              : /// the request holds its attachments and the directory the embedded Hyper mounts them in.
      61            1 : export function sessionPlanRequest(
      62            1 :     scripts: readonly ScriptData[],
      63            1 :     files: readonly string[],
      64            1 :     session?: Pick<SessionScripts, 'sessionId' | 'attachments'>,
      65            1 : ): core.SessionPlanRequest {
      66            1 :     const connections: ScriptData['connection'][] = [];
      67            1 :     for (const script of scripts) {
      68            1 :         if (connections.some(c => c.connectionId === script.connection.connectionId)) continue;
      69            1 :         connections.push(script.connection);
      70            1 :     }
      71            1 :     const connectionIds = connections.map(c => c.connectionId);
      72            1 :     return {
      73            1 :         connections: connections.map(c => ({
      74            1 :             id: c.connectionId,
      75            1 :             local: c.local ?? false,
      76            1 :             catalog: c.catalog,
      77            1 :             dialect: formattingDialectForConnector(c.connectorInfo?.connectorType),
      78            1 :             defaults: c.catalogNameDefaults,
      79            1 :         })),
      80            1 :         scripts: scripts.map((script, index) => ({
      81            1 :             name: scriptDisplayName(files[index]),
      82            1 :             connection: connectionIds.indexOf(script.connection.connectionId),
      83            1 :             session: script.scriptSession,
      84            1 :         })),
      85            1 :         attachments: planAttachments(session?.attachments),
      86            1 :         attachmentPrefix: session == null ? undefined : attachmentPrefix(session.sessionId),
      87            1 :     };
      88            1 : }
      89              : 
      90              : /// The type of an input of the core
      91            1 : export function readSessionInputType(type: core.buffers.analyzer.SqlTypeT | null, source: core.buffers.execution.SessionInputTypeSource): SessionInputType {
      92            1 :     return {
      93            1 :         kind: type?.kind ?? SqlTypeKind.TEXT,
      94            1 :         sql: String(type?.sql ?? 'text'),
      95            1 :         arrayDimensions: type?.arrayDimensions ?? 0,
      96            1 :         withTimeZone: type?.withTimeZone ?? false,
      97            1 :         cast: source === core.buffers.execution.SessionInputTypeSource.CAST,
      98            1 :         known: source !== core.buffers.execution.SessionInputTypeSource.NONE,
      99            1 :     };
     100            1 : }
     101              : 
     102              : /// Collect the inputs of a plan request with the core, `scripts` are the scripts of the request that have a key
     103            1 : function readSessionInputs(instance: core.DashQL, request: core.SessionPlanRequest, scripts: readonly Pick<ScriptData, 'scriptKey'>[]): SessionInput[] {
     104            1 :     const collected = instance.collectSessionInputs(request).unpack() as core.buffers.execution.SessionInputsT;
     105            1 :     return collected.inputs.map(input => ({
     106            1 :         name: String(input.name ?? ''),
     107            1 :         type: readSessionInputType(input.sqlType, input.typeSource),
     108            1 :         value: input.hasValue ? String(input.value ?? '') : null,
     109            1 :         literal: input.hasValue ? String(input.literal ?? '') : null,
     110            1 :         consumers: input.consumers.flatMap(c => {
     111            1 :             const script = scripts[c.script];
     112            1 :             return script == null ? [] : [{ scriptKey: script.scriptKey, statementId: c.statement }];
     113            1 :         }),
     114            1 :         warnings: input.warnings.map(String),
     115            1 :     }));
     116            1 : }
     117              : 
     118              : /// Collect the inputs of a session with the core
     119            1 : function collectSessionInputs(session: SessionScripts): SessionInput[] {
     120            1 :     const scripts = orderedSessionScripts(session);
     121            1 :     if (scripts.length === 0 || typeof session.instance?.collectSessionInputs !== 'function') return NO_INPUTS;
     122            1 :     return readSessionInputs(session.instance, sessionPlanRequest(scripts, scripts.map(s => s.fileName), session), scripts);
     123            1 : }
     124              : 
     125              : /// The inputs of the draft of a composer, a script session that is no session script: nothing consumes them yet
     126            1 : export function collectDraftInputs(instance: core.DashQL, draft: core.DashQLScriptSession, catalog: core.DashQLCatalog): SessionInput[] {
     127            1 :     try {
     128            1 :         return readSessionInputs(instance, {
     129            1 :             connections: [{ id: 'draft', local: false, catalog }],
     130            1 :             scripts: [{ name: 'draft', connection: 0, session: draft }],
     131            1 :         }, []);
     132            1 :     } catch (e) {
     133            0 :         console.warn('cannot collect the inputs of the draft', e);
     134            0 :         return NO_INPUTS;
     135            0 :     }
     136            1 : }
     137              : 
     138              : /// The scripts that use an input, without duplicates
     139            0 : export function inputConsumerScripts(input: SessionInput): number[] {
     140            0 :     return [...new Set(input.consumers.map(c => c.scriptKey))];
     141            0 : }
     142              : 
     143              : interface CachedInputs {
     144              :     signature: string;
     145              :     inputs: SessionInput[];
     146              : }
     147              : 
     148              : /// The last inputs of every session, by session id
     149            1 : const INPUTS = new Map<string, CachedInputs>();
     150              : 
     151            1 : export function sameInputs(l: SessionInput[], r: SessionInput[]): boolean {
     152            1 :     return JSON.stringify(l) === JSON.stringify(r);
     153            1 : }
     154              : 
     155              : /// The inputs of a session, collected again when a script or its analysis changed. A script that is being edited
     156              : /// keeps the inputs of its last analysis: an input stays while its analysis lags behind its text.
     157            1 : export function deriveSessionInputs(session: SessionScripts): SessionInput[] {
     158              :     // Sessions without the core, e.g. of tests, have no inputs
     159            1 :     if (session.scriptOrder == null || session.scriptOrder.length === 0 || typeof session.instance?.collectSessionInputs !== 'function') return NO_INPUTS;
     160            1 :     const signature = sessionAnalysisSignature(session);
     161            1 :     const cached = INPUTS.get(session.sessionId);
     162            1 :     if (cached?.signature === signature) return cached.inputs;
     163            1 :     let inputs: SessionInput[];
     164            1 :     try {
     165            1 :         inputs = collectSessionInputs(session);
     166            1 :     } catch (e) {
     167            1 :         console.warn('cannot collect the inputs of the session', e);
     168            1 :         return cached?.inputs ?? NO_INPUTS;
     169            1 :     }
     170            1 :     if (cached != null && sameInputs(cached.inputs, inputs)) inputs = cached.inputs;
     171            1 :     INPUTS.set(session.sessionId, { signature, inputs });
     172            1 :     return inputs;
     173            1 : }
     174              : 
     175              : /// The control of the value of an input
     176            1 : export function sessionInputControl(type: SessionInputType): SessionInputControl {
     177            1 :     if (type.arrayDimensions > 0) return SessionInputControl.Text;
     178            1 :     switch (type.kind) {
     179            0 :         case SqlTypeKind.INT1:
     180            0 :         case SqlTypeKind.INT2:
     181            1 :         case SqlTypeKind.INT4:
     182            1 :         case SqlTypeKind.INT8:
     183            1 :             return type.cast ? SessionInputControl.Integer : SessionInputControl.Number;
     184            1 :         case SqlTypeKind.INTERVAL:
     185            1 :             return SessionInputControl.Interval;
     186            0 :         case SqlTypeKind.FLOAT4:
     187            0 :         case SqlTypeKind.FLOAT8:
     188            1 :         case SqlTypeKind.NUMERIC:
     189            1 :             return SessionInputControl.Number;
     190            1 :         case SqlTypeKind.BOOL:
     191            1 :             return SessionInputControl.Boolean;
     192            1 :         case SqlTypeKind.DATE:
     193            1 :             return SessionInputControl.Date;
     194            1 :         case SqlTypeKind.TIMESTAMP:
     195            1 :             return SessionInputControl.Timestamp;
     196            1 :         default:
     197            1 :             return SessionInputControl.Text;
     198            1 :     }
     199            1 : }
     200              : 
     201              : /// The literal of the value of a control in the Hyper dialect, the value of the `@param` tags, or the problem with the
     202              : /// value. The type of the input decides what the value is (`SqlLiteral::from_value` of the core), an input whose type
     203              : /// is unknown reads a number, `true`, `false` or a string.
     204            1 : export function sessionInputLiteral(instance: core.DashQL, type: SessionInputType, value: string): { literal: string } | { error: string } {
     205            1 :     if (value.trim().length === 0) return { error: 'The input has no value' };
     206            1 :     try {
     207            1 :         return { literal: instance.inputLiteral(value, type.known ? type.sql : null) };
     208            1 :     } catch (e: any) {
     209            0 :         return { error: String(e?.message ?? e) };
     210            0 :     }
     211            1 : }
     212              : 
     213              : /// The scripts that an input change reruns: the scripts that use one of the inputs and every script downstream of
     214              : /// them, by the references and relation edges of the dataflow. A script that runs only on request (`@manual`) is left
     215              : /// out, the scripts downstream of it are not. Indexes the scripts of the dataflow.
     216            1 : export function affectedScriptIndices(dataflow: core.buffers.execution.SessionDataflowT, inputs: readonly string[]): number[] {
     217            1 :     const names = new Set(inputs);
     218            1 :     const affected = new Set<number>();
     219            1 :     const pending: number[] = [];
     220            1 :     for (const edge of dataflow.edges) {
     221            1 :         if (edge.kind !== EdgeKind.INPUT) continue;
     222            1 :         const input = dataflow.inputs[edge.fromScript];
     223            1 :         if (input == null || !names.has(String(input.name ?? ''))) continue;
     224            1 :         if (!affected.has(edge.toScript)) {
     225            1 :             affected.add(edge.toScript);
     226            1 :             pending.push(edge.toScript);
     227            1 :         }
     228            1 :     }
     229            1 :     while (pending.length > 0) {
     230            1 :         const script = pending.pop()!;
     231            1 :         for (const edge of dataflow.edges) {
     232            1 :             if (edge.kind === EdgeKind.INPUT || edge.kind === EdgeKind.INVALID_REFERENCE || edge.fromScript !== script) continue;
     233            1 :             if (!affected.has(edge.toScript)) {
     234            1 :                 affected.add(edge.toScript);
     235            1 :                 pending.push(edge.toScript);
     236            1 :             }
     237            1 :         }
     238            1 :     }
     239            1 :     return [...affected].filter(script => !dataflow.scripts[script]?.manual).sort((l, r) => l - r);
     240            1 : }
        

Generated by: LCOV version 2.0-1