Line data Source code
1 : import * as dashql from '@ankoh/dashql-core';
2 :
3 : import type { ClientFunctionCallDescription } from '../../connections/clfn/clfn_spec.js';
4 : import { CancelQuery, QueryExecutor } from '../../connections/query_executor.js';
5 : import type { DashQLShellEnvironment } from '../../shell/api.js';
6 : import { reportShellTiming, SHELL_QUERY_METADATA } from '../../shell/shell_result.js';
7 :
8 : /// The client function call of a shell statement, null if the statement goes to the backend. A spec shadows its
9 : /// namespace and no backend judges a call in it, so a call that can't run fails here, like it fails a session plan.
10 1 : export function resolveClientFunctionCall(catalog: dashql.DashQLCatalog, query: string): ClientFunctionCallDescription | null {
11 1 : const script = catalog.ptr.api.createScript(catalog);
12 1 : try {
13 1 : script.replaceText(query);
14 1 : script.analyze();
15 1 : if (!script.startsClientFunctionCall()) return null;
16 1 : const parsed = script.getParsed().read();
17 1 : const syntaxError = parsed.scannerErrorsLength() > 0 ? parsed.scannerErrors(0) : parsed.parserErrorsLength() > 0 ? parsed.parserErrors(0) : null;
18 1 : if (syntaxError != null) throw new Error(syntaxError.message() ?? 'Invalid client function call');
19 1 : if (parsed.statementsLength() > 1) throw new Error('A client function call runs as a statement of its own');
20 1 : const analyzed = script.getAnalyzed().read();
21 1 : for (let i = 0; i < analyzed.errorsLength(); ++i) {
22 1 : const error = analyzed.errors(i)!;
23 1 : if (error.severity() === dashql.buffers.analyzer.AnalyzerErrorSeverity.ERROR) throw new Error(error.message() ?? 'Invalid client function call');
24 1 : }
25 1 : const description = JSON.parse(analyzed.clientFunctionCalls(0)!.description()!) as ClientFunctionCallDescription;
26 : // The inputs of a session belong to its scripts, the shell has no values for them
27 1 : const binding = description.bindings?.[0];
28 1 : if (binding != null) throw new Error(`input :${binding.param} has no value`);
29 1 : return description;
30 1 : } finally {
31 1 : script.destroy();
32 1 : }
33 1 : }
34 :
35 1 : export function createSessionShellEnvironment(
36 1 : sessionId: string,
37 1 : connectionId: string,
38 1 : executeQuery: QueryExecutor,
39 1 : cancelQuery: CancelQuery,
40 1 : releaseResult: (queryId: number) => void = () => { },
41 1 : resolveCall: (query: string) => ClientFunctionCallDescription | null = () => null,
42 1 : ): DashQLShellEnvironment {
43 1 : return {
44 1 : async executeQuery(query, signal, onProgress, onResult, onTiming) {
45 1 : const reportTiming = (label: string, elapsedMs: number) => reportShellTiming(onTiming, label, elapsedMs);
46 1 : const clientFunctionCall = resolveCall(query) ?? undefined;
47 1 : const [queryId, execution] = executeQuery(connectionId, {
48 1 : query,
49 1 : clientFunctionCall,
50 1 : analyzeResults: true,
51 1 : cacheable: false,
52 1 : throwOnError: true,
53 1 : onLog: onProgress,
54 1 : onTiming: reportTiming,
55 1 : metadata: { ...SHELL_QUERY_METADATA, sessionId },
56 1 : });
57 1 : const abort = () => cancelQuery(connectionId, queryId);
58 1 : if (signal?.aborted) {
59 0 : abort();
60 : } else {
61 1 : signal?.addEventListener('abort', abort, { once: true });
62 : }
63 1 : try {
64 1 : const table = await execution;
65 1 : if (table == null) {
66 0 : if (signal?.aborted) throw new DOMException('Query was cancelled', 'AbortError');
67 0 : throw new Error('Query failed without an error');
68 0 : }
69 1 : if (table.numCols > 0) {
70 1 : onResult?.(queryId, table.numRows);
71 : } else {
72 : // A statement without columns has nothing to show, its computation is not needed
73 1 : releaseResult(queryId);
74 : }
75 1 : } finally {
76 1 : signal?.removeEventListener('abort', abort);
77 1 : }
78 1 : },
79 1 : };
80 1 : }
|