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 : }
|