@narrative.io/data-collaboration-sdk-ts 0.0.122 → 0.0.124

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -93,7 +93,7 @@ function parseFieldFromString(fieldStr) {
93
93
  },
94
94
  field: f
95
95
  .map((field) => {
96
- return field.replace('"value"', "value").replace('"type"', "type");
96
+ return field.replace("`value`", "value").replace("`type`", "type");
97
97
  })
98
98
  .join("."),
99
99
  };
@@ -0,0 +1,8 @@
1
+ import { type NqlBudget } from "./Ast";
2
+ import { type Select, type Expression, type Raw, type Table } from "./AstParser";
3
+ type CompiledNql = string;
4
+ export declare function compileSelect(q: Select): CompiledNql;
5
+ export declare function compileBudget(budget: NqlBudget): CompiledNql;
6
+ export declare function compileFrom(tables: Table[] | Raw): CompiledNql;
7
+ export declare function compileExpression(n: Expression): CompiledNql;
8
+ export {};
@@ -0,0 +1,297 @@
1
+ const quoteChar = '"';
2
+ export function compileSelect(q) {
3
+ const columns = q.columns.map((e) => compileExpression(e));
4
+ const select = `SELECT ${columns.join(", ")}`;
5
+ const budget = q.budget !== null ? compileBudget(q.budget) : "";
6
+ const from = `FROM ${compileFrom(q.from)}`;
7
+ const where = q.where !== null ? `WHERE ${compileExpression(q.where)}` : "";
8
+ return `${select} ${from} ${where} ${budget}`;
9
+ }
10
+ export function compileBudget(budget) {
11
+ let period;
12
+ switch (budget.period.type) {
13
+ case "calendar_daily": {
14
+ period = "PER CALENDAR DAY";
15
+ break;
16
+ }
17
+ case "calendar_monthly": {
18
+ period = "PER CALENDAR MONTH";
19
+ break;
20
+ }
21
+ default:
22
+ unreachable(budget.period.type);
23
+ }
24
+ // eslint-disable-next-line prettier/prettier
25
+ return `LIMIT ${budget.amount.value} ${budget.amount.currency.toUpperCase()} ${period}`;
26
+ }
27
+ export function compileFrom(tables) {
28
+ if (isArray(tables)) {
29
+ return compileTableRefs(tables);
30
+ }
31
+ else {
32
+ return compileRawRef(tables.nql, tables.as);
33
+ }
34
+ }
35
+ function compileTableRefs(tables) {
36
+ function ref(table) {
37
+ switch (table.type) {
38
+ case "dataset":
39
+ return aliased(`company_data.${quote(table.datasetId.toString())}`, table.as, false);
40
+ case "rosetta_stone":
41
+ return aliased(`narrative.rosetta_stone`, table.as, false);
42
+ case "raw_table":
43
+ return aliased(table.nql, table.as, false);
44
+ }
45
+ }
46
+ function compileTableRef(table) {
47
+ let stmt = "";
48
+ // NB: by convention the first table in list will never have a join associated with it,
49
+ // hence the leading ' ' below does what we want.
50
+ if (table.join !== undefined) {
51
+ switch (table.join.joinType) {
52
+ case "comma": {
53
+ stmt += ",";
54
+ break;
55
+ }
56
+ case "cross": {
57
+ stmt += " CROSS JOIN";
58
+ break;
59
+ }
60
+ case "full": {
61
+ stmt += " FULL OUTER JOIN";
62
+ break;
63
+ }
64
+ case "inner": {
65
+ stmt += " JOIN";
66
+ break;
67
+ }
68
+ case "left": {
69
+ stmt += " LEFT OUTER JOIN";
70
+ break;
71
+ }
72
+ case "left_semi": {
73
+ stmt += " LEFT SEMI JOIN";
74
+ break;
75
+ }
76
+ case "right": {
77
+ stmt += " RIGHT OUTER JOIN";
78
+ break;
79
+ }
80
+ default:
81
+ unreachable(table.join.joinType);
82
+ }
83
+ stmt += ` ${ref(table)}`;
84
+ }
85
+ else {
86
+ stmt += ref(table);
87
+ }
88
+ if (
89
+ // Prefer explicit `join !== undefined` to the use of the `?` operator so that typescript
90
+ // compile knows that the join is defined in the subsequent lexical scope.
91
+ // eslint-disable-next-line @typescript-eslint/prefer-optional-chain
92
+ table.join !== undefined &&
93
+ table.join.condition !== null &&
94
+ table.join.conditionType !== "none") {
95
+ switch (table.join.conditionType) {
96
+ case "on": {
97
+ stmt += " ON";
98
+ break;
99
+ }
100
+ case "using": {
101
+ stmt += " USING";
102
+ break;
103
+ }
104
+ default:
105
+ unreachable(table.join.conditionType);
106
+ }
107
+ stmt += ` ${compileExpression(table.join.condition)}`;
108
+ }
109
+ return stmt;
110
+ }
111
+ let nql = "";
112
+ for (const table of tables) {
113
+ const stmt = compileTableRef(table);
114
+ nql += stmt;
115
+ }
116
+ return nql;
117
+ }
118
+ export function compileExpression(n) {
119
+ switch (n.type) {
120
+ case "=":
121
+ return compileBinaryOperator("=", [n.left, n.right], n.as);
122
+ case "<>":
123
+ return compileBinaryOperator("<>", [n.left, n.right], n.as);
124
+ case "<":
125
+ return compileBinaryOperator("<", [n.left, n.right], n.as);
126
+ case "<=":
127
+ return compileBinaryOperator("<=", [n.left, n.right], n.as);
128
+ case ">":
129
+ return compileBinaryOperator("<=", [n.left, n.right], n.as);
130
+ case ">=":
131
+ return compileBinaryOperator("<=", [n.left, n.right], n.as);
132
+ case "+":
133
+ return compileBinaryOperator("+", n.operands, n.as);
134
+ case "/":
135
+ return compileBinaryOperator("/", n.operands, n.as);
136
+ case "*":
137
+ return compileBinaryOperator("*", n.operands, n.as);
138
+ case "-":
139
+ return compileBinaryOperator("-", n.operands, n.as);
140
+ case "||":
141
+ return compileBinaryOperator("||", n.operands, n.as);
142
+ case "and":
143
+ return compileBinaryOperator("AND", n.operands, n.as);
144
+ case "attribute_ref":
145
+ return compileAttributeRef(n.column, n.as);
146
+ case "between":
147
+ return compileBetween(n.operand, n.lower, n.upper, n.as);
148
+ case "dataset_column_ref":
149
+ return compileDatasetColumnRef(n.datasetId, n.column, n.as);
150
+ case "function":
151
+ return compileFunctionCall(n.name, n.args, n.as);
152
+ case "in":
153
+ return compileIn(n.expression, n.values, n.negated, n.as);
154
+ case "is_null":
155
+ return compileIsNull(n.operand, n.negated, n.as);
156
+ case "like":
157
+ return compileLike(n.value, n.pattern, n.negated, n.as);
158
+ case "lit":
159
+ return compileLit(n.value, n.value_type, n.as);
160
+ case "not":
161
+ return compileNot(n.operand, n.as);
162
+ case "or":
163
+ return compileBinaryOperator("OR", n.operands, n.as);
164
+ case "nql":
165
+ return raw(n);
166
+ case "raw_ref":
167
+ return compileRawRef(n.nql, n.as);
168
+ case "unary_-":
169
+ return compileUnaryMinus(n.operand, n.as);
170
+ }
171
+ return unreachable(n);
172
+ }
173
+ function compileAttributeRef(column, as) {
174
+ return aliased(`narrative.rosetta_stone.${quote(column)}`, as);
175
+ }
176
+ function compileBetween(operand, lower, upper, as) {
177
+ const operandNql = compileExpression(operand);
178
+ const lowerNql = compileExpression(lower);
179
+ const upperNql = compileExpression(upper);
180
+ return aliased(`${operandNql} BETWEEN ASYMMETRIC ${lowerNql} AND ${upperNql}`, as);
181
+ }
182
+ function compileBinaryOperator(op, operands, as) {
183
+ return aliased(`(${operands.map((e) => compileExpression(e)).join(` ${op} `)})`, as);
184
+ }
185
+ function compileDatasetColumnRef(datasetId, column, as) {
186
+ return aliased(`company_data.${quote(datasetId.toString())}.${quote(column)}`, as);
187
+ }
188
+ function compileFunctionCall(name, args, as) {
189
+ // Calling convenions for functions are all over the place:
190
+ // - CAST is invoked as CAST($value AS $type)
191
+ // - EXTRACT is invoked as EXTRACT($part FROM $value)
192
+ // - etc., etc., with many exceptions
193
+ // We'll likely wanto types for all the functions we know how to support, or have every function
194
+ // described by the API, including providing a template for its calling convention. For now the
195
+ // below will get us a to a v1.
196
+ // Useful reference: https://calcite.apache.org/docs/reference.html#arithmetic-operators-and-functions
197
+ const argsNql = args.map((e) => compileExpression(e));
198
+ let callNql;
199
+ switch (name) {
200
+ case "CAST":
201
+ callNql = `CAST(${argsNql[0]} AS ${argsNql[1]})`;
202
+ break;
203
+ case "EXTRACT":
204
+ callNql = `EXTRACT(${argsNql[0]} FROM ${argsNql[1]})`;
205
+ break;
206
+ case "TRIM":
207
+ callNql = `TRIM(${argsNql[0]} ${argsNql[1]} FROM ${argsNql[2]})`;
208
+ break;
209
+ default: {
210
+ // hope for the best. definitely does not work in all cases.
211
+ callNql =
212
+ argsNql.length > 0 ? `${name}(${argsNql.join(", ")})` : `${name}`;
213
+ }
214
+ }
215
+ return aliased(callNql, as);
216
+ }
217
+ function compileIn(expression, values, negated, as) {
218
+ const op = !negated ? "IN" : "NOT IN";
219
+ const compiledValues = isArray(values)
220
+ ? `(${values.map((e) => compileExpression(e)).join(", ")})`
221
+ : raw(values);
222
+ return aliased(`${compileExpression(expression)} ${op} ${compiledValues}`, as);
223
+ }
224
+ function compileIsNull(operand, negated, as) {
225
+ const op = !negated ? "IS NULL" : "IS NOT NULL";
226
+ return compilePostfixUnaryOperator(op, operand, as);
227
+ }
228
+ function compileLike(value, pattern, negated, as) {
229
+ const op = !negated ? "LIKE" : "NOT LIKE";
230
+ return aliased(`${compileExpression(value)} ${op} ${str(pattern)}`, as);
231
+ }
232
+ function compileLit(value, valueType, as) {
233
+ let valueNql;
234
+ if (value !== null) {
235
+ switch (valueType.type) {
236
+ case "bool":
237
+ valueNql = str(value.toUpperCase());
238
+ break;
239
+ case "interval": {
240
+ if (valueType.period.includes("_")) {
241
+ // split e.g. "year_month" into [YEAR, MONTH]
242
+ const parts = valueType.period.split("_").map((s) => s.toUpperCase());
243
+ valueNql = `INTERVAL ${str(value)} ${parts[0]} TO ${parts[1]}`;
244
+ }
245
+ else {
246
+ valueNql = `INTERVAL ${str(value)} ${valueType.period.toUpperCase()}`;
247
+ }
248
+ break;
249
+ }
250
+ default:
251
+ // todo(mbabic) don't really know what to do here/how complicated this needs to be
252
+ valueNql = str(value);
253
+ }
254
+ }
255
+ else {
256
+ valueNql = "NULL";
257
+ }
258
+ return aliased(valueNql, as);
259
+ }
260
+ function compileNot(operand, as) {
261
+ return aliased(`NOT ${compileExpression(operand)}`, as);
262
+ }
263
+ function compilePostfixUnaryOperator(op, operand, as) {
264
+ return aliased(`(${compileExpression(operand)}) ${op}`, as);
265
+ }
266
+ function compileRawRef(nql, as) {
267
+ return aliased(nql, as);
268
+ }
269
+ function compileUnaryMinus(operand, as) {
270
+ return aliased(`-(${compileExpression(operand)})`, as);
271
+ }
272
+ function aliased(nql, as, parens) {
273
+ const alias = as !== undefined ? `${nql} AS ${quote(as)}` : nql;
274
+ if ((parens === undefined || parens) && as !== undefined) {
275
+ return `(${alias})`;
276
+ }
277
+ else {
278
+ return alias;
279
+ }
280
+ }
281
+ function str(s) {
282
+ // todo(mbabic) is this the right escaping for calcite?
283
+ return `'${s.replaceAll("'", "''")}'`;
284
+ }
285
+ function quote(s) {
286
+ return `${quoteChar}${s.replace(quoteChar, `\\${quoteChar}`)}${quoteChar}`;
287
+ }
288
+ function raw(n) {
289
+ return aliased(n.nql, n.as);
290
+ }
291
+ function isArray(ns) {
292
+ return Array.isArray(ns);
293
+ }
294
+ // typescript trick to help with checking exhaustivity of swith statements
295
+ function unreachable(x) {
296
+ throw new Error("unreachable");
297
+ }
@@ -1,5 +1,6 @@
1
1
  import { BaseApi } from "../base-api";
2
2
  import type { Plan } from "./SubstraitParser";
3
+ import { type NqlAst } from "./Ast";
3
4
  import type { Nql, NqlField, NqlResult, NqlCompileResult, NqlQueryInput, NqlExpression, NqlWhere, NqlFilterBinaryExpression, NqlBooleanExpression, NqlFilterExpression, NqlFilterUnaryExpression } from "./types";
4
5
  /**
5
6
  * A class for accessing the NQL API.
@@ -27,6 +28,14 @@ declare class NqlApi extends BaseApi {
27
28
  * @returns {Promise<NqlCompileResult>} A promise that resolves with the result of the NQL compilation.
28
29
  */
29
30
  parseNql(nqlQuery: NqlQueryInput): Promise<Plan>;
31
+ /**
32
+ * Validates an NQL query and returns an abstract syntax tree (AST) representing a structured form of
33
+ * the query.
34
+ *
35
+ * @param {string} nqlQuery - The NQL query to validate.
36
+ * @returns {Promise<NqlCompileResult>} A promise that resolves with the result of the NQL validation.
37
+ */
38
+ validateNql(nqlQuery: NqlQueryInput): Promise<NqlAst>;
30
39
  /**
31
40
  * Gets an existing NQL query by it's id
32
41
  *
@@ -35,4 +44,4 @@ declare class NqlApi extends BaseApi {
35
44
  */
36
45
  getNqlByJobId(id: string): Promise<NqlResult>;
37
46
  }
38
- export { NqlApi, type Nql, type NqlField, type NqlResult, type NqlQueryInput, type NqlCompileResult, type NqlExpression, type NqlWhere, type NqlFilterExpression, type NqlBooleanExpression, type NqlFilterBinaryExpression, type NqlFilterUnaryExpression, };
47
+ export { NqlApi, type Nql, type NqlAst, type NqlField, type NqlResult, type NqlQueryInput, type NqlCompileResult, type NqlExpression, type NqlWhere, type NqlFilterExpression, type NqlBooleanExpression, type NqlFilterBinaryExpression, type NqlFilterUnaryExpression, };
@@ -42,6 +42,17 @@ class NqlApi extends BaseApi {
42
42
  const response = await this.post(`${resourceName}/parse`, nqlQuery);
43
43
  return response;
44
44
  }
45
+ /**
46
+ * Validates an NQL query and returns an abstract syntax tree (AST) representing a structured form of
47
+ * the query.
48
+ *
49
+ * @param {string} nqlQuery - The NQL query to validate.
50
+ * @returns {Promise<NqlCompileResult>} A promise that resolves with the result of the NQL validation.
51
+ */
52
+ async validateNql(nqlQuery) {
53
+ const response = await this.post(`${resourceName}/validate`, nqlQuery);
54
+ return response;
55
+ }
45
56
  /**
46
57
  * Gets an existing NQL query by it's id
47
58
  *