Skip to content

Commit 0947ae5

Browse files
authored
feat: implement extend keyword (#47)
1 parent 373a2be commit 0947ae5

4 files changed

Lines changed: 121 additions & 63 deletions

File tree

kql/QueryBuilder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export function getQueryBuilder(): Knex.QueryBuilder {
2121
return _client.queryBuilder();
2222
}
2323

24-
export const raw = _client.raw;
24+
export const raw: Knex.RawBuilder = _client.raw;
2525

2626
export interface SQLResult {
2727
readonly sql: string;

kql/expressionHelper.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import * as kustoHelper from './kustoHelper';
2+
import { Syntax, SyntaxKind } from './kustoHelper';
3+
4+
type KindsFamilyMap = { [key: string]: boolean };
5+
6+
const containsExprFamilyKinds: KindsFamilyMap = {
7+
[SyntaxKind.ContainsExpression]: true,
8+
[SyntaxKind.ContainsCsExpression]: true,
9+
[SyntaxKind.NotContainsExpression]: true,
10+
[SyntaxKind.NotContainsCsExpression]: true,
11+
};
12+
13+
const supportedContainsExprFamilyKinds: KindsFamilyMap = {
14+
[SyntaxKind.ContainsExpression]: true,
15+
[SyntaxKind.NotContainsExpression]: true,
16+
}
17+
18+
const containsExprOpToSqlOp: { [key: string]: string } = {
19+
[SyntaxKind.ContainsExpression]: 'like',
20+
[SyntaxKind.NotContainsExpression]: 'not like',
21+
}
22+
23+
export interface whereRaw {
24+
sql: string;
25+
bindings?: readonly string[];
26+
}
27+
28+
function containsExpressionToWhereRaw(expr: Syntax.BinaryExpression): whereRaw {
29+
if (!supportedContainsExprFamilyKinds[expr.Kind]) {
30+
const op = kustoHelper.kqlToString(expr.Operator).toLowerCase();
31+
32+
throw new Error(`${op} expression not implemented`);
33+
}
34+
35+
const left = kustoHelper.kqlToString(expr.Left);
36+
// use getTokenValue to unquote string
37+
const right = kustoHelper.getTokenValue(expr.Right);
38+
const op = containsExprOpToSqlOp[expr.Kind];
39+
40+
return {
41+
sql: `?? ${op} ?`,
42+
bindings: [left, `%${right}%`],
43+
};
44+
}
45+
46+
export function expressionToWhereRaw(expr: Syntax.Expression): whereRaw {
47+
if (containsExprFamilyKinds[expr.Kind]) {
48+
return containsExpressionToWhereRaw(expr as Syntax.BinaryExpression);
49+
}
50+
51+
// defaults to convert the kql to sql literal
52+
const sql = kustoHelper.kqlToString(expr);
53+
return {
54+
sql,
55+
};
56+
}

kql/index.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,29 @@ describe('toSQL', () => {
117117
`,
118118
`with q0 as (select distinct x, y from source) select * from q0`,
119119
],
120+
[
121+
`
122+
source
123+
| project x = y contains 'foobar'
124+
`,
125+
`with q0 as (select y like '%foobar%' as x from source) select * from q0`,
126+
],
127+
[
128+
`
129+
source
130+
| extend x = y contains 'foobar'
131+
| project x
132+
`,
133+
`with q1 as (with q0 as (select y like '%foobar%' as x, * from source) select x from q0) select * from q1`,
134+
],
135+
[
136+
`
137+
source
138+
| extend x = 1 + y
139+
| project x, y
140+
`,
141+
`with q1 as (with q0 as (select 1 + y as x, * from source) select x, y from q0) select * from q1`,
142+
],
120143
].forEach((testCase, idx) => {
121144
const [kql, expectedSQL] = testCase;
122145

kql/index.ts

Lines changed: 41 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,9 @@
1+
import { expressionToWhereRaw } from './expressionHelper';
12
import * as kustoHelper from './kustoHelper';
23
import { Syntax, SyntaxKind } from './kustoHelper';
34
import { parsePatternsToRe2, PrimitiveTypeLong, PrimitiveTypeString, unescapeRegexPlaceholders } from './parseExpressionHelper';
45
import { DebugSQLOptions, getQueryBuilder, QueryContext, QueryInterface, raw, SQLResult } from "./QueryBuilder";
56

6-
function visitBinaryExpression(
7-
qc: QueryContext,
8-
qb: QueryInterface,
9-
v: Syntax.BinaryExpression,
10-
) {
11-
// TODO: recursive visit
12-
const left = kustoHelper.kqlToString(v.Left!);
13-
const right = kustoHelper.kqlToString(v.Right!);
14-
const op = kustoHelper.kqlToString(v.Operator!);
15-
16-
const raw = `${left} ${op} ${right}`;
17-
qb.whereRaw(raw);
18-
}
19-
20-
function visitContainsExpression(
21-
qc: QueryContext,
22-
qb: QueryInterface,
23-
v: Syntax.BinaryExpression,
24-
) {
25-
const left = kustoHelper.kqlToString(v.Left!);
26-
// use getTokenValue to unquote string
27-
const right = kustoHelper.getTokenValue(v.Right!);
28-
const op = kustoHelper.kqlToString(v.Operator!).toLowerCase();
29-
30-
switch (op) {
31-
case 'contains':
32-
qb.whereLike(left, `%${right}%`);
33-
break;
34-
case '!contains':
35-
qb.not.whereLike(left, `%${right}%`);
36-
break
37-
case 'contains_cs':
38-
throw new Error(`contains_cs not implemented`);
39-
case '!contains_cs':
40-
throw new Error(`!contains_cs not implemented`);
41-
}
42-
}
43-
447
function visitFilterOperator(
458
qc: QueryContext,
469
qb: QueryInterface,
@@ -50,26 +13,8 @@ function visitFilterOperator(
5013
throw new Error(`missing condition`);
5114
}
5215

53-
switch (v.Condition.Kind) {
54-
case SyntaxKind.AndExpression:
55-
case SyntaxKind.OrExpression:
56-
case SyntaxKind.GreaterThanExpression:
57-
case SyntaxKind.GreaterThanOrEqualExpression:
58-
case SyntaxKind.LessThanExpression:
59-
case SyntaxKind.LessThanOrEqualExpression:
60-
case SyntaxKind.NotEqualExpression:
61-
case SyntaxKind.EqualExpression:
62-
visitBinaryExpression(qc, qb, v.Condition as Syntax.BinaryExpression);
63-
break;
64-
case SyntaxKind.ContainsExpression:
65-
case SyntaxKind.ContainsCsExpression:
66-
case SyntaxKind.NotContainsExpression:
67-
case SyntaxKind.NotContainsCsExpression:
68-
visitContainsExpression(qc, qb, v.Condition as Syntax.BinaryExpression);
69-
break
70-
default:
71-
throw new Error(`unsupported condition type ${kustoHelper.getSyntaxKindName(v.Condition.Kind)}`);
72-
}
16+
const whereRaw = expressionToWhereRaw(v.Condition);
17+
qb.whereRaw(whereRaw.sql, whereRaw.bindings);
7318
}
7419

7520
function visitProjectOperator_SeparatedElement(
@@ -91,17 +36,17 @@ function visitProjectOperator_SeparatedElement(
9136
// case: project a = foo
9237
{
9338
const projectAsExpr = (exprChild as Syntax.SimpleNamedExpression);
39+
const projectSource = expressionToWhereRaw(projectAsExpr.Expression);
9440
const projectAsName = kustoHelper.kqlToString(projectAsExpr.Name);
95-
const projectSource = kustoHelper.kqlToString(projectAsExpr.Expression);
96-
qb.select(raw(`${projectSource} as ${projectAsName}`));
41+
qb.select(raw(`${projectSource.sql} as ${projectAsName}`, projectSource.bindings));
9742
}
9843
break;
9944
default:
10045
// case: project foo + 10
10146
{
102-
const projectSource = kustoHelper.kqlToString(exprChild);
47+
const projectSource = expressionToWhereRaw(exprChild as Syntax.Expression);
10348
const projectAsName = qc.acquireAutoProjectAsName();
104-
qb.select(raw(`${projectSource} as ${projectAsName}`));
49+
qb.select(raw(`${projectSource.sql} as ${projectAsName}`, projectSource.bindings));
10550
}
10651
}
10752

@@ -244,6 +189,38 @@ function visitDistinctOperator(
244189
return qc.wrapAsCTE(qb);
245190
}
246191

192+
function visitExtendOperator(
193+
qc: QueryContext,
194+
qb: QueryInterface,
195+
v: Syntax.ExtendOperator,
196+
): QueryInterface {
197+
if (!v.Expressions) {
198+
return qb;
199+
}
200+
201+
kustoHelper.visitChild(
202+
v.Expressions,
203+
(child) => {
204+
if (child.Kind !== SyntaxKind.SeparatedElement) {
205+
// unexpected
206+
return;
207+
}
208+
209+
visitProjectOperator_SeparatedElement(
210+
qc,
211+
qb,
212+
child as Syntax.SeparatedElement,
213+
);
214+
}
215+
);
216+
217+
// select all fields from CTE, but after extended columns as extended columns
218+
// take higher precedence.
219+
qb.select('*');
220+
221+
return qc.wrapAsCTE(qb);
222+
}
223+
247224
function visitQueryBlock(
248225
qc: QueryContext,
249226
qb: QueryInterface,
@@ -312,6 +289,8 @@ function visit(
312289
return visitCountOperator(qc, qb, v as Syntax.CountOperator);
313290
case SyntaxKind.DistinctOperator:
314291
return visitDistinctOperator(qc, qb, v as Syntax.DistinctOperator);
292+
case SyntaxKind.ExtendOperator:
293+
return visitExtendOperator(qc, qb, v as Syntax.ExtendOperator);
315294
default:
316295
qc.logUnknown(`unhandled kind: ${kustoHelper.getSyntaxKindName(v.Kind)}`);
317296
return qb;

0 commit comments

Comments
 (0)