Line data Source code
1 : import * as dashql from '@ankoh/dashql-core';
2 :
3 : import { formatSqlText } from '../../session/scripts/script_format.js';
4 :
5 : /// The name of the common table expression that holds the query of the script
6 1 : const PROJECTION_INPUT = 'base';
7 :
8 : /// Why a script cannot get the projection of its result
9 1 : export enum ResultProjectionError {
10 : /// The script does not parse or has no statement
11 1 : NotParsed = 'not_parsed',
12 : /// The last statement of the script is no query
13 1 : NoQuery = 'no_query',
14 : /// The result has no columns
15 1 : NoColumns = 'no_columns',
16 : /// Two columns of the result have the same name, a projection cannot tell them apart
17 1 : DuplicateColumns = 'duplicate_columns',
18 1 : }
19 :
20 : /// Wrap the terminal query of a script, its last statement, into a common table expression and select every
21 : /// column of its result below it: `with base as (<query>) select a, b from base`. A column is excluded by
22 : /// deleting its line. The new statement is formatted with the pretty printer, the rest of the script stays.
23 : /// Returns the new text of the script.
24 1 : export function addResultProjection(
25 1 : core: dashql.DashQL,
26 1 : scriptText: string,
27 1 : columnNames: string[],
28 1 : config: dashql.buffers.formatting.FormattingConfigT,
29 1 : ): string | ResultProjectionError {
30 1 : if (columnNames.length === 0) return ResultProjectionError.NoColumns;
31 1 : if (new Set(columnNames).size !== columnNames.length) return ResultProjectionError.DuplicateColumns;
32 :
33 : // The span of the terminal query, in UTF-8 bytes
34 1 : const bytes = new TextEncoder().encode(scriptText);
35 1 : const catalog = core.createCatalog();
36 1 : const script = core.createScript(catalog);
37 1 : let span: { offset: number; length: number } | null = null;
38 1 : try {
39 1 : script.replaceText(scriptText);
40 1 : script.parse();
41 1 : const reader = script.getParsed().read();
42 1 : if (reader.scannerErrorsLength() > 0 || reader.parserErrorsLength() > 0 || reader.statementsLength() === 0) {
43 1 : return ResultProjectionError.NotParsed;
44 : }
45 1 : const statement = reader.statements(reader.statementsLength() - 1)!;
46 1 : if (statement.statementType() !== dashql.buffers.parser.StatementType.SELECT) {
47 1 : return ResultProjectionError.NoQuery;
48 : }
49 1 : const statementSpan = statement.statementSpan();
50 1 : if (statementSpan == null) return ResultProjectionError.NotParsed;
51 1 : span = { offset: statementSpan.offset(), length: statementSpan.length() };
52 1 : } finally {
53 1 : script.destroy();
54 1 : catalog.destroy();
55 1 : }
56 :
57 1 : const decoder = new TextDecoder();
58 1 : const before = decoder.decode(bytes.subarray(0, span.offset));
59 1 : const query = decoder.decode(bytes.subarray(span.offset, span.offset + span.length));
60 1 : const after = decoder.decode(bytes.subarray(span.offset + span.length));
61 :
62 1 : const input = core.quoteColumnName(PROJECTION_INPUT);
63 1 : const columns = columnNames.map(name => core.quoteColumnName(name));
64 1 : const projection = `with ${input} as (${query}) select ${columns.join(', ')} from ${input};`;
65 : // The separator of the statement stays in the text after it
66 1 : const formatted = formatSqlText(core, projection, config)?.trimEnd().replace(/;$/, '')
67 0 : ?? layoutProjection(input, query, columns);
68 1 : return `${before}${formatted}${after}`;
69 1 : }
70 :
71 : /// The projection when the formatter cannot format the query, the query keeps its text
72 0 : function layoutProjection(input: string, query: string, columns: string[]): string {
73 0 : return `with ${input} as (
74 0 : ${query.split('\n').join('\n ')}
75 : )
76 : select
77 0 : ${columns.join(',\n ')}
78 0 : from ${input}`;
79 0 : }
|