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