LCOV - code coverage report
Current view: top level - dashql-app/src/session/inputs - session_inputs.ts Coverage Total Hit
Test: DashQL 915828b Lines: 83.6 % 146 122
Test Date: 2026-11-06 19:30:32

            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 :         })),
      79            1 :         scripts: scripts.map((script, index) => ({
      80            1 :             name: scriptDisplayName(files[index]),
      81            1 :             connection: connectionIds.indexOf(script.connection.connectionId),
      82            1 :             session: script.scriptSession,
      83            1 :         })),
      84            1 :         attachments: planAttachments(session?.attachments),
      85            1 :         attachmentPrefix: session == null ? undefined : attachmentPrefix(session.sessionId),
      86            1 :     };
      87            1 : }
      88              : 
      89              : /// The type of an input of the core
      90            1 : export function readSessionInputType(type: core.buffers.analyzer.SqlTypeT | null, source: core.buffers.execution.SessionInputTypeSource): SessionInputType {
      91            1 :     return {
      92            1 :         kind: type?.kind ?? SqlTypeKind.TEXT,
      93            1 :         sql: String(type?.sql ?? 'text'),
      94            1 :         arrayDimensions: type?.arrayDimensions ?? 0,
      95            1 :         withTimeZone: type?.withTimeZone ?? false,
      96            1 :         cast: source === core.buffers.execution.SessionInputTypeSource.CAST,
      97            1 :         known: source !== core.buffers.execution.SessionInputTypeSource.NONE,
      98            1 :     };
      99            1 : }
     100              : 
     101              : /// Collect the inputs of a session with the core
     102            1 : function collectSessionInputs(session: SessionScripts): SessionInput[] {
     103            1 :     const scripts = orderedSessionScripts(session);
     104            1 :     if (scripts.length === 0 || typeof session.instance?.collectSessionInputs !== 'function') return NO_INPUTS;
     105            1 :     const request = sessionPlanRequest(scripts, scripts.map(s => s.fileName), session);
     106            1 :     const collected = session.instance.collectSessionInputs(request).unpack() as core.buffers.execution.SessionInputsT;
     107            1 :     return collected.inputs.map(input => ({
     108            0 :         name: String(input.name ?? ''),
     109            0 :         type: readSessionInputType(input.sqlType, input.typeSource),
     110            0 :         value: input.hasValue ? String(input.value ?? '') : null,
     111            0 :         literal: input.hasValue ? String(input.literal ?? '') : null,
     112            0 :         consumers: input.consumers.flatMap(c => {
     113            0 :             const script = scripts[c.script];
     114            0 :             return script == null ? [] : [{ scriptKey: script.scriptKey, statementId: c.statement }];
     115            0 :         }),
     116            0 :         warnings: input.warnings.map(String),
     117            1 :     }));
     118            1 : }
     119              : 
     120              : /// The scripts that use an input, without duplicates
     121            0 : export function inputConsumerScripts(input: SessionInput): number[] {
     122            0 :     return [...new Set(input.consumers.map(c => c.scriptKey))];
     123            0 : }
     124              : 
     125              : interface CachedInputs {
     126              :     signature: string;
     127              :     inputs: SessionInput[];
     128              : }
     129              : 
     130              : /// The last inputs of every session, by session id
     131            1 : const INPUTS = new Map<string, CachedInputs>();
     132              : 
     133            1 : function sameInputs(l: SessionInput[], r: SessionInput[]): boolean {
     134            1 :     return JSON.stringify(l) === JSON.stringify(r);
     135            1 : }
     136              : 
     137              : /// The inputs of a session, collected again when a script or its analysis changed. A script that is being edited
     138              : /// keeps the inputs of its last analysis: an input stays while its analysis lags behind its text.
     139            1 : export function deriveSessionInputs(session: SessionScripts): SessionInput[] {
     140              :     // Sessions without the core, e.g. of tests, have no inputs
     141            1 :     if (session.scriptOrder == null || session.scriptOrder.length === 0 || typeof session.instance?.collectSessionInputs !== 'function') return NO_INPUTS;
     142            1 :     const signature = sessionAnalysisSignature(session);
     143            1 :     const cached = INPUTS.get(session.sessionId);
     144            1 :     if (cached?.signature === signature) return cached.inputs;
     145            1 :     let inputs: SessionInput[];
     146            1 :     try {
     147            1 :         inputs = collectSessionInputs(session);
     148            1 :     } catch (e) {
     149            1 :         console.warn('cannot collect the inputs of the session', e);
     150            1 :         return cached?.inputs ?? NO_INPUTS;
     151            1 :     }
     152            1 :     if (cached != null && sameInputs(cached.inputs, inputs)) inputs = cached.inputs;
     153            1 :     INPUTS.set(session.sessionId, { signature, inputs });
     154            1 :     return inputs;
     155            1 : }
     156              : 
     157              : /// The control of the value of an input
     158            1 : export function sessionInputControl(type: SessionInputType): SessionInputControl {
     159            1 :     if (type.arrayDimensions > 0) return SessionInputControl.Text;
     160            1 :     switch (type.kind) {
     161            0 :         case SqlTypeKind.INT1:
     162            0 :         case SqlTypeKind.INT2:
     163            1 :         case SqlTypeKind.INT4:
     164            1 :         case SqlTypeKind.INT8:
     165            1 :             return type.cast ? SessionInputControl.Integer : SessionInputControl.Number;
     166            1 :         case SqlTypeKind.INTERVAL:
     167            1 :             return SessionInputControl.Interval;
     168            0 :         case SqlTypeKind.FLOAT4:
     169            0 :         case SqlTypeKind.FLOAT8:
     170            1 :         case SqlTypeKind.NUMERIC:
     171            1 :             return SessionInputControl.Number;
     172            1 :         case SqlTypeKind.BOOL:
     173            1 :             return SessionInputControl.Boolean;
     174            1 :         case SqlTypeKind.DATE:
     175            1 :             return SessionInputControl.Date;
     176            1 :         case SqlTypeKind.TIMESTAMP:
     177            1 :             return SessionInputControl.Timestamp;
     178            1 :         default:
     179            1 :             return SessionInputControl.Text;
     180            1 :     }
     181            1 : }
     182              : 
     183              : /// The literal of the value of a control in the Hyper dialect, the value of the `@param` tags, or the problem with the
     184              : /// value. The type of the input decides what the value is (`SqlLiteral::from_value` of the core), an input whose type
     185              : /// is unknown reads a number, `true`, `false` or a string.
     186            0 : export function sessionInputLiteral(instance: core.DashQL, type: SessionInputType, value: string): { literal: string } | { error: string } {
     187            0 :     if (value.trim().length === 0) return { error: 'The input has no value' };
     188            0 :     try {
     189            0 :         return { literal: instance.inputLiteral(value, type.known ? type.sql : null) };
     190            0 :     } catch (e: any) {
     191            0 :         return { error: String(e?.message ?? e) };
     192            0 :     }
     193            0 : }
     194              : 
     195              : /// The scripts that an input change reruns: the scripts that use one of the inputs and every script downstream of
     196              : /// them, by the references and relation edges of the dataflow. A script that runs only on request (`@manual`) is left
     197              : /// out, the scripts downstream of it are not. Indexes the scripts of the dataflow.
     198            1 : export function affectedScriptIndices(dataflow: core.buffers.execution.SessionDataflowT, inputs: readonly string[]): number[] {
     199            1 :     const names = new Set(inputs);
     200            1 :     const affected = new Set<number>();
     201            1 :     const pending: number[] = [];
     202            1 :     for (const edge of dataflow.edges) {
     203            1 :         if (edge.kind !== EdgeKind.INPUT) continue;
     204            1 :         const input = dataflow.inputs[edge.fromScript];
     205            1 :         if (input == null || !names.has(String(input.name ?? ''))) continue;
     206            1 :         if (!affected.has(edge.toScript)) {
     207            1 :             affected.add(edge.toScript);
     208            1 :             pending.push(edge.toScript);
     209            1 :         }
     210            1 :     }
     211            1 :     while (pending.length > 0) {
     212            1 :         const script = pending.pop()!;
     213            1 :         for (const edge of dataflow.edges) {
     214            1 :             if (edge.kind === EdgeKind.INPUT || edge.kind === EdgeKind.INVALID_REFERENCE || edge.fromScript !== script) continue;
     215            1 :             if (!affected.has(edge.toScript)) {
     216            1 :                 affected.add(edge.toScript);
     217            1 :                 pending.push(edge.toScript);
     218            1 :             }
     219            1 :         }
     220            1 :     }
     221            1 :     return [...affected].filter(script => !dataflow.scripts[script]?.manual).sort((l, r) => l - r);
     222            1 : }
        

Generated by: LCOV version 2.0-1