@webda/ql 4.0.0-beta.6 → 4.0.0-beta.8
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.
- package/README.md +42 -4
- package/lib/WebdaQLLexer.d.ts +10 -9
- package/lib/WebdaQLLexer.d.ts.map +1 -1
- package/lib/WebdaQLLexer.js +227 -363
- package/lib/WebdaQLLexer.js.map +1 -1
- package/lib/WebdaQLParserLexer.d.ts +10 -9
- package/lib/WebdaQLParserLexer.d.ts.map +1 -1
- package/lib/WebdaQLParserLexer.js +227 -363
- package/lib/WebdaQLParserLexer.js.map +1 -1
- package/lib/WebdaQLParserListener.d.ts +26 -2
- package/lib/WebdaQLParserListener.d.ts.map +1 -1
- package/lib/WebdaQLParserParser.d.ts +34 -9
- package/lib/WebdaQLParserParser.d.ts.map +1 -1
- package/lib/WebdaQLParserParser.js +585 -732
- package/lib/WebdaQLParserParser.js.map +1 -1
- package/lib/WebdaQLParserVisitor.d.ts +16 -1
- package/lib/WebdaQLParserVisitor.d.ts.map +1 -1
- package/lib/aggregation-select.spec.js +4 -3
- package/lib/aggregation-select.spec.js.map +1 -1
- package/lib/completion.d.ts +84 -0
- package/lib/completion.d.ts.map +1 -0
- package/lib/completion.js +467 -0
- package/lib/completion.js.map +1 -0
- package/lib/completion.spec.d.ts +25 -0
- package/lib/completion.spec.d.ts.map +1 -0
- package/lib/completion.spec.js +409 -0
- package/lib/completion.spec.js.map +1 -0
- package/lib/filter.d.ts +54 -0
- package/lib/filter.d.ts.map +1 -0
- package/lib/filter.js +135 -0
- package/lib/filter.js.map +1 -0
- package/lib/filter.spec.d.ts +2 -0
- package/lib/filter.spec.d.ts.map +1 -0
- package/lib/filter.spec.js +124 -0
- package/lib/filter.spec.js.map +1 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -1
- package/lib/now.spec.d.ts +2 -0
- package/lib/now.spec.d.ts.map +1 -0
- package/lib/now.spec.js +258 -0
- package/lib/now.spec.js.map +1 -0
- package/lib/query.d.ts +134 -8
- package/lib/query.d.ts.map +1 -1
- package/lib/query.js +314 -39
- package/lib/query.js.map +1 -1
- package/lib/query.spec.js +1 -0
- package/lib/query.spec.js.map +1 -1
- package/lib/statements.spec.js +1 -0
- package/lib/statements.spec.js.map +1 -1
- package/package.json +3 -2
package/lib/query.js
CHANGED
|
@@ -4,6 +4,151 @@ import { WebdaQLLexer } from "./WebdaQLLexer.js";
|
|
|
4
4
|
import { CountAllItemContext, CountDistinctItemContext, FieldItemContext, MetricItemContext, OrderFieldExpressionContext, SubExpressionContext, WebdaQLParserParser } from "./WebdaQLParserParser.js";
|
|
5
5
|
import { checkAlias, validateAggregation } from "./aggregation-query.js";
|
|
6
6
|
import { escapeValue, WebdaQLError } from "./webdaql-string.js";
|
|
7
|
+
/**
|
|
8
|
+
* A value computed at evaluation time
|
|
9
|
+
*/
|
|
10
|
+
export class DynamicValue {
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Dynamic value representing the current timestamp (in milliseconds since epoch) with an optional duration offset
|
|
14
|
+
*/
|
|
15
|
+
export class NowValue extends DynamicValue {
|
|
16
|
+
offsetMs;
|
|
17
|
+
raw;
|
|
18
|
+
name = "NOW";
|
|
19
|
+
/**
|
|
20
|
+
* @param offsetMs - offset in milliseconds to add to Date.now()
|
|
21
|
+
* @param raw - original duration string literal (if parsed from one)
|
|
22
|
+
*/
|
|
23
|
+
constructor(offsetMs = 0, raw) {
|
|
24
|
+
super();
|
|
25
|
+
this.offsetMs = offsetMs;
|
|
26
|
+
this.raw = raw;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Resolve to current timestamp + offset
|
|
30
|
+
* @returns timestamp in milliseconds
|
|
31
|
+
*/
|
|
32
|
+
resolve() {
|
|
33
|
+
return Date.now() + this.offsetMs;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* String representation of NOW function call
|
|
37
|
+
* @returns WQL NOW function string
|
|
38
|
+
*/
|
|
39
|
+
toString() {
|
|
40
|
+
if (this.raw !== undefined && this.raw !== "") {
|
|
41
|
+
return `NOW('${this.raw}')`;
|
|
42
|
+
}
|
|
43
|
+
if (this.offsetMs !== 0) {
|
|
44
|
+
if (this.offsetMs % 1000 === 0) {
|
|
45
|
+
return `NOW('${this.offsetMs / 1000}s')`;
|
|
46
|
+
}
|
|
47
|
+
return `NOW('${this.offsetMs}ms')`;
|
|
48
|
+
}
|
|
49
|
+
return "NOW()";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Supported duration units and their factor in seconds
|
|
54
|
+
*/
|
|
55
|
+
const DURATION_UNITS = {
|
|
56
|
+
s: 1,
|
|
57
|
+
sec: 1,
|
|
58
|
+
secs: 1,
|
|
59
|
+
second: 1,
|
|
60
|
+
seconds: 1,
|
|
61
|
+
m: 60,
|
|
62
|
+
min: 60,
|
|
63
|
+
mins: 60,
|
|
64
|
+
minute: 60,
|
|
65
|
+
minutes: 60,
|
|
66
|
+
h: 3600,
|
|
67
|
+
hr: 3600,
|
|
68
|
+
hour: 3600,
|
|
69
|
+
hours: 3600,
|
|
70
|
+
d: 86400,
|
|
71
|
+
day: 86400,
|
|
72
|
+
days: 86400,
|
|
73
|
+
mo: 86400 * 30,
|
|
74
|
+
mon: 86400 * 30,
|
|
75
|
+
month: 86400 * 30,
|
|
76
|
+
months: 86400 * 30,
|
|
77
|
+
y: 86400 * 365,
|
|
78
|
+
yr: 86400 * 365,
|
|
79
|
+
year: 86400 * 365,
|
|
80
|
+
years: 86400 * 365,
|
|
81
|
+
ms: 0.001,
|
|
82
|
+
msec: 0.001,
|
|
83
|
+
msecs: 0.001,
|
|
84
|
+
millisecond: 0.001,
|
|
85
|
+
milliseconds: 0.001
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Parse a duration string or numeric seconds into milliseconds.
|
|
89
|
+
*
|
|
90
|
+
* Supports an optional leading sign (`+` or `-`), and composite parts like `1d2h`, `10m`, `30`, `1.5h`.
|
|
91
|
+
* Numeric inputs are treated as seconds.
|
|
92
|
+
*
|
|
93
|
+
* @param input - duration string or numeric seconds
|
|
94
|
+
* @returns duration offset in milliseconds
|
|
95
|
+
* @throws {WebdaQLError} if the format or unit is invalid
|
|
96
|
+
*/
|
|
97
|
+
export function parseDurationToMs(input) {
|
|
98
|
+
if (typeof input === "number") {
|
|
99
|
+
if (!Number.isFinite(input)) {
|
|
100
|
+
throw new WebdaQLError(`Invalid duration: ${input}`);
|
|
101
|
+
}
|
|
102
|
+
return Math.round(input * 1000);
|
|
103
|
+
}
|
|
104
|
+
const raw = String(input).trim();
|
|
105
|
+
if (!raw) {
|
|
106
|
+
throw new WebdaQLError("Duration value cannot be empty");
|
|
107
|
+
}
|
|
108
|
+
let sign = 1;
|
|
109
|
+
let rest = raw;
|
|
110
|
+
if (rest.startsWith("-")) {
|
|
111
|
+
sign = -1;
|
|
112
|
+
rest = rest.slice(1).trim();
|
|
113
|
+
}
|
|
114
|
+
else if (rest.startsWith("+")) {
|
|
115
|
+
rest = rest.slice(1).trim();
|
|
116
|
+
}
|
|
117
|
+
if (!rest) {
|
|
118
|
+
throw new WebdaQLError(`Invalid duration format: ${raw}`);
|
|
119
|
+
}
|
|
120
|
+
const re = /(\d+(?:\.\d+)?)\s*([a-zA-Z]+)?/g;
|
|
121
|
+
let totalSeconds = 0;
|
|
122
|
+
let matched = false;
|
|
123
|
+
let lastIndex = 0;
|
|
124
|
+
let m;
|
|
125
|
+
while ((m = re.exec(rest)) !== null) {
|
|
126
|
+
const skipped = rest.slice(lastIndex, m.index).trim();
|
|
127
|
+
if (skipped) {
|
|
128
|
+
throw new WebdaQLError(`Invalid duration format: ${raw}`);
|
|
129
|
+
}
|
|
130
|
+
matched = true;
|
|
131
|
+
lastIndex = re.lastIndex;
|
|
132
|
+
const num = parseFloat(m[1]);
|
|
133
|
+
const unitStr = m[2];
|
|
134
|
+
if (unitStr) {
|
|
135
|
+
const u = unitStr.toLowerCase();
|
|
136
|
+
const factor = DURATION_UNITS[u];
|
|
137
|
+
if (factor === undefined) {
|
|
138
|
+
throw new WebdaQLError(`Unsupported duration unit: ${unitStr}`);
|
|
139
|
+
}
|
|
140
|
+
totalSeconds += num * factor;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
totalSeconds += num;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const trailing = rest.slice(lastIndex).trim();
|
|
147
|
+
if (!matched || trailing) {
|
|
148
|
+
throw new WebdaQLError(`Invalid duration format: ${raw}`);
|
|
149
|
+
}
|
|
150
|
+
return Math.round(sign * totalSeconds * 1000);
|
|
151
|
+
}
|
|
7
152
|
/**
|
|
8
153
|
* Strip the quotes of a WebdaQL string literal and unescape its contents.
|
|
9
154
|
*
|
|
@@ -151,8 +296,8 @@ export class ExpressionBuilder extends AbstractParseTreeVisitor {
|
|
|
151
296
|
* @param ctx - the order expression context
|
|
152
297
|
*/
|
|
153
298
|
visitOrderExpression(ctx) {
|
|
154
|
-
this.orderBy = ctx.children
|
|
155
|
-
|
|
299
|
+
this.orderBy = (ctx.children ?? [])
|
|
300
|
+
.filter(c => c instanceof OrderFieldExpressionContext)
|
|
156
301
|
.map((c) => this.visitOrderFieldExpression(c));
|
|
157
302
|
}
|
|
158
303
|
/**
|
|
@@ -161,7 +306,9 @@ export class ExpressionBuilder extends AbstractParseTreeVisitor {
|
|
|
161
306
|
* @returns the built Query
|
|
162
307
|
*/
|
|
163
308
|
visitWebdaql(ctx) {
|
|
164
|
-
|
|
309
|
+
const statement = ctx.statement();
|
|
310
|
+
const filter = ctx.filterQuery();
|
|
311
|
+
return statement ? this.visitStatement(statement) : filter ? this.visitFilterQuery(filter) : this.defaultResult();
|
|
165
312
|
}
|
|
166
313
|
/**
|
|
167
314
|
* Visit a DELETE, UPDATE or SELECT statement
|
|
@@ -242,9 +389,19 @@ export class ExpressionBuilder extends AbstractParseTreeVisitor {
|
|
|
242
389
|
* @returns the assignment
|
|
243
390
|
*/
|
|
244
391
|
visitAssignment(ctx) {
|
|
392
|
+
const raw = this.visit(ctx.getChild(2));
|
|
393
|
+
if (raw instanceof DynamicValue) {
|
|
394
|
+
return {
|
|
395
|
+
field: ctx.identifier().text,
|
|
396
|
+
rawValue: raw,
|
|
397
|
+
get value() {
|
|
398
|
+
return raw.resolve();
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
}
|
|
245
402
|
return {
|
|
246
403
|
field: ctx.identifier().text,
|
|
247
|
-
value:
|
|
404
|
+
value: raw
|
|
248
405
|
};
|
|
249
406
|
}
|
|
250
407
|
/**
|
|
@@ -327,8 +484,8 @@ export class ExpressionBuilder extends AbstractParseTreeVisitor {
|
|
|
327
484
|
*/
|
|
328
485
|
getComparison(ctx) {
|
|
329
486
|
const res = [];
|
|
330
|
-
|
|
331
|
-
let
|
|
487
|
+
let left = ctx.getChild(0);
|
|
488
|
+
let right = ctx.getChild(2);
|
|
332
489
|
if (right instanceof SubExpressionContext) {
|
|
333
490
|
right = right.getChild(1);
|
|
334
491
|
}
|
|
@@ -366,7 +523,9 @@ export class ExpressionBuilder extends AbstractParseTreeVisitor {
|
|
|
366
523
|
* @returns the comparison expression
|
|
367
524
|
*/
|
|
368
525
|
visitBinaryComparisonExpression(ctx) {
|
|
369
|
-
const
|
|
526
|
+
const left = ctx.getChild(0);
|
|
527
|
+
const op = ctx.getChild(1);
|
|
528
|
+
const right = ctx.getChild(2);
|
|
370
529
|
// @ts-ignore
|
|
371
530
|
return new ComparisonExpression(op.text, left.text, this.visit(right));
|
|
372
531
|
}
|
|
@@ -376,7 +535,7 @@ export class ExpressionBuilder extends AbstractParseTreeVisitor {
|
|
|
376
535
|
* @returns the array of values
|
|
377
536
|
*/
|
|
378
537
|
visitSetExpression(ctx) {
|
|
379
|
-
return ctx.children.filter((_i, id) => id % 2).map(c => this.visit(c));
|
|
538
|
+
return (ctx.children ?? []).filter((_i, id) => id % 2).map(c => this.visit(c));
|
|
380
539
|
}
|
|
381
540
|
/**
|
|
382
541
|
* Visit a LIKE expression (e.g. `field LIKE '%pattern_'`)
|
|
@@ -384,7 +543,8 @@ export class ExpressionBuilder extends AbstractParseTreeVisitor {
|
|
|
384
543
|
* @returns the comparison expression
|
|
385
544
|
*/
|
|
386
545
|
visitLikeExpression(ctx) {
|
|
387
|
-
const
|
|
546
|
+
const left = ctx.getChild(0);
|
|
547
|
+
const right = ctx.getChild(2);
|
|
388
548
|
const value = this.visit(right);
|
|
389
549
|
return new ComparisonExpression("LIKE", left.text, value);
|
|
390
550
|
}
|
|
@@ -394,7 +554,8 @@ export class ExpressionBuilder extends AbstractParseTreeVisitor {
|
|
|
394
554
|
* @returns the comparison expression
|
|
395
555
|
*/
|
|
396
556
|
visitInExpression(ctx) {
|
|
397
|
-
const
|
|
557
|
+
const left = ctx.getChild(0);
|
|
558
|
+
const right = ctx.getChild(2);
|
|
398
559
|
const value = this.visit(right);
|
|
399
560
|
return new ComparisonExpression("IN", left.text, value);
|
|
400
561
|
}
|
|
@@ -404,7 +565,8 @@ export class ExpressionBuilder extends AbstractParseTreeVisitor {
|
|
|
404
565
|
* @returns the comparison expression
|
|
405
566
|
*/
|
|
406
567
|
visitContainsExpression(ctx) {
|
|
407
|
-
const
|
|
568
|
+
const left = ctx.getChild(0);
|
|
569
|
+
const right = ctx.getChild(2);
|
|
408
570
|
const value = this.visit(right);
|
|
409
571
|
return new ComparisonExpression("CONTAINS", left.text, value);
|
|
410
572
|
}
|
|
@@ -480,6 +642,34 @@ export class ExpressionBuilder extends AbstractParseTreeVisitor {
|
|
|
480
642
|
visitNumberLiteral(ctx) {
|
|
481
643
|
return parseFloat(ctx.text);
|
|
482
644
|
}
|
|
645
|
+
/**
|
|
646
|
+
* Visit a NOW() function call with optional duration offset
|
|
647
|
+
* @param ctx - the now function context
|
|
648
|
+
* @returns the NowValue
|
|
649
|
+
*/
|
|
650
|
+
visitNowFunction(ctx) {
|
|
651
|
+
const stringLiteral = ctx.stringLiteral();
|
|
652
|
+
if (stringLiteral) {
|
|
653
|
+
const raw = unescapeStringLiteral(stringLiteral.text);
|
|
654
|
+
const offsetMs = parseDurationToMs(raw);
|
|
655
|
+
return new NowValue(offsetMs, raw);
|
|
656
|
+
}
|
|
657
|
+
const integerLiteral = ctx.integerLiteral();
|
|
658
|
+
if (integerLiteral) {
|
|
659
|
+
const seconds = parseInt(integerLiteral.text);
|
|
660
|
+
return new NowValue(seconds * 1000, integerLiteral.text);
|
|
661
|
+
}
|
|
662
|
+
const numberLiteral = ctx.numberLiteral();
|
|
663
|
+
if (numberLiteral) {
|
|
664
|
+
const seconds = parseFloat(numberLiteral.text);
|
|
665
|
+
return new NowValue(Math.round(seconds * 1000), numberLiteral.text);
|
|
666
|
+
}
|
|
667
|
+
const parameter = ctx.parameter();
|
|
668
|
+
if (parameter) {
|
|
669
|
+
return this.visitParameter(parameter);
|
|
670
|
+
}
|
|
671
|
+
return new NowValue(0);
|
|
672
|
+
}
|
|
483
673
|
/**
|
|
484
674
|
* A `?` or `:name` placeholder reaching evaluation was never bound
|
|
485
675
|
* @param ctx - the parameter context
|
|
@@ -514,9 +704,9 @@ export const UNARY_COMPARISON_OPERATORS = ["IS NULL", "IS NOT NULL"];
|
|
|
514
704
|
*/
|
|
515
705
|
export class ComparisonExpression extends Expression {
|
|
516
706
|
/**
|
|
517
|
-
* Right side of the comparison
|
|
707
|
+
* Right side of the comparison (raw parsed value, possibly DynamicValue)
|
|
518
708
|
*/
|
|
519
|
-
|
|
709
|
+
rawValue;
|
|
520
710
|
/**
|
|
521
711
|
* Attribute to read from the object (split by .)
|
|
522
712
|
*/
|
|
@@ -524,13 +714,45 @@ export class ComparisonExpression extends Expression {
|
|
|
524
714
|
/**
|
|
525
715
|
* @param operator - comparison operator
|
|
526
716
|
* @param attribute - dot-notation path to the object property (e.g. `"user.name"`)
|
|
527
|
-
* @param
|
|
717
|
+
* @param rawValue - literal value, DynamicValue, or array of values (for IN operator) to compare against
|
|
528
718
|
*/
|
|
529
|
-
constructor(operator, attribute,
|
|
719
|
+
constructor(operator, attribute, rawValue) {
|
|
530
720
|
super(operator);
|
|
531
|
-
this.
|
|
721
|
+
this.rawValue = rawValue;
|
|
532
722
|
this.attribute = attribute.split(".");
|
|
533
723
|
}
|
|
724
|
+
/**
|
|
725
|
+
* Resolved value at evaluation time
|
|
726
|
+
* @returns the resolved scalar or array value
|
|
727
|
+
*/
|
|
728
|
+
get value() {
|
|
729
|
+
if (this.rawValue instanceof DynamicValue) {
|
|
730
|
+
return this.rawValue.resolve();
|
|
731
|
+
}
|
|
732
|
+
if (Array.isArray(this.rawValue)) {
|
|
733
|
+
return this.rawValue.map(v => (v instanceof DynamicValue ? v.resolve() : v));
|
|
734
|
+
}
|
|
735
|
+
return this.rawValue;
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Set the raw value
|
|
739
|
+
* @param val - new value
|
|
740
|
+
*/
|
|
741
|
+
set value(val) {
|
|
742
|
+
this.rawValue = val;
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* @override
|
|
746
|
+
*/
|
|
747
|
+
isDynamic() {
|
|
748
|
+
if (this.rawValue instanceof DynamicValue) {
|
|
749
|
+
return true;
|
|
750
|
+
}
|
|
751
|
+
if (Array.isArray(this.rawValue)) {
|
|
752
|
+
return this.rawValue.some(v => v instanceof DynamicValue);
|
|
753
|
+
}
|
|
754
|
+
return false;
|
|
755
|
+
}
|
|
534
756
|
/**
|
|
535
757
|
* Convert a SQL LIKE pattern to a JavaScript RegExp
|
|
536
758
|
*
|
|
@@ -597,19 +819,27 @@ export class ComparisonExpression extends Expression {
|
|
|
597
819
|
* @override
|
|
598
820
|
*/
|
|
599
821
|
eval(target) {
|
|
600
|
-
|
|
822
|
+
let left = ComparisonExpression.getAttributeValue(target, this.attribute);
|
|
823
|
+
// WebdaQL has no date literal: a date compared with a string compares as its ISO 8601 form, the way it is
|
|
824
|
+
// serialized; comparisons with numbers keep the native Date behavior
|
|
825
|
+
if (left instanceof Date &&
|
|
826
|
+
!isNaN(left.getTime()) &&
|
|
827
|
+
this.operator !== "LIKE" &&
|
|
828
|
+
(typeof this.value === "string" || (Array.isArray(this.value) && this.value.some(v => typeof v === "string")))) {
|
|
829
|
+
left = left.toISOString();
|
|
830
|
+
}
|
|
601
831
|
switch (this.operator) {
|
|
602
832
|
case "=":
|
|
603
833
|
// ignore strong type on purpose
|
|
604
834
|
return left == this.value;
|
|
605
835
|
case "<=":
|
|
606
|
-
return left <= this.value;
|
|
836
|
+
return left <= (this.value ?? 0);
|
|
607
837
|
case ">=":
|
|
608
|
-
return left >= this.value;
|
|
838
|
+
return left >= (this.value ?? 0);
|
|
609
839
|
case "<":
|
|
610
|
-
return left < this.value;
|
|
840
|
+
return left < (this.value ?? 0);
|
|
611
841
|
case ">":
|
|
612
|
-
return left > this.value;
|
|
842
|
+
return left > (this.value ?? 0);
|
|
613
843
|
case "!=":
|
|
614
844
|
return left != this.value;
|
|
615
845
|
case "LIKE":
|
|
@@ -638,10 +868,14 @@ export class ComparisonExpression extends Expression {
|
|
|
638
868
|
* Serialize a value to its WebdaQL string representation
|
|
639
869
|
*
|
|
640
870
|
* Strings are double-quoted, booleans are uppercased, arrays are bracket-wrapped.
|
|
871
|
+
* Dynamic values serialize to their canonical function call string (e.g. `NOW('-10m')`).
|
|
641
872
|
* @param value - the value to serialize
|
|
642
873
|
* @returns the serialized string
|
|
643
874
|
*/
|
|
644
875
|
toStringValue(value) {
|
|
876
|
+
if (value instanceof DynamicValue) {
|
|
877
|
+
return value.toString();
|
|
878
|
+
}
|
|
645
879
|
if (Array.isArray(value)) {
|
|
646
880
|
return `[${value.map(v => this.toStringValue(v)).join(", ")}]`;
|
|
647
881
|
}
|
|
@@ -651,7 +885,7 @@ export class ComparisonExpression extends Expression {
|
|
|
651
885
|
case "boolean":
|
|
652
886
|
return value.toString().toUpperCase();
|
|
653
887
|
}
|
|
654
|
-
return value
|
|
888
|
+
return value === null ? "NULL" : value.toString();
|
|
655
889
|
}
|
|
656
890
|
/**
|
|
657
891
|
* Allow subclass to create different display
|
|
@@ -674,7 +908,7 @@ export class ComparisonExpression extends Expression {
|
|
|
674
908
|
if (UNARY_COMPARISON_OPERATORS.includes(this.operator)) {
|
|
675
909
|
return `${this.toStringAttribute()} ${this.toStringOperator()}`;
|
|
676
910
|
}
|
|
677
|
-
return `${this.toStringAttribute()} ${this.toStringOperator()} ${this.toStringValue(this.
|
|
911
|
+
return `${this.toStringAttribute()} ${this.toStringOperator()} ${this.toStringValue(this.rawValue)}`;
|
|
678
912
|
}
|
|
679
913
|
}
|
|
680
914
|
/**
|
|
@@ -704,6 +938,12 @@ export class LogicalExpression extends Expression {
|
|
|
704
938
|
}
|
|
705
939
|
return this.children.map(c => c.toString(depth + 1)).join(` ${this.operator} `);
|
|
706
940
|
}
|
|
941
|
+
/**
|
|
942
|
+
* @override
|
|
943
|
+
*/
|
|
944
|
+
isDynamic() {
|
|
945
|
+
return this.children.some(c => c.isDynamic());
|
|
946
|
+
}
|
|
707
947
|
}
|
|
708
948
|
/**
|
|
709
949
|
* Logical AND expression — evaluates to `true` only if all children are `true`
|
|
@@ -786,6 +1026,12 @@ export class BooleanExpression extends Expression {
|
|
|
786
1026
|
toString() {
|
|
787
1027
|
return this.operator;
|
|
788
1028
|
}
|
|
1029
|
+
/**
|
|
1030
|
+
* @override
|
|
1031
|
+
*/
|
|
1032
|
+
isDynamic() {
|
|
1033
|
+
return false;
|
|
1034
|
+
}
|
|
789
1035
|
}
|
|
790
1036
|
/**
|
|
791
1037
|
* Build an AND / OR expression, folding `TRUE` / `FALSE` constants away
|
|
@@ -839,18 +1085,32 @@ function throwingErrorListener(sql) {
|
|
|
839
1085
|
};
|
|
840
1086
|
}
|
|
841
1087
|
/**
|
|
842
|
-
* Create a lexer and a parser for a query
|
|
1088
|
+
* Create a lexer and a parser for a query
|
|
843
1089
|
* @param sql - the query string
|
|
844
|
-
* @
|
|
1090
|
+
* @param options - parser options
|
|
1091
|
+
* @returns the lexer, parser and token stream
|
|
845
1092
|
*/
|
|
846
|
-
function createParser(sql) {
|
|
1093
|
+
export function createParser(sql, options) {
|
|
847
1094
|
const lexer = new WebdaQLLexer(CharStreams.fromString(sql || ""));
|
|
848
1095
|
lexer.removeErrorListeners();
|
|
849
|
-
|
|
850
|
-
|
|
1096
|
+
if (!options?.tolerant) {
|
|
1097
|
+
lexer.addErrorListener(throwingErrorListener(sql));
|
|
1098
|
+
}
|
|
1099
|
+
const tokens = new CommonTokenStream(lexer);
|
|
1100
|
+
const parser = new WebdaQLParserParser(tokens);
|
|
851
1101
|
parser.removeErrorListeners();
|
|
852
|
-
|
|
853
|
-
|
|
1102
|
+
if (!options?.tolerant) {
|
|
1103
|
+
parser.addErrorListener(throwingErrorListener(sql));
|
|
1104
|
+
}
|
|
1105
|
+
return { lexer, parser, tokens };
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Create a tolerant lexer and parser that do not throw on syntax errors
|
|
1109
|
+
* @param sql - the query string
|
|
1110
|
+
* @returns the lexer, parser and token stream
|
|
1111
|
+
*/
|
|
1112
|
+
export function createTolerantParser(sql) {
|
|
1113
|
+
return createParser(sql, { tolerant: true });
|
|
854
1114
|
}
|
|
855
1115
|
/**
|
|
856
1116
|
* Check a query against the WebdaQL grammar without evaluating it
|
|
@@ -942,7 +1202,7 @@ export class QueryValidator {
|
|
|
942
1202
|
* @param type - logical operator to combine filters (`"AND"` or `"OR"`)
|
|
943
1203
|
* @returns `this` for chaining
|
|
944
1204
|
*/
|
|
945
|
-
merge(query, type = "AND") {
|
|
1205
|
+
merge(query = "", type = "AND") {
|
|
946
1206
|
const adds = new QueryValidator(query);
|
|
947
1207
|
if (adds.query.type !== "SELECT" || adds.query.fields) {
|
|
948
1208
|
throw new SyntaxError(`Only a filter can be merged into a query, not a statement (Query: ${query})`);
|
|
@@ -1010,6 +1270,13 @@ export class QueryValidator {
|
|
|
1010
1270
|
eval(target) {
|
|
1011
1271
|
return this.query.filter.eval(target);
|
|
1012
1272
|
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Check whether this query's filter contains any dynamic values
|
|
1275
|
+
* @returns true if any child comparison holds a DynamicValue
|
|
1276
|
+
*/
|
|
1277
|
+
isDynamic() {
|
|
1278
|
+
return this.query.filter.isDynamic();
|
|
1279
|
+
}
|
|
1013
1280
|
/**
|
|
1014
1281
|
* Display parse tree back as query
|
|
1015
1282
|
* @param tree - the parse tree to display
|
|
@@ -1205,7 +1472,8 @@ export class PartialExpressionBuilder extends ExpressionBuilder {
|
|
|
1205
1472
|
* @returns the partial comparison expression
|
|
1206
1473
|
*/
|
|
1207
1474
|
visitLikeExpression(ctx) {
|
|
1208
|
-
const
|
|
1475
|
+
const left = ctx.getChild(0);
|
|
1476
|
+
const right = ctx.getChild(2);
|
|
1209
1477
|
const value = this.visit(right);
|
|
1210
1478
|
return new PartialComparisonExpression(this, "LIKE", left.text, value);
|
|
1211
1479
|
}
|
|
@@ -1215,7 +1483,9 @@ export class PartialExpressionBuilder extends ExpressionBuilder {
|
|
|
1215
1483
|
* @returns the partial comparison expression
|
|
1216
1484
|
*/
|
|
1217
1485
|
visitBinaryComparisonExpression(ctx) {
|
|
1218
|
-
const
|
|
1486
|
+
const left = ctx.getChild(0);
|
|
1487
|
+
const op = ctx.getChild(1);
|
|
1488
|
+
const right = ctx.getChild(2);
|
|
1219
1489
|
// @ts-ignore
|
|
1220
1490
|
return new PartialComparisonExpression(this, op.text, left.text, this.visit(right));
|
|
1221
1491
|
}
|
|
@@ -1225,7 +1495,8 @@ export class PartialExpressionBuilder extends ExpressionBuilder {
|
|
|
1225
1495
|
* @returns the partial comparison expression
|
|
1226
1496
|
*/
|
|
1227
1497
|
visitInExpression(ctx) {
|
|
1228
|
-
const
|
|
1498
|
+
const left = ctx.getChild(0);
|
|
1499
|
+
const right = ctx.getChild(2);
|
|
1229
1500
|
const value = this.visit(right);
|
|
1230
1501
|
return new PartialComparisonExpression(this, "IN", left.text, value);
|
|
1231
1502
|
}
|
|
@@ -1235,7 +1506,8 @@ export class PartialExpressionBuilder extends ExpressionBuilder {
|
|
|
1235
1506
|
* @returns the partial comparison expression
|
|
1236
1507
|
*/
|
|
1237
1508
|
visitContainsExpression(ctx) {
|
|
1238
|
-
const
|
|
1509
|
+
const left = ctx.getChild(0);
|
|
1510
|
+
const right = ctx.getChild(2);
|
|
1239
1511
|
const value = this.visit(right);
|
|
1240
1512
|
return new PartialComparisonExpression(this, "CONTAINS", left.text, value);
|
|
1241
1513
|
}
|
|
@@ -1282,7 +1554,7 @@ export function stringifyQuery(query) {
|
|
|
1282
1554
|
head = "DELETE";
|
|
1283
1555
|
}
|
|
1284
1556
|
else if (query.type === "UPDATE") {
|
|
1285
|
-
head = `UPDATE SET ${(query.assignments ?? []).map(a => new ComparisonExpression("=", a.field, a.value).toString()).join(", ")}`;
|
|
1557
|
+
head = `UPDATE SET ${(query.assignments ?? []).map(a => new ComparisonExpression("=", a.field, a.rawValue ?? a.value).toString()).join(", ")}`;
|
|
1286
1558
|
}
|
|
1287
1559
|
else if (query.aggregation) {
|
|
1288
1560
|
const metrics = Object.entries(query.aggregation.metrics).map(([alias, metric]) => {
|
|
@@ -1321,7 +1593,7 @@ const FIELD_SEGMENT = /^[A-Za-z0-9_]+$/;
|
|
|
1321
1593
|
* @returns the dotted path
|
|
1322
1594
|
* @throws {WebdaQLError} when a segment is empty or holds anything but letters, digits and `_`
|
|
1323
1595
|
*/
|
|
1324
|
-
function checkFieldPath(field) {
|
|
1596
|
+
export function checkFieldPath(field) {
|
|
1325
1597
|
// Copy first: what is checked is what is printed, whatever the object does on later reads
|
|
1326
1598
|
const segments = Array.isArray(field) ? snapshot(field) : typeof field === "string" ? field.split(".") : undefined;
|
|
1327
1599
|
if (!segments?.length || segments.some(segment => typeof segment !== "string" || !FIELD_SEGMENT.test(segment))) {
|
|
@@ -1349,6 +1621,9 @@ function snapshot(value) {
|
|
|
1349
1621
|
* @throws {WebdaQLError} for anything but a string, a finite number or a boolean
|
|
1350
1622
|
*/
|
|
1351
1623
|
function checkScalar(value) {
|
|
1624
|
+
if (value instanceof DynamicValue) {
|
|
1625
|
+
return value.toString();
|
|
1626
|
+
}
|
|
1352
1627
|
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
|
|
1353
1628
|
throw new WebdaQLError(`Invalid value in WebdaQL query: ${typeof value}`);
|
|
1354
1629
|
}
|
|
@@ -1387,7 +1662,7 @@ function printExpression(expression, nested) {
|
|
|
1387
1662
|
if (expression instanceof ComparisonExpression) {
|
|
1388
1663
|
const attribute = checkFieldPath(expression.attribute);
|
|
1389
1664
|
const operator = expression.operator;
|
|
1390
|
-
const value = expression.value;
|
|
1665
|
+
const value = expression.rawValue !== undefined ? expression.rawValue : expression.value;
|
|
1391
1666
|
if (typeof operator !== "string") {
|
|
1392
1667
|
throw new WebdaQLError("Invalid operator in WebdaQL query");
|
|
1393
1668
|
}
|
|
@@ -1448,7 +1723,7 @@ export function normalizeQuery(query) {
|
|
|
1448
1723
|
head = `UPDATE SET ${assignments
|
|
1449
1724
|
.map(a => {
|
|
1450
1725
|
const field = a?.field;
|
|
1451
|
-
const value = a?.value;
|
|
1726
|
+
const value = a?.rawValue ?? a?.value;
|
|
1452
1727
|
return `${checkFieldPath(field)} = ${checkScalar(value)}`;
|
|
1453
1728
|
})
|
|
1454
1729
|
.join(", ")}`;
|